diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 596615308c..d89926d6ad 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,8 @@ stages: - build - deploy + - verifysanity + - verify variables: LANG: "en_US.UTF-8" @@ -50,3 +52,39 @@ deploy_beta_testflight: - bash buildbox/deploy-telegram.sh appstore environment: name: testflight_llc + +verifysanity_beta_testflight: + tags: + - ios_beta + stage: verifysanity + only: + - beta + except: + - tags + script: + - bash buildbox/verify-telegram.sh appstore cached + environment: + name: testflight_llc + artifacts: + when: on_failure + paths: + - build/verifysanity_artifacts + expire_in: 1 week + +verify_beta_testflight: + tags: + - ios_beta + stage: verify + only: + - beta + except: + - tags + script: + - bash buildbox/verify-telegram.sh appstore full + environment: + name: testflight_llc + artifacts: + when: on_failure + paths: + - build/verify_artifacts + expire_in: 1 week diff --git a/.gitmodules b/.gitmodules index 84449c1aa0..766b93a2ea 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,10 +1,10 @@ [submodule "submodules/rlottie/rlottie"] path = submodules/rlottie/rlottie - url = https://github.com/laktyushin/rlottie.git + url=../rlottie.git [submodule "build-system/bazel-rules/rules_apple"] path = build-system/bazel-rules/rules_apple -url=https://github.com/ali-fareed/rules_apple.git + url=https://github.com/ali-fareed/rules_apple.git [submodule "build-system/bazel-rules/rules_swift"] path = build-system/bazel-rules/rules_swift url = https://github.com/bazelbuild/rules_swift.git diff --git a/BUCK b/BUCK index fe788a304d..aff0dc7694 100644 --- a/BUCK +++ b/BUCK @@ -48,8 +48,8 @@ resource_dependencies = [ "//submodules/LegacyComponents:LegacyComponentsResources", "//submodules/TelegramUI:TelegramUIAssets", "//submodules/TelegramUI:TelegramUIResources", - "//submodules/WalletUI:WalletUIAssets", - "//submodules/WalletUI:WalletUIResources", + #"//submodules/WalletUI:WalletUIAssets", + #"//submodules/WalletUI:WalletUIResources", "//submodules/PasswordSetupUI:PasswordSetupUIResources", "//submodules/PasswordSetupUI:PasswordSetupUIAssets", "//submodules/OverlayStatusController:OverlayStatusControllerResources", @@ -464,8 +464,9 @@ apple_binary( "-DTARGET_OS_WATCH=1", ], linker_flags = [ - #"-e", - #"_NSExtensionMain", + "-e", + "_WKExtensionMain", + "-lWKExtensionMainLegacy", ], configs = watch_extension_binary_configs(), frameworks = [ diff --git a/Config/app_configuration.bzl b/Config/app_configuration.bzl index dbc6f547a5..da3e824e1e 100644 --- a/Config/app_configuration.bzl +++ b/Config/app_configuration.bzl @@ -2,7 +2,7 @@ def appConfig(): apiId = native.read_config("custom", "apiId") apiHash = native.read_config("custom", "apiHash") - hockeyAppId = native.read_config("custom", "hockeyAppId") + appCenterId = native.read_config("custom", "appCenterId") isInternalBuild = native.read_config("custom", "isInternalBuild") isAppStoreBuild = native.read_config("custom", "isAppStoreBuild") appStoreId = native.read_config("custom", "appStoreId") @@ -11,7 +11,7 @@ def appConfig(): return { "apiId": apiId, "apiHash": apiHash, - "hockeyAppId": hockeyAppId, + "appCenterId": appCenterId, "isInternalBuild": isInternalBuild, "isAppStoreBuild": isAppStoreBuild, "appStoreId": appStoreId, diff --git a/Config/buck_rule_macros.bzl b/Config/buck_rule_macros.bzl index 6ec78c36be..e597aa6ecd 100644 --- a/Config/buck_rule_macros.bzl +++ b/Config/buck_rule_macros.bzl @@ -21,6 +21,8 @@ def apple_lib( deps = [], exported_deps = [], additional_linker_flags = None, + exported_preprocessor_flags = [], + exported_linker_flags = [], frameworks = [], weak_frameworks = [], swift_version = None, @@ -114,7 +116,7 @@ def apple_lib( else: linker_flags = [] - resolved_exported_linker_flags = linker_flags + additional_linker_flags + resolved_exported_linker_flags = exported_linker_flags + linker_flags + additional_linker_flags if native.read_config("custom", "mode") == "project": resolved_frameworks = resolved_frameworks + ["$SDKROOT/System/Library/Frameworks/%s.framework" % x for x in weak_frameworks] @@ -140,6 +142,7 @@ def apple_lib( platform_compiler_flags = platform_compiler_flags, swift_compiler_flags = swift_compiler_flags, preferred_linkage = "static", + exported_preprocessor_flags = exported_preprocessor_flags, ) def static_library( @@ -152,6 +155,8 @@ def static_library( extra_xcode_files = [], deps = [], additional_linker_flags = None, + exported_preprocessor_flags = [], + exported_linker_flags = [], frameworks = [], weak_frameworks = [], info_plist = None, @@ -161,7 +166,8 @@ def static_library( platform_compiler_flags = None, swift_compiler_flags = None, warning_as_error = False, - suppress_warnings = True): + suppress_warnings = True + ): apple_lib( name = name, srcs = srcs, @@ -175,6 +181,8 @@ def static_library( extra_xcode_files = extra_xcode_files, deps = deps, additional_linker_flags = additional_linker_flags, + exported_preprocessor_flags = exported_preprocessor_flags, + exported_linker_flags = exported_linker_flags, frameworks = frameworks, weak_frameworks = weak_frameworks, warning_as_error = warning_as_error, diff --git a/Makefile b/Makefile index 86cb7456a9..d8a8f39a01 100644 --- a/Makefile +++ b/Makefile @@ -3,14 +3,14 @@ include Utils.makefile BUCK_OPTIONS=\ - --config custom.appVersion="5.12.2" \ + --config custom.appVersion="5.15.1" \ --config custom.developmentCodeSignIdentity="${DEVELOPMENT_CODE_SIGN_IDENTITY}" \ --config custom.distributionCodeSignIdentity="${DISTRIBUTION_CODE_SIGN_IDENTITY}" \ --config custom.developmentTeam="${DEVELOPMENT_TEAM}" \ --config custom.baseApplicationBundleId="${BUNDLE_ID}" \ --config custom.apiId="${API_ID}" \ --config custom.apiHash="${API_HASH}" \ - --config custom.hockeyAppId="${HOCKEYAPP_ID}" \ + --config custom.appCenterId="${APP_CENTER_ID}" \ --config custom.isInternalBuild="${IS_INTERNAL_BUILD}" \ --config custom.isAppStoreBuild="${IS_APPSTORE_BUILD}" \ --config custom.appStoreId="${APPSTORE_ID}" \ @@ -356,7 +356,7 @@ build_verbose: check_env //:NotificationContentExtension#dwarf-and-dsym,iphoneos-arm64 \ //:NotificationServiceExtension#dwarf-and-dsym,iphoneos-arm64 \ //:IntentsExtension#dwarf-and-dsym,iphoneos-arm64 \ - --verbose 8 ${BUCK_OPTIONS} ${BUCK_THREADS_OPTIONS} ${BUCK_DEBUG_OPTIONS} + --verbose 7 ${BUCK_OPTIONS} ${BUCK_THREADS_OPTIONS} ${BUCK_DEBUG_OPTIONS} ${BUCK_CACHE_OPTIONS} deps: check_env $(BUCK) query "deps(//:AppPackage)" --dot \ diff --git a/NotificationContent/NotificationViewController.swift b/NotificationContent/NotificationViewController.swift index 9f5f7e31e6..8a6372ecdd 100644 --- a/NotificationContent/NotificationViewController.swift +++ b/NotificationContent/NotificationViewController.swift @@ -38,7 +38,7 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown" - self.impl = NotificationViewControllerImpl(initializationData: NotificationViewControllerInitializationData(appGroupPath: appGroupUrl.path, apiId: buildConfig.apiId, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), setPreferredContentSize: { [weak self] size in + self.impl = NotificationViewControllerImpl(initializationData: NotificationViewControllerInitializationData(appGroupPath: appGroupUrl.path, apiId: buildConfig.apiId, apiHash: buildConfig.apiHash, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), setPreferredContentSize: { [weak self] size in self?.preferredContentSize = size }) } diff --git a/NotificationService/InAppNotificationSettings.swift b/NotificationService/InAppNotificationSettings.swift index a5f3ce36cc..ea6e849c8f 100644 --- a/NotificationService/InAppNotificationSettings.swift +++ b/NotificationService/InAppNotificationSettings.swift @@ -40,7 +40,7 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable { public var displayNotificationsFromAllAccounts: Bool public static var defaultSettings: InAppNotificationSettings { - return InAppNotificationSettings(playSounds: true, vibrate: false, displayPreviews: true, totalUnreadCountDisplayStyle: .filtered, totalUnreadCountDisplayCategory: .messages, totalUnreadCountIncludeTags: [.regularChatsAndPrivateGroups], displayNameOnLockscreen: true, displayNotificationsFromAllAccounts: true) + return InAppNotificationSettings(playSounds: true, vibrate: false, displayPreviews: true, totalUnreadCountDisplayStyle: .filtered, totalUnreadCountDisplayCategory: .messages, totalUnreadCountIncludeTags: [.privateChat, .secretChat, .bot, .privateGroup], displayNameOnLockscreen: true, displayNotificationsFromAllAccounts: true) } public init(playSounds: Bool, vibrate: Bool, displayPreviews: Bool, totalUnreadCountDisplayStyle: TotalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: TotalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: PeerSummaryCounterTags, displayNameOnLockscreen: Bool, displayNotificationsFromAllAccounts: Bool) { @@ -60,10 +60,25 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable { self.displayPreviews = decoder.decodeInt32ForKey("p", orElse: 0) != 0 self.totalUnreadCountDisplayStyle = TotalUnreadCountDisplayStyle(rawValue: decoder.decodeInt32ForKey("cds", orElse: 0)) ?? .filtered self.totalUnreadCountDisplayCategory = TotalUnreadCountDisplayCategory(rawValue: decoder.decodeInt32ForKey("totalUnreadCountDisplayCategory", orElse: 1)) ?? .messages - if let value = decoder.decodeOptionalInt32ForKey("totalUnreadCountIncludeTags") { + if let value = decoder.decodeOptionalInt32ForKey("totalUnreadCountIncludeTags_2") { self.totalUnreadCountIncludeTags = PeerSummaryCounterTags(rawValue: value) + } else if let value = decoder.decodeOptionalInt32ForKey("totalUnreadCountIncludeTags") { + var resultTags: PeerSummaryCounterTags = [] + for legacyTag in LegacyPeerSummaryCounterTags(rawValue: value) { + if legacyTag == .regularChatsAndPrivateGroups { + resultTags.insert(.privateChat) + resultTags.insert(.secretChat) + resultTags.insert(.bot) + resultTags.insert(.privateGroup) + } else if legacyTag == .publicGroups { + resultTags.insert(.publicGroup) + } else if legacyTag == .channels { + resultTags.insert(.channel) + } + } + self.totalUnreadCountIncludeTags = resultTags } else { - self.totalUnreadCountIncludeTags = [.regularChatsAndPrivateGroups] + self.totalUnreadCountIncludeTags = [.privateChat, .secretChat, .bot, .privateGroup] } self.displayNameOnLockscreen = decoder.decodeInt32ForKey("displayNameOnLockscreen", orElse: 1) != 0 self.displayNotificationsFromAllAccounts = decoder.decodeInt32ForKey("displayNotificationsFromAllAccounts", orElse: 1) != 0 @@ -75,7 +90,7 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable { encoder.encodeInt32(self.displayPreviews ? 1 : 0, forKey: "p") encoder.encodeInt32(self.totalUnreadCountDisplayStyle.rawValue, forKey: "cds") encoder.encodeInt32(self.totalUnreadCountDisplayCategory.rawValue, forKey: "totalUnreadCountDisplayCategory") - encoder.encodeInt32(self.totalUnreadCountIncludeTags.rawValue, forKey: "totalUnreadCountIncludeTags") + encoder.encodeInt32(self.totalUnreadCountIncludeTags.rawValue, forKey: "totalUnreadCountIncludeTags_2") encoder.encodeInt32(self.displayNameOnLockscreen ? 1 : 0, forKey: "displayNameOnLockscreen") encoder.encodeInt32(self.displayNotificationsFromAllAccounts ? 1 : 0, forKey: "displayNotificationsFromAllAccounts") } diff --git a/NotificationService/Namespaces.swift b/NotificationService/Namespaces.swift index b60b7ff89a..d8aed1ec40 100644 --- a/NotificationService/Namespaces.swift +++ b/NotificationService/Namespaces.swift @@ -1,9 +1,43 @@ import PostboxDataTypes +struct LegacyPeerSummaryCounterTags: OptionSet, Sequence, Hashable { + var rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue + } + + static let regularChatsAndPrivateGroups = LegacyPeerSummaryCounterTags(rawValue: 1 << 0) + static let publicGroups = LegacyPeerSummaryCounterTags(rawValue: 1 << 1) + static let channels = LegacyPeerSummaryCounterTags(rawValue: 1 << 2) + + public func makeIterator() -> AnyIterator { + var index = 0 + return AnyIterator { () -> LegacyPeerSummaryCounterTags? in + while index < 31 { + let currentTags = self.rawValue >> UInt32(index) + let tag = LegacyPeerSummaryCounterTags(rawValue: 1 << UInt32(index)) + index += 1 + if currentTags == 0 { + break + } + + if (currentTags & 1) != 0 { + return tag + } + } + return nil + } + } +} + extension PeerSummaryCounterTags { - static let regularChatsAndPrivateGroups = PeerSummaryCounterTags(rawValue: 1 << 0) - static let publicGroups = PeerSummaryCounterTags(rawValue: 1 << 1) - static let channels = PeerSummaryCounterTags(rawValue: 1 << 2) + static let privateChat = PeerSummaryCounterTags(rawValue: 1 << 3) + static let secretChat = PeerSummaryCounterTags(rawValue: 1 << 4) + static let privateGroup = PeerSummaryCounterTags(rawValue: 1 << 5) + static let bot = PeerSummaryCounterTags(rawValue: 1 << 6) + static let channel = PeerSummaryCounterTags(rawValue: 1 << 7) + static let publicGroup = PeerSummaryCounterTags(rawValue: 1 << 8) } struct Namespaces { @@ -17,4 +51,4 @@ struct Namespaces { static let CloudChannel: Int32 = 2 static let SecretChat: Int32 = 3 } -} \ No newline at end of file +} diff --git a/NotificationService/Serialization.m b/NotificationService/Serialization.m index 6f6f9191a5..f7eea7807e 100644 --- a/NotificationService/Serialization.m +++ b/NotificationService/Serialization.m @@ -3,7 +3,7 @@ @implementation Serialization - (NSUInteger)currentLayer { - return 106; + return 110; } - (id _Nullable)parseMessage:(NSData * _Nullable)data { diff --git a/NotificationService/Sync.swift b/NotificationService/Sync.swift index 21a93f439b..26fdd95d42 100644 --- a/NotificationService/Sync.swift +++ b/NotificationService/Sync.swift @@ -94,19 +94,21 @@ enum SyncProviderImpl { if let channel = peerTable.get(peerId) as? TelegramChannel { switch channel.info { case .broadcast: - tag = .channels + tag = .channel case .group: if channel.username != nil { - tag = .publicGroups + tag = .publicGroup } else { - tag = .regularChatsAndPrivateGroups + tag = .privateGroup } } } else { - tag = .channels + tag = .channel } + } else if peerId.namespace == Namespaces.Peer.CloudGroup { + tag = .privateGroup } else { - tag = .regularChatsAndPrivateGroups + tag = .privateChat } var totalCount: Int32 = -1 diff --git a/Share/ShareRootController.swift b/Share/ShareRootController.swift index 5dac1a0097..c0da95155d 100644 --- a/Share/ShareRootController.swift +++ b/Share/ShareRootController.swift @@ -45,7 +45,7 @@ class ShareRootController: UIViewController { let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown" - self.impl = ShareRootControllerImpl(initializationData: ShareRootControllerInitializationData(appGroupPath: appGroupUrl.path, apiId: buildConfig.apiId, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), getExtensionContext: { [weak self] in + self.impl = ShareRootControllerImpl(initializationData: ShareRootControllerInitializationData(appGroupPath: appGroupUrl.path, apiId: buildConfig.apiId, apiHash: buildConfig.apiHash, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), getExtensionContext: { [weak self] in return self?.extensionContext }) } diff --git a/SiriIntents/IntentHandler.swift b/SiriIntents/IntentHandler.swift index 694cbce0ae..efc0b4076a 100644 --- a/SiriIntents/IntentHandler.swift +++ b/SiriIntents/IntentHandler.swift @@ -69,6 +69,7 @@ public class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo let buildConfig = BuildConfig(baseAppBundleId: baseAppBundleId) let apiId: Int32 = buildConfig.apiId + let apiHash: String = buildConfig.apiHash let languagesCategory = "ios" let appGroupName = "group.\(baseAppBundleId)" @@ -100,7 +101,7 @@ public class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId) let encryptionParameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!) - account = currentAccount(allocateIfNotExists: false, networkArguments: NetworkInitializationArguments(apiId: apiId, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, appData: .single(buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()), supplementary: true, manager: accountManager, rootPath: rootPath, auxiliaryMethods: accountAuxiliaryMethods, encryptionParameters: encryptionParameters) + account = currentAccount(allocateIfNotExists: false, networkArguments: NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, appData: .single(buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()), supplementary: true, manager: accountManager, rootPath: rootPath, auxiliaryMethods: accountAuxiliaryMethods, encryptionParameters: encryptionParameters) |> mapToSignal { account -> Signal in if let account = account { switch account { @@ -566,9 +567,9 @@ public class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo completion(.success(with: .missed)) } - public func resolveCallType(for intent: INSearchCallHistoryIntent, with completion: @escaping (INCallRecordTypeResolutionResult) -> Void) { + /*public func resolveCallType(for intent: INSearchCallHistoryIntent, with completion: @escaping (INCallRecordTypeResolutionResult) -> Void) { completion(.success(with: .missed)) - } + }*/ public func handle(intent: INSearchCallHistoryIntent, completion: @escaping (INSearchCallHistoryIntentResponse) -> Void) { self.actionDisposable.set((self.accountPromise.get() diff --git a/SiriIntents/IntentMessages.swift b/SiriIntents/IntentMessages.swift index d8a8831fd6..f8add17d11 100644 --- a/SiriIntents/IntentMessages.swift +++ b/SiriIntents/IntentMessages.swift @@ -37,7 +37,7 @@ func unreadMessages(account: Account) -> Signal<[INMessage], NoError> { |> mapToSignal { view -> Signal<[INMessage], NoError> in var signals: [Signal<[INMessage], NoError>] = [] for entry in view.0.entries { - if case let .MessageEntry(index, _, readState, notificationSettings, _, _, _, _) = entry { + if case let .MessageEntry(index, _, readState, notificationSettings, _, _, _, _, _) = entry { if index.messageIndex.id.peerId.namespace != Namespaces.Peer.CloudUser { continue } diff --git a/Telegram-iOS/Icons.xcassets/Shortcuts/Account.imageset/Contents.json b/Telegram-iOS/Icons.xcassets/Shortcuts/Account.imageset/Contents.json new file mode 100644 index 0000000000..1dbbff15e7 --- /dev/null +++ b/Telegram-iOS/Icons.xcassets/Shortcuts/Account.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_lt_user.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-iOS/Icons.xcassets/Shortcuts/Account.imageset/ic_lt_user.pdf b/Telegram-iOS/Icons.xcassets/Shortcuts/Account.imageset/ic_lt_user.pdf new file mode 100644 index 0000000000..cc60b084aa Binary files /dev/null and b/Telegram-iOS/Icons.xcassets/Shortcuts/Account.imageset/ic_lt_user.pdf differ diff --git a/Telegram-iOS/Resources/Compass.tgs b/Telegram-iOS/Resources/Compass.tgs new file mode 100644 index 0000000000..535e4362bf Binary files /dev/null and b/Telegram-iOS/Resources/Compass.tgs differ diff --git a/Telegram-iOS/Resources/Dice_1.tgs b/Telegram-iOS/Resources/Dice_1.tgs new file mode 100644 index 0000000000..e078e6b6de Binary files /dev/null and b/Telegram-iOS/Resources/Dice_1.tgs differ diff --git a/Telegram-iOS/Resources/Dice_2.tgs b/Telegram-iOS/Resources/Dice_2.tgs new file mode 100644 index 0000000000..c350c4d88c Binary files /dev/null and b/Telegram-iOS/Resources/Dice_2.tgs differ diff --git a/Telegram-iOS/Resources/Dice_3.tgs b/Telegram-iOS/Resources/Dice_3.tgs new file mode 100644 index 0000000000..388043434d Binary files /dev/null and b/Telegram-iOS/Resources/Dice_3.tgs differ diff --git a/Telegram-iOS/Resources/Dice_4.tgs b/Telegram-iOS/Resources/Dice_4.tgs new file mode 100644 index 0000000000..0cc05f7cea Binary files /dev/null and b/Telegram-iOS/Resources/Dice_4.tgs differ diff --git a/Telegram-iOS/Resources/Dice_5.tgs b/Telegram-iOS/Resources/Dice_5.tgs new file mode 100644 index 0000000000..2005eef2f4 Binary files /dev/null and b/Telegram-iOS/Resources/Dice_5.tgs differ diff --git a/Telegram-iOS/Resources/Dice_6.tgs b/Telegram-iOS/Resources/Dice_6.tgs new file mode 100644 index 0000000000..6c298dde36 Binary files /dev/null and b/Telegram-iOS/Resources/Dice_6.tgs differ diff --git a/Telegram-iOS/Telegram-iOS-AppStoreLLC.entitlements b/Telegram-iOS/Telegram-iOS-AppStoreLLC.entitlements index a710d1b1fb..042b86f4cf 100644 --- a/Telegram-iOS/Telegram-iOS-AppStoreLLC.entitlements +++ b/Telegram-iOS/Telegram-iOS-AppStoreLLC.entitlements @@ -32,6 +32,7 @@ merchant.sberbank.test.ph.telegra.Telegraph merchant.privatbank.test.telergramios merchant.privatbank.prod.telergram + merchant.telegram.tranzzo.test com.apple.developer.pushkit.unrestricted-voip diff --git a/Telegram-iOS/en.lproj/Localizable.strings b/Telegram-iOS/en.lproj/Localizable.strings index 78f281f97d..061741053b 100644 --- a/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram-iOS/en.lproj/Localizable.strings @@ -22,6 +22,10 @@ "PUSH_CHANNEL_MESSAGE_POLL" = "%1$@|posted a poll %2$@"; "PUSH_PINNED_POLL" = "%1$@|pinned a poll"; +"PUSH_MESSAGE_QUIZ" = "%1$@|sent you a quiz %2$@"; +"PUSH_CHANNEL_MESSAGE_QUIZ" = "%1$@|posted a quiz %2$@"; +"PUSH_PINNED_QUIZ" = "%1$@|pinned a quiz"; + "PUSH_CHAT_MESSAGE_TEXT" = "%2$@|%1$@: %3$@"; "PUSH_CHAT_MESSAGE_NOTEXT" = "%2$@|%1$@ sent a message"; "PUSH_CHAT_MESSAGE_PHOTO" = "%2$@|%1$@ sent a photo"; @@ -93,6 +97,7 @@ "PUSH_MESSAGE_GEO" = "%1$@|sent you a map"; "PUSH_MESSAGE_GEOLIVE" = "%1$@|started sharing their live location"; "PUSH_MESSAGE_POLL" = "%1$@|sent you a poll"; +"PUSH_MESSAGE_QUIZ" = "%1$@|sent you a quiz"; "PUSH_MESSAGE_GIF" = "%1$@|sent you a GIF"; "PUSH_MESSAGE_GAME" = "%1$@|invited you to play %2$@"; "PUSH_MESSAGE_INVOICE" = "%1$@|sent you an invoice for %2$@"; @@ -125,6 +130,7 @@ "PUSH_CHANNEL_MESSAGE_GEO" = "%1$@|posted a map"; "PUSH_CHANNEL_MESSAGE_GEOLIVE" = "%1$@|posted a live location"; "PUSH_CHANNEL_MESSAGE_POLL" = "%1$@|posted a poll"; +"PUSH_CHANNEL_MESSAGE_QUIZ" = "%1$@|posted a quiz"; "PUSH_CHANNEL_MESSAGE_GIF" = "%1$@|posted a GIF"; "PUSH_CHANNEL_MESSAGE_GAME" = "%1$@|invited you to play %2$@"; "PUSH_CHANNEL_MESSAGE_FWD" = "%1$@|posted a forwarded message"; @@ -155,6 +161,7 @@ "PUSH_CHAT_MESSAGE_GEO" = "%2$@|%1$@ sent a map"; "PUSH_CHAT_MESSAGE_GEOLIVE" = "%2$@|%1$@ started sharing their live location"; "PUSH_CHAT_MESSAGE_POLL" = "%2$@|%1$@ sent a poll %3$@ to the group"; +"PUSH_CHAT_MESSAGE_QUIZ" = "%2$@|%1$@ sent a quiz %3$@ to the group"; "PUSH_CHAT_MESSAGE_GIF" = "%2$@|%1$@ sent a GIF"; "PUSH_CHAT_MESSAGE_GAME" = "%2$@|%1$@ invited the group to play %3$@"; "PUSH_CHAT_MESSAGE_INVOICE" = "%2$@|%1$@ sent an invoice for %3$@"; @@ -197,6 +204,7 @@ "PUSH_PINNED_GEO" = "%1$@|pinned a map"; "PUSH_PINNED_GEOLIVE" = "%1$@|pinned a live location"; "PUSH_PINNED_POLL" = "|%1$@|pinned a poll %2$@"; +"PUSH_PINNED_QUIZ" = "|%1$@|pinned a quiz %2$@"; "PUSH_PINNED_GAME" = "%1$@|pinned a game"; "PUSH_PINNED_INVOICE" = "%1$@|pinned an invoice"; "PUSH_PINNED_GIF" = "%1$@|pinned a GIF"; @@ -254,6 +262,11 @@ "State.Updating" = "Updating..."; "State.WaitingForNetwork" = "Waiting for network"; +"ChatState.Connecting" = "connecting..."; +"ChatState.ConnectingToProxy" = "connecting to proxy..."; +"ChatState.Updating" = "updating..."; +"ChatState.WaitingForNetwork" = "waiting for network..."; + // Presence "Presence.online" = "online"; @@ -505,6 +518,7 @@ "Notification.JoinedChat" = "%@ joined the group"; "Notification.JoinedChannel" = "%@ joined the channel"; "Notification.Invited" = "%@ invited %@"; +"Notification.InvitedMultiple" = "%@ invited %@"; "Notification.LeftChat" = "%@ left the group"; "Notification.LeftChannel" = "%@ left the channel"; "Notification.Kicked" = "%@ removed %@"; @@ -1926,6 +1940,7 @@ "Notification.PinnedContactMessage" = "%@ pinned a contact"; "Notification.PinnedDeletedMessage" = "%@ pinned deleted message"; "Notification.PinnedPollMessage" = "%@ pinned a poll"; +"Notification.PinnedQuizMessage" = "%@ pinned a quiz"; "Message.PinnedTextMessage" = "pinned \"%@\" "; "Message.PinnedPhotoMessage" = "pinned photo"; @@ -1936,7 +1951,6 @@ "Message.PinnedStickerMessage" = "pinned sticker"; "Message.PinnedLocationMessage" = "pinned location"; "Message.PinnedContactMessage" = "pinned contact"; -"Message.PinnedPollMessage" = "pinned poll"; "Notification.PinnedMessage" = "pinned message"; @@ -2561,6 +2575,7 @@ Unused sets are archived when you add more."; "Channel.AdminLog.InfoPanelTitle" = "What Is This?"; "Channel.AdminLog.InfoPanelAlertTitle" = "What is the event log?"; "Channel.AdminLog.InfoPanelAlertText" = "This is a list of all service actions taken by the group's members and admins in the last 48 hours."; +"Channel.AdminLog.InfoPanelChannelAlertText" = "This is a list of all service actions taken by the channel's admins in the last 48 hours."; "Channel.AdminLog.BanReadMessages" = "Read Messages"; "Channel.AdminLog.BanSendMessages" = "Send Messages"; @@ -3063,7 +3078,6 @@ Unused sets are archived when you add more."; "Settings.Appearance" = "Appearance"; "Appearance.Title" = "Appearance"; -"Appearance.TextSize" = "TEXT SIZE"; "Appearance.Preview" = "CHAT PREVIEW"; "Appearance.ColorTheme" = "COLOR THEME"; "Appearance.ThemeDayClassic" = "Day Classic"; @@ -3792,6 +3806,7 @@ Unused sets are archived when you add more."; "MessagePoll.VotedCount_any" = "%@ votes"; "AttachmentMenu.Poll" = "Poll"; "Conversation.PinnedPoll" = "Pinned Poll"; +"Conversation.PinnedQuiz" = "Pinned Quiz"; "CreatePoll.Title" = "New Poll"; "CreatePoll.Create" = "Send"; @@ -4676,6 +4691,12 @@ Any member of this group will be able to see messages in the channel."; "Appearance.ThemePreview.Chat.2.ReplyName" = "Bob Harris"; "Appearance.ThemePreview.Chat.2.Text" = "Right side. And, uh, with intensity."; "Appearance.ThemePreview.Chat.3.Text" = "Is that everything? It seemed like he said quite a bit more than that. 😯"; +"Appearance.ThemePreview.Chat.3.TextWithLink" = "Is that everything? It seemed like he said [quite a bit more] than that. 😯"; + +"Appearance.ThemePreview.Chat.4.Text" = "For relaxing times, make it Suntory time. 😎"; +"Appearance.ThemePreview.Chat.5.Text" = "He wants you to turn, look in camera. O.K.?"; +"Appearance.ThemePreview.Chat.6.Text" = "That’s all he said?"; +"Appearance.ThemePreview.Chat.7.Text" = "Yes, turn to camera."; "GroupInfo.Permissions.SlowmodeValue.Off" = "Off"; @@ -4814,7 +4835,7 @@ Any member of this group will be able to see messages in the channel."; "Wallet.Receive.CopyInvoiceUrl" = "Copy Invoice URL"; "Wallet.Receive.ShareAddress" = "Share Wallet Address"; "Wallet.Receive.ShareInvoiceUrl" = "Share Invoice URL"; -"Wallet.Receive.ShareUrlInfo" = "Share this link with other Gram wallet owners to receive Grams from them."; +"Wallet.Receive.ShareUrlInfo" = "Share this link with other Gram wallet owners to receive Grams from them. Note: this link won't work for real Grams."; "Wallet.Receive.AmountHeader" = "AMOUNT"; "Wallet.Receive.AmountText" = "Grams to receive"; "Wallet.Receive.AmountInfo" = "You can specify the amount and purpose of the payment to save the sender some time."; @@ -5104,3 +5125,222 @@ Any member of this group will be able to see messages in the channel."; "GroupInfo.ShowMoreMembers_3_10" = "%@ more"; "GroupInfo.ShowMoreMembers_many" = "%@ more"; "GroupInfo.ShowMoreMembers_any" = "%@ more"; + +"ContactInfo.Note" = "note"; + +"Group.Location.CreateInThisPlace" = "Create a group in this place"; + +"Theme.Colors.Accent" = "Accent"; +"Theme.Colors.Background" = "Background"; +"Theme.Colors.Messages" = "Messages"; +"Theme.Colors.ColorWallpaperWarning" = "Are you sure you want to change your chat wallpaper to a color?"; +"Theme.Colors.ColorWallpaperWarningProceed" = "Proceed"; + +"ChatSettings.IntentsSettings" = "Share Sheet"; +"IntentsSettings.Title" = "Share Sheet"; +"IntentsSettings.MainAccount" = "Main Account"; +"IntentsSettings.MainAccountInfo" = "Choose an account for Siri and share suggestions."; +"IntentsSettings.SuggestedChats" = "Suggested Chats"; +"IntentsSettings.SuggestedChatsContacts" = "Contacts"; +"IntentsSettings.SuggestedChatsSavedMessages" = "Saved Messages"; +"IntentsSettings.SuggestedChatsPrivateChats" = "Private Chats"; +"IntentsSettings.SuggestedChatsGroups" = "Groups"; +"IntentsSettings.SuggestedChatsInfo" = "Archived chats will not be suggested."; +"IntentsSettings.SuggestedAndSpotlightChatsInfo" = "Suggestions will appear in the Share Sheet and Spotlight search results. Archived chats will not be suggested."; +"IntentsSettings.SuggestBy" = "Suggest By"; +"IntentsSettings.SuggestByAll" = "All Sent Messages"; +"IntentsSettings.SuggestByShare" = "Only Shared Messages"; +"IntentsSettings.ResetAll" = "Reset All Share Suggestions"; +"IntentsSettings.Reset" = "Reset"; + +"Conversation.SendingOptionsTooltip" = "Hold this button to schedule your message\nor send it without sound."; + +"Appearance.TextSizeSetting" = "Text Size"; +"Appearance.TextSize.Automatic" = "System"; +"Appearance.TextSize.Title" = "Text Size"; +"Appearance.TextSize.UseSystem" = "User System Text Size"; +"Appearance.TextSize.Apply" = "Set"; + +"Shortcut.SwitchAccount" = "Switch Account"; + +"Settings.Devices" = "Devices"; +"Settings.AddDevice" = "Scan QR"; +"AuthSessions.DevicesTitle" = "Devices"; +"AuthSessions.AddDevice" = "Scan QR"; +"AuthSessions.AddDevice.ScanInfo" = "Scan a QR code to log into\nthis account on another device."; +"AuthSessions.AddDevice.ScanTitle" = "Scan QR Code"; +"AuthSessions.AddDevice.InvalidQRCode" = "Invalid QR Code"; +"AuthSessions.AddDeviceIntro.Title" = "Log in by QR Code"; +"AuthSessions.AddDeviceIntro.Text1" = "Download Telegram on your computer from [desktop.telegram.org]()"; +"AuthSessions.AddDeviceIntro.Text2" = "Run Telegram on your computer to get the QR code"; +"AuthSessions.AddDeviceIntro.Text3" = "Scan the QR code to connect your account"; +"AuthSessions.AddDeviceIntro.Action" = "Scan QR Code"; +"AuthSessions.AddedDeviceTitle" = "Login Successful"; +"AuthSessions.AddedDeviceTerminate" = "Terminate"; + +"Map.SendThisPlace" = "Send This Place"; +"Map.SetThisPlace" = "Set This Place"; +"Map.AddressOnMap" = "Address On Map"; +"Map.PlacesNearby" = "Places Nearby"; +"Map.Home" = "Home"; +"Map.Work" = "Work"; +"Map.HomeAndWorkTitle" = "Home & Work Addresses"; +"Map.HomeAndWorkInfo" = "Telegram uses the Home and Work addresses from your Contact Card.\n\nKeep your Contact Card up to date for quick access to sending Home and Work addresses."; +"Map.SearchNoResultsDescription" = "There were no results for \"%@\".\nTry a new search."; + +"ChatList.Search.ShowMore" = "Show more"; +"ChatList.Search.ShowLess" = "Show less"; + +"AuthSessions.OtherDevices" = "The official Telegram App is available for iPhone, iPad, Android, macOS, Windows and Linux. [Learn More]()"; + +"MediaPlayer.UnknownArtist" = "Unknown Artist"; +"MediaPlayer.UnknownTrack" = "Unknown Track"; + +"Contacts.InviteContacts_1" = "Invite %@ Contact"; +"Contacts.InviteContacts_2" = "Invite %@ Contacts"; +"Contacts.InviteContacts_3_10" = "Invite %@ Contacts"; +"Contacts.InviteContacts_any" = "Invite %@ Contacts"; +"Contacts.InviteContacts_many" = "Invite %@ Contacts"; +"Contacts.InviteContacts_0" = "Invite %@ Contacts"; + +"Theme.Context.ChangeColors" = "Change Colors"; + +"EditTheme.ChangeColors" = "Change Colors"; + +"Theme.Colors.Proceed" = "Proceed"; + +"AuthSessions.AddDevice.UrlLoginHint" = "This code can be used to allow someone to log in to your Telegram account.\n\nTo confirm Telegram login, please go to Settings > Devices > Scan QR and scan the code."; + +"Appearance.RemoveThemeColor" = "Remove"; +"Appearance.RemoveThemeColorConfirmation" = "Remove Color"; + +"WallpaperPreview.PatternTitle" = "Choose Pattern"; +"WallpaperPreview.PatternPaternDiscard" = "Discard"; +"WallpaperPreview.PatternPaternApply" = "Apply"; + +"ChatContextMenu.TextSelectionTip" = "Hold a word, then move cursor to select more| text to copy."; + +"OldChannels.Title" = "Limit Reached"; +"OldChannels.NoticeTitle" = "Too Many Groups and Channels"; +"OldChannels.NoticeText" = "Sorry, you are member of too many groups and channels.\nPlease leave some before joining new one."; +"OldChannels.NoticeCreateText" = "Sorry, you are member of too many groups and channels.\nPlease leave some before creating a new one."; +"OldChannels.NoticeUpgradeText" = "Sorry, you are a member of too many groups and channels.\nFor technical reasons, you need to leave some first before changing this setting in your groups."; +"OldChannels.ChannelsHeader" = "MOST INACTIVE"; +"OldChannels.Leave_1" = "Leave %@ Chat"; +"OldChannels.Leave_any" = "Leave %@ Chats"; + +"OldChannels.ChannelFormat" = "channel, "; +"OldChannels.GroupEmptyFormat" = "group, "; +"OldChannels.GroupFormat_1" = "%@ member "; +"OldChannels.GroupFormat_any" = "%@ members "; + +"OldChannels.InactiveWeek_1" = "inactive %@ week"; +"OldChannels.InactiveWeek_any" = "inactive %@ weeks"; + +"OldChannels.InactiveMonth_1" = "inactive %@ month"; +"OldChannels.InactiveMonth_any" = "inactive %@ months"; + +"OldChannels.InactiveYear_1" = "inactive %@ year"; +"OldChannels.InactiveYear_any" = "inactive %@ years"; + +"PrivacySettings.WebSessions" = "Active Websites"; + +"Appearance.ShareThemeColor" = "Share"; + +"Theme.ThemeChanged" = "Color Theme Changed"; +"Theme.ThemeChangedText" = "You can change it back in\n[Settings > Appearance]()."; + +"StickerPackActionInfo.AddedTitle" = "Stickers Added"; +"StickerPackActionInfo.AddedText" = "%@ has been added to your stickers."; +"StickerPackActionInfo.RemovedTitle" = "Stickers Removed"; +"StickerPackActionInfo.ArchivedTitle" = "Stickers Archived"; +"StickerPackActionInfo.RemovedText" = "%@ is no longer in your stickers."; + +"Conversation.ContextMenuCancelEditing" = "Cancel Editing"; + +"Map.NoPlacesNearby" = "There are no known places nearby.\nTry a different location."; + +"CreatePoll.QuizTitle" = "New Quiz"; +"CreatePoll.QuizOptionsHeader" = "QUIZ ANSWERS"; +"CreatePoll.Anonymous" = "Anonymous Voting"; +"CreatePoll.MultipleChoice" = "Multiple Choice"; +"CreatePoll.MultipleChoiceQuizAlert" = "A quiz has one correct answer."; +"CreatePoll.Quiz" = "Quiz Mode"; +"CreatePoll.QuizInfo" = "Polls in Quiz Mode have one correct answer. Users can't revoke their answers."; +"CreatePoll.QuizTip" = "Tap to choose the correct answer"; + +"MessagePoll.LabelPoll" = "Public Poll"; +"MessagePoll.LabelAnonymousQuiz" = "Anonymous Quiz"; +"MessagePoll.LabelQuiz" = "Quiz"; +"MessagePoll.SubmitVote" = "Vote"; +"MessagePoll.ViewResults" = "View Results"; +"MessagePoll.QuizNoUsers" = "Nobody answered yet"; +"MessagePoll.QuizCount_0" = "%@ answered"; +"MessagePoll.QuizCount_1" = "1 answered"; +"MessagePoll.QuizCount_2" = "2 answered"; +"MessagePoll.QuizCount_3_10" = "%@ answered"; +"MessagePoll.QuizCount_many" = "%@ answered"; +"MessagePoll.QuizCount_any" = "%@ answered"; + +"PollResults.Title" = "Poll Results"; +"PollResults.Collapse" = "COLLAPSE"; +"PollResults.ShowMore_1" = "Show More (%@)"; +"PollResults.ShowMore_any" = "Show More (%@)"; + +"Conversation.StopQuiz" = "Stop Quiz"; +"Conversation.StopQuizConfirmationTitle" = "If you stop this quiz now, nobody will be able to submit answers. This action cannot be undone."; +"Conversation.StopQuizConfirmation" = "Stop Quiz"; + +"Forward.ErrorDisabledForChat" = "Sorry, you can't forward messages to this chat."; +"Forward.ErrorPublicPollDisabledInChannels" = "Sorry, public polls can’t be forwarded to channels."; +"Forward.ErrorPublicQuizDisabledInChannels" = "Sorry, public polls can’t be forwarded to channels."; + +"Map.PlacesInThisArea" = "Places In This Area"; + +"Appearance.BubbleCornersSetting" = "Message Corners"; +"Appearance.BubbleCorners.Title" = "Message Corners"; +"Appearance.BubbleCorners.AdjustAdjacent" = "Adjust Adjacent Corners"; +"Appearance.BubbleCorners.Apply" = "Set"; + +"Conversation.LiveLocationYouAndOther" = "**You** and %@"; + +"PeopleNearby.MakeVisible" = "Make Myself Visible"; +"PeopleNearby.MakeInvisible" = "Stop Showing Me"; + +"PeopleNearby.ShowMorePeople_0" = "Show %@ More People"; +"PeopleNearby.ShowMorePeople_1" = "Show %@ More People"; +"PeopleNearby.ShowMorePeople_2" = "Show %@ More People"; +"PeopleNearby.ShowMorePeople_3_10" = "Show %@ More People"; +"PeopleNearby.ShowMorePeople_many" = "Show %@ More People"; +"PeopleNearby.ShowMorePeople_any" = "Show %@ More People"; + +"PeopleNearby.VisibleUntil" = "visible until %@"; + +"PeopleNearby.MakeVisibleTitle" = "Make Myself Visible"; +"PeopleNearby.MakeVisibleDescription" = "Users nearby will be able to view your profile and send you messages. This may help you find new friends, but could also attract excessive attention. You can stop sharing your profile at any time.\n\nYour phone number will remain hidden."; + +"PeopleNearby.DiscoverDescription" = "Exchange contact info with people nearby\nand find new friends."; + +"Time.TomorrowAt" = "tomorrow at %@"; + +"PeerInfo.ButtonMessage" = "Message"; +"PeerInfo.ButtonDiscuss" = "Discuss"; +"PeerInfo.ButtonCall" = "Call"; +"PeerInfo.ButtonMute" = "Mute"; +"PeerInfo.ButtonUnmute" = "Unmute"; +"PeerInfo.ButtonMore" = "More"; +"PeerInfo.ButtonAddMember" = "Add Members"; +"PeerInfo.ButtonSearch" = "Search"; +"PeerInfo.ButtonLeave" = "Leave"; + +"PeerInfo.PaneMedia" = "Media"; +"PeerInfo.PaneFiles" = "Files"; +"PeerInfo.PaneLinks" = "Links"; +"PeerInfo.PaneVoice" = "Voice Messages"; +"PeerInfo.PaneAudio" = "Audio"; +"PeerInfo.PaneGroups" = "Groups"; +"PeerInfo.PaneMembers" = "Members"; + +"PeerInfo.AddToContacts" = "Add to Contacts"; + +"PeerInfo.BioExpand" = "more"; diff --git a/Wallet/README.md b/Wallet/README.md index 792e3695d6..fc0c50909a 100644 --- a/Wallet/README.md +++ b/Wallet/README.md @@ -26,7 +26,7 @@ brew install cmake ant ``` mkdir -p $HOME/buck_source -cd tools/buck +cd tools/buck-build sh ./prepare_buck_source.sh $HOME/buck_source ``` diff --git a/Wallet/Sources/AppDelegate.swift b/Wallet/Sources/AppDelegate.swift index 3faf48026b..ebc8cc17ed 100644 --- a/Wallet/Sources/AppDelegate.swift +++ b/Wallet/Sources/AppDelegate.swift @@ -651,7 +651,7 @@ final class AppDelegate: NSObject, UIApplicationDelegate { print("Starting with \(documentsPath)") #endif - let storage = WalletStorageInterfaceImpl(path: documentsPath + "/data", configurationPath: documentsPath + "/configuration") + let storage = WalletStorageInterfaceImpl(path: documentsPath + "/data", configurationPath: documentsPath + "/configuration_v2") let initialConfigValue = storage.mergedLocalWalletConfiguration() |> take(1) diff --git a/Wallet/Strings/en.lproj/Localizable.strings b/Wallet/Strings/en.lproj/Localizable.strings index ac579c0e2b..83e12f54a4 100644 --- a/Wallet/Strings/en.lproj/Localizable.strings +++ b/Wallet/Strings/en.lproj/Localizable.strings @@ -39,7 +39,7 @@ "Wallet.Receive.CopyInvoiceUrl" = "Copy Invoice URL"; "Wallet.Receive.ShareAddress" = "Share Wallet Address"; "Wallet.Receive.ShareInvoiceUrl" = "Share Invoice URL"; -"Wallet.Receive.ShareUrlInfo" = "Share this link with other Gram wallet owners to receive Grams from them."; +"Wallet.Receive.ShareUrlInfo" = "Share this link with other Gram wallet owners to receive Grams from them. Note: this link won't work for real Grams."; "Wallet.Receive.AmountHeader" = "AMOUNT"; "Wallet.Receive.AmountText" = "Grams to receive"; "Wallet.Receive.AmountInfo" = "You can specify the amount and purpose of the payment to save the sender some time."; diff --git a/Widget/PeerNode.swift b/Widget/PeerNode.swift index b6c67abc23..b89deba9e6 100644 --- a/Widget/PeerNode.swift +++ b/Widget/PeerNode.swift @@ -117,24 +117,31 @@ final class PeerView: UIView { private let tapped: () -> Void - var primaryColor: UIColor { - didSet { - self.titleLabel.textColor = self.primaryColor - } - } - init(primaryColor: UIColor, accountPeerId: Int64, peer: WidgetDataPeer, tapped: @escaping () -> Void) { - self.primaryColor = primaryColor self.peer = peer self.tapped = tapped self.avatarView = AvatarView(accountPeerId: accountPeerId, peer: peer, size: avatarSize) self.titleLabel = UILabel() - let title = peer.name + var title = peer.name + if let lastName = peer.lastName, !lastName.isEmpty { + title.append("\n") + title.append(lastName) + } + + let systemFontSize = UIFont.preferredFont(forTextStyle: .body).pointSize + let fontSize = floor(systemFontSize * 11.0 / 17.0) + self.titleLabel.text = title - self.titleLabel.textColor = primaryColor - self.titleLabel.font = UIFont.systemFont(ofSize: 11.0) + if #available(iOSApplicationExtension 13.0, *) { + self.titleLabel.textColor = UIColor.label + } else { + self.titleLabel.textColor = primaryColor + } + self.titleLabel.font = UIFont.systemFont(ofSize: fontSize) self.titleLabel.lineBreakMode = .byTruncatingTail + self.titleLabel.numberOfLines = 2 + self.titleLabel.textAlignment = .center super.init(frame: CGRect()) diff --git a/Widget/TodayViewController.swift b/Widget/TodayViewController.swift index d90dee34cb..d9dbbbddae 100644 --- a/Widget/TodayViewController.swift +++ b/Widget/TodayViewController.swift @@ -20,15 +20,6 @@ class TodayViewController: UIViewController, NCWidgetProviding { override func viewDidLoad() { super.viewDidLoad() - if #available(iOSApplicationExtension 13.0, *) { - switch self.traitCollection.userInterfaceStyle { - case .dark: - self.primaryColor = .white - default: - break - } - } - let appBundleIdentifier = Bundle.main.bundleIdentifier! guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else { return @@ -54,10 +45,16 @@ class TodayViewController: UIViewController, NCWidgetProviding { presentationData = WidgetPresentationData(applicationLockedString: "Unlock the app to use widget") } + let fontSize = UIFont.preferredFont(forTextStyle: .body).pointSize + if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) { let appLockedLabel = UILabel() - appLockedLabel.textColor = self.primaryColor - appLockedLabel.font = UIFont.systemFont(ofSize: 16.0) + if #available(iOSApplicationExtension 13.0, *) { + appLockedLabel.textColor = UIColor.label + } else { + appLockedLabel.textColor = self.primaryColor + } + appLockedLabel.font = UIFont.systemFont(ofSize: fontSize) appLockedLabel.text = presentationData.applicationLockedString appLockedLabel.sizeToFit() self.appLockedLabel = appLockedLabel @@ -77,21 +74,6 @@ class TodayViewController: UIViewController, NCWidgetProviding { } } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - if #available(iOSApplicationExtension 13.0, *) { - switch self.traitCollection.userInterfaceStyle { - case .dark: - self.primaryColor = .white - default: - self.primaryColor = .black - } - } - self.appLockedLabel?.textColor = self.primaryColor - for view in self.peerViews { - view.primaryColor = self.primaryColor - } - } - func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) { completionHandler(.newData) } @@ -171,7 +153,7 @@ class TodayViewController: UIViewController, NCWidgetProviding { offset = floor(spacing / 2.0) for i in 0 ..< peerFrames.count { let peerView = self.peerViews[i] - peerView.frame = CGRect(origin: CGPoint(x: offset, y: 20.0), size: peerFrames[i].size) + peerView.frame = CGRect(origin: CGPoint(x: offset, y: 16.0), size: peerFrames[i].size) peerView.updateLayout(size: peerFrames[i].size) offset += peerFrames[i].width + spacing } diff --git a/buildbox/fake-codesigning/certs/SelfSigned.p12 b/build-system/fake-codesigning/certs/distribution/SelfSigned.p12 similarity index 100% rename from buildbox/fake-codesigning/certs/SelfSigned.p12 rename to build-system/fake-codesigning/certs/distribution/SelfSigned.p12 diff --git a/buildbox/fake-codesigning/profiles/self_17a6dde8-0db2-42b4-9215-d8544da24da1.mobileprovision b/build-system/fake-codesigning/profiles/appstore/self_17a6dde8-0db2-42b4-9215-d8544da24da1.mobileprovision similarity index 100% rename from buildbox/fake-codesigning/profiles/self_17a6dde8-0db2-42b4-9215-d8544da24da1.mobileprovision rename to build-system/fake-codesigning/profiles/appstore/self_17a6dde8-0db2-42b4-9215-d8544da24da1.mobileprovision diff --git a/buildbox/fake-codesigning/profiles/self_38e2d395-3b03-4327-b13e-5a81d77a417f.mobileprovision b/build-system/fake-codesigning/profiles/appstore/self_38e2d395-3b03-4327-b13e-5a81d77a417f.mobileprovision similarity index 100% rename from buildbox/fake-codesigning/profiles/self_38e2d395-3b03-4327-b13e-5a81d77a417f.mobileprovision rename to build-system/fake-codesigning/profiles/appstore/self_38e2d395-3b03-4327-b13e-5a81d77a417f.mobileprovision diff --git a/buildbox/fake-codesigning/profiles/self_66d9e1ed-89b0-43a9-81dc-c5db42768deb.mobileprovision b/build-system/fake-codesigning/profiles/appstore/self_66d9e1ed-89b0-43a9-81dc-c5db42768deb.mobileprovision similarity index 100% rename from buildbox/fake-codesigning/profiles/self_66d9e1ed-89b0-43a9-81dc-c5db42768deb.mobileprovision rename to build-system/fake-codesigning/profiles/appstore/self_66d9e1ed-89b0-43a9-81dc-c5db42768deb.mobileprovision diff --git a/buildbox/fake-codesigning/profiles/self_a064880e-5214-4456-96f3-3beab43c8a49.mobileprovision b/build-system/fake-codesigning/profiles/appstore/self_a064880e-5214-4456-96f3-3beab43c8a49.mobileprovision similarity index 100% rename from buildbox/fake-codesigning/profiles/self_a064880e-5214-4456-96f3-3beab43c8a49.mobileprovision rename to build-system/fake-codesigning/profiles/appstore/self_a064880e-5214-4456-96f3-3beab43c8a49.mobileprovision diff --git a/buildbox/fake-codesigning/profiles/self_c7ce0e4f-4a34-4f37-9bdf-d93d7e67f935.mobileprovision b/build-system/fake-codesigning/profiles/appstore/self_c7ce0e4f-4a34-4f37-9bdf-d93d7e67f935.mobileprovision similarity index 100% rename from buildbox/fake-codesigning/profiles/self_c7ce0e4f-4a34-4f37-9bdf-d93d7e67f935.mobileprovision rename to build-system/fake-codesigning/profiles/appstore/self_c7ce0e4f-4a34-4f37-9bdf-d93d7e67f935.mobileprovision diff --git a/buildbox/fake-codesigning/profiles/self_d5420e53-0e9a-4745-ab22-1dc7f0f8f0e6.mobileprovision b/build-system/fake-codesigning/profiles/appstore/self_d5420e53-0e9a-4745-ab22-1dc7f0f8f0e6.mobileprovision similarity index 100% rename from buildbox/fake-codesigning/profiles/self_d5420e53-0e9a-4745-ab22-1dc7f0f8f0e6.mobileprovision rename to build-system/fake-codesigning/profiles/appstore/self_d5420e53-0e9a-4745-ab22-1dc7f0f8f0e6.mobileprovision diff --git a/buildbox/fake-codesigning/profiles/self_f5d6daf2-b88a-4de9-846a-bc207ea9b8dd.mobileprovision b/build-system/fake-codesigning/profiles/appstore/self_f5d6daf2-b88a-4de9-846a-bc207ea9b8dd.mobileprovision similarity index 100% rename from buildbox/fake-codesigning/profiles/self_f5d6daf2-b88a-4de9-846a-bc207ea9b8dd.mobileprovision rename to build-system/fake-codesigning/profiles/appstore/self_f5d6daf2-b88a-4de9-846a-bc207ea9b8dd.mobileprovision diff --git a/buildbox/fake-codesigning/profiles/self_fd2bced7-6353-4f44-8022-979f48e73c04.mobileprovision b/build-system/fake-codesigning/profiles/appstore/self_fd2bced7-6353-4f44-8022-979f48e73c04.mobileprovision similarity index 100% rename from buildbox/fake-codesigning/profiles/self_fd2bced7-6353-4f44-8022-979f48e73c04.mobileprovision rename to build-system/fake-codesigning/profiles/appstore/self_fd2bced7-6353-4f44-8022-979f48e73c04.mobileprovision diff --git a/build-system/verify.sh b/build-system/verify.sh new file mode 100644 index 0000000000..89e33bef02 --- /dev/null +++ b/build-system/verify.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +export TELEGRAM_ENV_SET="1" + +export DEVELOPMENT_CODE_SIGN_IDENTITY="iPhone Distribution: Digital Fortress LLC (C67CF9S4VU)" +export DISTRIBUTION_CODE_SIGN_IDENTITY="iPhone Distribution: Digital Fortress LLC (C67CF9S4VU)" +export DEVELOPMENT_TEAM="C67CF9S4VU" + +export API_ID="8" +export API_HASH="7245de8e747a0d6fbe11f7cc14fcc0bb" + +export BUNDLE_ID="ph.telegra.Telegraph" +export APP_CENTER_ID="" +export IS_INTERNAL_BUILD="false" +export IS_APPSTORE_BUILD="true" +export APPSTORE_ID="686449807" +export APP_SPECIFIC_URL_SCHEME="tgapp" + +if [ -z "$BUILD_NUMBER" ]; then + echo "BUILD_NUMBER is not defined" + exit 1 +fi + +export ENTITLEMENTS_APP="Telegram-iOS/Telegram-iOS-AppStoreLLC.entitlements" +export DEVELOPMENT_PROVISIONING_PROFILE_APP="match Development ph.telegra.Telegraph" +export DISTRIBUTION_PROVISIONING_PROFILE_APP="match AppStore ph.telegra.Telegraph" +export ENTITLEMENTS_EXTENSION_SHARE="Share/Share-AppStoreLLC.entitlements" +export DEVELOPMENT_PROVISIONING_PROFILE_EXTENSION_SHARE="match Development ph.telegra.Telegraph.Share" +export DISTRIBUTION_PROVISIONING_PROFILE_EXTENSION_SHARE="match AppStore ph.telegra.Telegraph.Share" +export ENTITLEMENTS_EXTENSION_WIDGET="Widget/Widget-AppStoreLLC.entitlements" +export DEVELOPMENT_PROVISIONING_PROFILE_EXTENSION_WIDGET="match Development ph.telegra.Telegraph.Widget" +export DISTRIBUTION_PROVISIONING_PROFILE_EXTENSION_WIDGET="match AppStore ph.telegra.Telegraph.Widget" +export ENTITLEMENTS_EXTENSION_NOTIFICATIONSERVICE="NotificationService/NotificationService-AppStoreLLC.entitlements" +export DEVELOPMENT_PROVISIONING_PROFILE_EXTENSION_NOTIFICATIONSERVICE="match Development ph.telegra.Telegraph.NotificationService" +export DISTRIBUTION_PROVISIONING_PROFILE_EXTENSION_NOTIFICATIONSERVICE="match AppStore ph.telegra.Telegraph.NotificationService" +export ENTITLEMENTS_EXTENSION_NOTIFICATIONCONTENT="NotificationContent/NotificationContent-AppStoreLLC.entitlements" +export DEVELOPMENT_PROVISIONING_PROFILE_EXTENSION_NOTIFICATIONCONTENT="match Development ph.telegra.Telegraph.NotificationContent" +export DISTRIBUTION_PROVISIONING_PROFILE_EXTENSION_NOTIFICATIONCONTENT="match AppStore ph.telegra.Telegraph.NotificationContent" +export ENTITLEMENTS_EXTENSION_INTENTS="SiriIntents/SiriIntents-AppStoreLLC.entitlements" +export DEVELOPMENT_PROVISIONING_PROFILE_EXTENSION_INTENTS="match Development ph.telegra.Telegraph.SiriIntents" +export DISTRIBUTION_PROVISIONING_PROFILE_EXTENSION_INTENTS="match AppStore ph.telegra.Telegraph.SiriIntents" +export DEVELOPMENT_PROVISIONING_PROFILE_WATCH_APP="match Development ph.telegra.Telegraph.watchkitapp" +export DISTRIBUTION_PROVISIONING_PROFILE_WATCH_APP="match AppStore ph.telegra.Telegraph.watchkitapp" +export DEVELOPMENT_PROVISIONING_PROFILE_WATCH_EXTENSION="match Development ph.telegra.Telegraph.watchkitapp.watchkitextension" +export DISTRIBUTION_PROVISIONING_PROFILE_WATCH_EXTENSION="match AppStore ph.telegra.Telegraph.watchkitapp.watchkitextension" + +BUILDBOX_DIR="buildbox" + +export CODESIGNING_PROFILES_VARIANT="appstore" +export PACKAGE_METHOD="appstore" + +$@ diff --git a/buildbox/build-telegram.sh b/buildbox/build-telegram.sh index 28a82587be..8a98f552d6 100644 --- a/buildbox/build-telegram.sh +++ b/buildbox/build-telegram.sh @@ -69,7 +69,14 @@ else exit 1 fi -COMMIT_ID=$(git rev-parse HEAD) +COMMIT_COMMENT="$(git log -1 --pretty=%B)" +case "$COMMIT_COMMENT" in + *"[nocache]"*) + export BUCK_HTTP_CACHE="" + ;; +esac + +COMMIT_ID="$(git rev-parse HEAD)" COMMIT_AUTHOR=$(git log -1 --pretty=format:'%an') if [ -z "$2" ]; then COMMIT_COUNT=$(git rev-list --count HEAD) @@ -156,7 +163,7 @@ elif [ "$BUILD_MACHINE" == "macOS" ]; then echo "Getting VM IP" while [ 1 ]; do - TEST_IP=$(prlctl exec "$VM_NAME" "ifconfig | grep inet | grep broadcast | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1 | tr '\n' '\0'" || echo "") + TEST_IP=$(prlctl exec "$VM_NAME" "ifconfig | grep inet | grep broadcast | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1 | tr '\n' '\0'" 2>/dev/null || echo "") if [ ! -z "$TEST_IP" ]; then RESPONSE=$(ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null telegram@"$TEST_IP" -o ServerAliveInterval=60 -t "echo -n 1") if [ "$RESPONSE" == "1" ]; then @@ -180,7 +187,7 @@ else fi scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr "$BUILDBOX_DIR/guest-build-telegram.sh" "$BUILDBOX_DIR/transient-data/source.tar" telegram@"$VM_IP": -ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null telegram@"$VM_IP" -o ServerAliveInterval=60 -t "export TELEGRAM_BUILD_APPSTORE_PASSWORD=\"$TELEGRAM_BUILD_APPSTORE_PASSWORD\"; export TELEGRAM_BUILD_APPSTORE_TEAM_NAME=\"$TELEGRAM_BUILD_APPSTORE_TEAM_NAME\"; export TELEGRAM_BUILD_APPSTORE_USERNAME=\"$TELEGRAM_BUILD_APPSTORE_USERNAME\"; export BUILD_NUMBER=\"$BUILD_NUMBER\"; export COMMIT_ID=\"$COMMIT_ID\"; export COMMIT_AUTHOR=\"$COMMIT_AUTHOR\"; export BUCK_HTTP_CACHE=\"$BUCK_HTTP_CACHE\"; $GUEST_SHELL -l guest-build-telegram.sh $BUILD_CONFIGURATION" || true +ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null telegram@"$VM_IP" -o ServerAliveInterval=60 -t "export TELEGRAM_BUILD_APPSTORE_PASSWORD=\"$TELEGRAM_BUILD_APPSTORE_PASSWORD\"; export TELEGRAM_BUILD_APPSTORE_TEAM_NAME=\"$TELEGRAM_BUILD_APPSTORE_TEAM_NAME\"; export TELEGRAM_BUILD_APPSTORE_USERNAME=\"$TELEGRAM_BUILD_APPSTORE_USERNAME\"; export BUILD_NUMBER=\"$BUILD_NUMBER\"; export COMMIT_ID=\"$COMMIT_ID\"; export COMMIT_AUTHOR=\"$COMMIT_AUTHOR\"; export BUCK_HTTP_CACHE=\"$BUCK_HTTP_CACHE\"; export BUCK_DIR_CACHE=\"$BUCK_DIR_CACHE\"; export BUCK_CACHE_MODE=\"$BUCK_CACHE_MODE\"; $GUEST_SHELL -l guest-build-telegram.sh $BUILD_CONFIGURATION" || true OUTPUT_PATH="build/artifacts" rm -rf "$OUTPUT_PATH" diff --git a/buildbox/deploy-appcenter.sh b/buildbox/deploy-appcenter.sh new file mode 100644 index 0000000000..58376b2c20 --- /dev/null +++ b/buildbox/deploy-appcenter.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +set -e +set -x + +API_HOST="https://api.appcenter.ms" +IPA_PATH="build/artifacts/Telegram.ipa" +DSYM_PATH="build/artifacts/Telegram.DSYMs.zip" + +upload_ipa() { + GROUP_DATA=$(curl \ + -X GET \ + --header "X-API-Token: $API_TOKEN" \ + "$API_HOST/v0.1/apps/$API_USER_NAME/$API_APP_NAME/distribution_groups/Internal" \ + ) + + GROUP_ID=$(echo "$GROUP_DATA" | python -c 'import json,sys; obj=json.load(sys.stdin); print obj["id"];') + + UPLOAD_TOKEN=$(curl \ + -X POST \ + --header "Content-Type: application/json" \ + --header "Accept: application/json" \ + --header "X-API-Token: $API_TOKEN" \ + "$API_HOST/v0.1/apps/$API_USER_NAME/$API_APP_NAME/release_uploads" \ + ) + + + UPLOAD_URL=$(echo "$UPLOAD_TOKEN" | python -c 'import json,sys; obj=json.load(sys.stdin); print obj["upload_url"];') + UPLOAD_ID=$(echo "$UPLOAD_TOKEN" | python -c 'import json,sys; obj=json.load(sys.stdin); print obj["upload_id"];') + + curl --progress-bar -F "ipa=@${IPA_PATH}" "$UPLOAD_URL" + + RELEASE_TOKEN=$(curl \ + -X PATCH \ + --header "Content-Type: application/json" \ + --header "Accept: application/json" \ + --header "X-API-Token: $API_TOKEN" \ + -d '{ "status": "committed" }' \ + "$API_HOST/v0.1/apps/$API_USER_NAME/$API_APP_NAME/release_uploads/$UPLOAD_ID" \ + ) + + + RELEASE_URL=$(echo "$RELEASE_TOKEN" | python -c 'import json,sys; obj=json.load(sys.stdin); print obj["release_url"];') + RELEASE_ID=$(echo "$RELEASE_TOKEN" | python -c 'import json,sys; obj=json.load(sys.stdin); print obj["release_id"];') + + curl \ + -X POST \ + --header "Content-Type: application/json" \ + --header "Accept: application/json" \ + --header "X-API-Token: $API_TOKEN" \ + -d "{ \"id\": \"$GROUP_ID\", \"mandatory_update\": false, \"notify_testers\": false }" \ + "$API_HOST/$RELEASE_URL/groups" +} + +upload_dsym() { + UPLOAD_DSYM_DATA=$(curl \ + -X POST \ + --header "Content-Type: application/json" \ + --header "Accept: application/json" \ + --header "X-API-Token: $API_TOKEN" \ + -d "{ \"symbol_type\": \"Apple\"}" \ + "$API_HOST/v0.1/apps/$API_USER_NAME/$API_APP_NAME/symbol_uploads" \ + ) + + DSYM_UPLOAD_URL=$(echo "$UPLOAD_DSYM_DATA" | python -c 'import json,sys; obj=json.load(sys.stdin); print obj["upload_url"];') + DSYM_UPLOAD_ID=$(echo "$UPLOAD_DSYM_DATA" | python -c 'import json,sys; obj=json.load(sys.stdin); print obj["symbol_upload_id"];') + + curl \ + --progress-bar \ + --header "x-ms-blob-type: BlockBlob" \ + --upload-file "${DSYM_PATH}" \ + "$DSYM_UPLOAD_URL" + + curl \ + -X PATCH \ + --header "Content-Type: application/json" \ + --header "Accept: application/json" \ + --header "X-API-Token: $API_TOKEN" \ + -d '{ "status": "committed" }' \ + "$API_HOST/v0.1/apps/$API_USER_NAME/$API_APP_NAME/symbol_uploads/$DSYM_UPLOAD_ID" +} + +upload_ipa diff --git a/buildbox/deploy-telegram.sh b/buildbox/deploy-telegram.sh index 85374f2d68..744c99ea44 100644 --- a/buildbox/deploy-telegram.sh +++ b/buildbox/deploy-telegram.sh @@ -34,12 +34,10 @@ fi if [ "$CONFIGURATION" == "hockeyapp" ]; then FASTLANE_PASSWORD="" FASTLANE_ITC_TEAM_NAME="" - FASTLANE_BUILD_CONFIGURATION="internalhockeyapp" elif [ "$CONFIGURATION" == "appstore" ]; then FASTLANE_PASSWORD="$TELEGRAM_BUILD_APPSTORE_PASSWORD" FASTLANE_ITC_TEAM_NAME="$TELEGRAM_BUILD_APPSTORE_TEAM_NAME" FASTLANE_ITC_USERNAME="$TELEGRAM_BUILD_APPSTORE_USERNAME" - FASTLANE_BUILD_CONFIGURATION="testflight_llc" else echo "Unknown configuration $CONFIGURATION" exit 1 @@ -62,7 +60,6 @@ fi if [ "$1" == "appstore" ]; then export DELIVER_ITMSTRANSPORTER_ADDITIONAL_UPLOAD_PARAMETERS="-t DAV" FASTLANE_PASSWORD="$FASTLANE_PASSWORD" xcrun altool --upload-app --type ios --file "$IPA_PATH" --username "$FASTLANE_ITC_USERNAME" --password "@env:FASTLANE_PASSWORD" - #FASTLANE_PASSWORD="$FASTLANE_PASSWORD" FASTLANE_ITC_TEAM_NAME="$FASTLANE_ITC_TEAM_NAME" fastlane "$FASTLANE_BUILD_CONFIGURATION" build_number:"$BUILD_NUMBER" commit_hash:"$COMMIT_ID" commit_author:"$COMMIT_AUTHOR" skip_build:1 skip_pilot:1 -else - FASTLANE_PASSWORD="$FASTLANE_PASSWORD" FASTLANE_ITC_TEAM_NAME="$FASTLANE_ITC_TEAM_NAME" fastlane "$FASTLANE_BUILD_CONFIGURATION" build_number:"$BUILD_NUMBER" commit_hash:"$COMMIT_ID" commit_author:"$COMMIT_AUTHOR" skip_build:1 +elif [ "$1" == "hockeyapp" ]; then + API_USER_NAME="$API_USER_NAME" API_APP_NAME="$API_APP_NAME" API_TOKEN="$API_TOKEN" sh buildbox/deploy-appcenter.sh fi diff --git a/buildbox/fake-codesigning/certs/distribution/SelfSigned.p12 b/buildbox/fake-codesigning/certs/distribution/SelfSigned.p12 new file mode 100644 index 0000000000..aa2097875b Binary files /dev/null and b/buildbox/fake-codesigning/certs/distribution/SelfSigned.p12 differ diff --git a/buildbox/fake-codesigning/profiles/appstore/self_17a6dde8-0db2-42b4-9215-d8544da24da1.mobileprovision b/buildbox/fake-codesigning/profiles/appstore/self_17a6dde8-0db2-42b4-9215-d8544da24da1.mobileprovision new file mode 100644 index 0000000000..1aa18b4714 Binary files /dev/null and b/buildbox/fake-codesigning/profiles/appstore/self_17a6dde8-0db2-42b4-9215-d8544da24da1.mobileprovision differ diff --git a/buildbox/fake-codesigning/profiles/appstore/self_38e2d395-3b03-4327-b13e-5a81d77a417f.mobileprovision b/buildbox/fake-codesigning/profiles/appstore/self_38e2d395-3b03-4327-b13e-5a81d77a417f.mobileprovision new file mode 100644 index 0000000000..fd919b2eaf Binary files /dev/null and b/buildbox/fake-codesigning/profiles/appstore/self_38e2d395-3b03-4327-b13e-5a81d77a417f.mobileprovision differ diff --git a/buildbox/fake-codesigning/profiles/appstore/self_66d9e1ed-89b0-43a9-81dc-c5db42768deb.mobileprovision b/buildbox/fake-codesigning/profiles/appstore/self_66d9e1ed-89b0-43a9-81dc-c5db42768deb.mobileprovision new file mode 100644 index 0000000000..9c07924826 Binary files /dev/null and b/buildbox/fake-codesigning/profiles/appstore/self_66d9e1ed-89b0-43a9-81dc-c5db42768deb.mobileprovision differ diff --git a/buildbox/fake-codesigning/profiles/appstore/self_a064880e-5214-4456-96f3-3beab43c8a49.mobileprovision b/buildbox/fake-codesigning/profiles/appstore/self_a064880e-5214-4456-96f3-3beab43c8a49.mobileprovision new file mode 100644 index 0000000000..e51dbf5f7a Binary files /dev/null and b/buildbox/fake-codesigning/profiles/appstore/self_a064880e-5214-4456-96f3-3beab43c8a49.mobileprovision differ diff --git a/buildbox/fake-codesigning/profiles/appstore/self_c7ce0e4f-4a34-4f37-9bdf-d93d7e67f935.mobileprovision b/buildbox/fake-codesigning/profiles/appstore/self_c7ce0e4f-4a34-4f37-9bdf-d93d7e67f935.mobileprovision new file mode 100644 index 0000000000..6fbbd2bde9 Binary files /dev/null and b/buildbox/fake-codesigning/profiles/appstore/self_c7ce0e4f-4a34-4f37-9bdf-d93d7e67f935.mobileprovision differ diff --git a/buildbox/fake-codesigning/profiles/appstore/self_d5420e53-0e9a-4745-ab22-1dc7f0f8f0e6.mobileprovision b/buildbox/fake-codesigning/profiles/appstore/self_d5420e53-0e9a-4745-ab22-1dc7f0f8f0e6.mobileprovision new file mode 100644 index 0000000000..86b7d0a3f8 Binary files /dev/null and b/buildbox/fake-codesigning/profiles/appstore/self_d5420e53-0e9a-4745-ab22-1dc7f0f8f0e6.mobileprovision differ diff --git a/buildbox/fake-codesigning/profiles/appstore/self_f5d6daf2-b88a-4de9-846a-bc207ea9b8dd.mobileprovision b/buildbox/fake-codesigning/profiles/appstore/self_f5d6daf2-b88a-4de9-846a-bc207ea9b8dd.mobileprovision new file mode 100644 index 0000000000..ffa8bb22b6 Binary files /dev/null and b/buildbox/fake-codesigning/profiles/appstore/self_f5d6daf2-b88a-4de9-846a-bc207ea9b8dd.mobileprovision differ diff --git a/buildbox/fake-codesigning/profiles/appstore/self_fd2bced7-6353-4f44-8022-979f48e73c04.mobileprovision b/buildbox/fake-codesigning/profiles/appstore/self_fd2bced7-6353-4f44-8022-979f48e73c04.mobileprovision new file mode 100644 index 0000000000..e2e7954016 Binary files /dev/null and b/buildbox/fake-codesigning/profiles/appstore/self_fd2bced7-6353-4f44-8022-979f48e73c04.mobileprovision differ diff --git a/buildbox/generate_fake_codesigning.sh b/buildbox/generate_fake_codesigning.sh new file mode 100644 index 0000000000..05a7907cf5 --- /dev/null +++ b/buildbox/generate_fake_codesigning.sh @@ -0,0 +1,2 @@ +#!/bin/bash + diff --git a/buildbox/guest-build-telegram.sh b/buildbox/guest-build-telegram.sh index 2b1ea2a1fc..0f136a3518 100644 --- a/buildbox/guest-build-telegram.sh +++ b/buildbox/guest-build-telegram.sh @@ -1,5 +1,7 @@ #!/bin/sh +set -x + if [ -z "BUILD_NUMBER" ]; then echo "BUILD_NUMBER is not set" exit 1 @@ -11,15 +13,12 @@ if [ -z "COMMIT_ID" ]; then fi if [ "$1" == "hockeyapp" ] || [ "$1" == "testinghockeyapp" ]; then - FASTLANE_BUILD_CONFIGURATION="internalhockeyapp" - CERTS_PATH="codesigning_data/certs" - PROFILES_PATH="codesigning_data/profiles" + CERTS_PATH="$HOME/codesigning_data/certs" + PROFILES_PATH="$HOME/codesigning_data/profiles" elif [ "$1" == "testinghockeyapp-local" ]; then - FASTLANE_BUILD_CONFIGURATION="testinghockeyapp" - CERTS_PATH="codesigning_data/certs" - PROFILES_PATH="codesigning_data/profiles" + CERTS_PATH="$HOME/codesigning_data/certs" + PROFILES_PATH="$HOME/codesigning_data/profiles" elif [ "$1" == "appstore" ]; then - FASTLANE_BUILD_CONFIGURATION="testflight_llc" if [ -z "$TELEGRAM_BUILD_APPSTORE_PASSWORD" ]; then echo "TELEGRAM_BUILD_APPSTORE_PASSWORD is not set" exit 1 @@ -28,19 +27,11 @@ elif [ "$1" == "appstore" ]; then echo "TELEGRAM_BUILD_APPSTORE_TEAM_NAME is not set" exit 1 fi - FASTLANE_ITC_USERNAME="$TELEGRAM_BUILD_APPSTORE_USERNAME" - FASTLANE_PASSWORD="$TELEGRAM_BUILD_APPSTORE_PASSWORD" - FASTLANE_ITC_TEAM_NAME="$TELEGRAM_BUILD_APPSTORE_TEAM_NAME" - CERTS_PATH="codesigning_data/certs" - PROFILES_PATH="codesigning_data/profiles" + CERTS_PATH="$HOME/codesigning_data/certs" + PROFILES_PATH="$HOME/codesigning_data/profiles" elif [ "$1" == "verify" ]; then - FASTLANE_BUILD_CONFIGURATION="build_for_appstore" - CERTS_PATH="codesigning_data/certs" - PROFILES_PATH="codesigning_data/profiles" -elif [ "$1" == "verify-local" ]; then - FASTLANE_BUILD_CONFIGURATION="build_for_appstore" - CERTS_PATH="buildbox/fake-codesigning/certs" - PROFILES_PATH="buildbox/fake-codesigning/profiles" + CERTS_PATH="build-system/fake-codesigning/certs/distribution" + PROFILES_PATH="build-system/fake-codesigning/profiles" else echo "Unknown configuration $1" exit 1 @@ -57,31 +48,16 @@ security list-keychains -d user -s "$MY_KEYCHAIN" $(security list-keychains -d u security set-keychain-settings "$MY_KEYCHAIN" security unlock-keychain -p "$MY_KEYCHAIN_PASSWORD" "$MY_KEYCHAIN" -for f in $(ls "$CERTS_PATH"); do - fastlane run import_certificate "certificate_path:$CERTS_PATH/$f" keychain_name:"$MY_KEYCHAIN" keychain_password:"$MY_KEYCHAIN_PASSWORD" log_output:true -done +SOURCE_PATH="telegram-ios" -mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles" - -for f in $(ls "$PROFILES_PATH"); do - PROFILE_PATH="$PROFILES_PATH/$f" - uuid=`grep UUID -A1 -a "$PROFILE_PATH" | grep -io "[-A-F0-9]\{36\}"` - cp -f "$PROFILE_PATH" "$HOME/Library/MobileDevice/Provisioning Profiles/$uuid.mobileprovision" -done - -if [ "$1" == "verify-local" ]; then - fastlane "$FASTLANE_BUILD_CONFIGURATION" -else - SOURCE_PATH="telegram-ios" - - if [ -d "$SOURCE_PATH" ]; then - echo "Directory $SOURCE_PATH should not exist" - exit 1 - fi - - mkdir "$SOURCE_PATH" +if [ -d "$SOURCE_PATH" ]; then + echo "Directory $SOURCE_PATH should not exist" + exit 1 +fi +mkdir "$SOURCE_PATH" +if [ "$1" != "verify" ]; then SIZE_IN_BLOCKS=$((12*1024*1024*1024/512)) DEV=`hdid -nomount ram://$SIZE_IN_BLOCKS` @@ -95,38 +71,60 @@ else echo "Error creating ramdisk" exit 1 fi - - echo "Unpacking files..." - - mkdir -p "$SOURCE_PATH/buildbox" - mkdir -p "$SOURCE_PATH/buildbox/transient-data" - cp -r "$HOME/codesigning_teams" "$SOURCE_PATH/buildbox/transient-data/teams" - - BASE_DIR=$(pwd) - cd "$SOURCE_PATH" - tar -xf "../source.tar" - - if [ "$1" == "hockeyapp" ]; then - BUILD_ENV_SCRIPT="internal" - FASTLANE_BUILD_CONFIGURATION="internalhockeyapp" - APP_TARGET="app_arm64" - elif [ "$1" == "appstore" ]; then - BUILD_ENV_SCRIPT="appstore" - FASTLANE_BUILD_CONFIGURATION="testflight_llc" - APP_TARGET="app" - else - echo "Unsupported configuration $1" - exit 1 - fi - - BUCK="$(pwd)/tools/buck" BUCK_HTTP_CACHE="$BUCK_HTTP_CACHE" LOCAL_CODESIGNING=1 sh "../telegram-ios-shared/buildbox/bin/$BUILD_ENV_SCRIPT.sh" make "$APP_TARGET" - - OUTPUT_PATH="build/artifacts" - rm -rf "$OUTPUT_PATH" - mkdir -p "$OUTPUT_PATH" - - cp "build/Telegram_signed.ipa" "./$OUTPUT_PATH/Telegram.ipa" - cp "build/DSYMs.zip" "./$OUTPUT_PATH/Telegram.DSYMs.zip" - - cd "$BASE_DIR" fi + +echo "Unpacking files..." + +mkdir -p "$SOURCE_PATH/buildbox" +mkdir -p "$SOURCE_PATH/buildbox/transient-data" +cp -r "$HOME/codesigning_teams" "$SOURCE_PATH/buildbox/transient-data/teams" + +BASE_DIR=$(pwd) +cd "$SOURCE_PATH" +tar -xf "../source.tar" + +for f in $(ls "$CERTS_PATH"); do + security import "$CERTS_PATH/$f" -k "$MY_KEYCHAIN" -P "" -T /usr/bin/codesign -T /usr/bin/security +done + +security set-key-partition-list -S apple-tool:,apple: -k "$MY_KEYCHAIN_PASSWORD" "$MY_KEYCHAIN" + +mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles" + +for f in $(ls "$PROFILES_PATH"); do + PROFILE_PATH="$PROFILES_PATH/$f" + uuid=`grep UUID -A1 -a "$PROFILE_PATH" | grep -io "[-A-F0-9]\{36\}"` + cp -f "$PROFILE_PATH" "$HOME/Library/MobileDevice/Provisioning Profiles/$uuid.mobileprovision" +done + +if [ "$1" == "hockeyapp" ]; then + BUILD_ENV_SCRIPT="../telegram-ios-shared/buildbox/bin/internal.sh" + APP_TARGET="app_arm64" +elif [ "$1" == "appstore" ]; then + BUILD_ENV_SCRIPT="../telegram-ios-shared/buildbox/bin/appstore.sh" + APP_TARGET="app" +elif [ "$1" == "verify" ]; then + BUILD_ENV_SCRIPT="build-system/verify.sh" + APP_TARGET="app" + export CODESIGNING_DATA_PATH="build-system/fake-codesigning" + export CODESIGNING_CERTS_VARIANT="distribution" + export CODESIGNING_PROFILES_VARIANT="appstore" +else + echo "Unsupported configuration $1" + exit 1 +fi + +if [ -d "$BUCK_DIR_CACHE" ]; then + sudo chown telegram "$BUCK_DIR_CACHE" +fi + +BUCK="$(pwd)/tools/buck" BUCK_HTTP_CACHE="$BUCK_HTTP_CACHE" BUCK_CACHE_MODE="$BUCK_CACHE_MODE" BUCK_DIR_CACHE="$BUCK_DIR_CACHE" LOCAL_CODESIGNING=1 sh "$BUILD_ENV_SCRIPT" make "$APP_TARGET" + +OUTPUT_PATH="build/artifacts" +rm -rf "$OUTPUT_PATH" +mkdir -p "$OUTPUT_PATH" + +cp "build/Telegram_signed.ipa" "./$OUTPUT_PATH/Telegram.ipa" +cp "build/DSYMs.zip" "./$OUTPUT_PATH/Telegram.DSYMs.zip" + +cd "$BASE_DIR" diff --git a/buildbox/verify-telegram.sh b/buildbox/verify-telegram.sh new file mode 100644 index 0000000000..371382f6be --- /dev/null +++ b/buildbox/verify-telegram.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +set -e +set -x + +CONFIGURATION="$1" +MODE="$2" + +if [ -z "$CONFIGURATION" ] || [ -z "$MODE" ] ; then + echo "Usage: sh deploy-telegram.sh CONFIGURATION [cached|full]" + exit 1 +fi + +if [ "$MODE" == "cached" ]; then + BUCK_HTTP_CACHE="$BUCK_HTTP_CACHE" + ERROR_OUTPUT_PATH="build/verifysanity_artifacts" +elif [ "$MODE" == "full" ]; then + BUCK_HTTP_CACHE="" + ERROR_OUTPUT_PATH="build/verify_artifacts" +else + echo "Unknown mode $MODE" + exit 1 +fi + +OUTPUT_PATH="build/artifacts" + +if [ "$CONFIGURATION" == "appstore" ]; then + IPA_PATH="$OUTPUT_PATH/Telegram.ipa" +else + echo "Unknown configuration $CONFIGURATION" + exit 1 +fi + +if [ ! -f "$IPA_PATH" ]; then + echo "$IPA_PATH not found" + exit 1 +fi + +VERIFY_PATH="TelegramVerifyBuild.ipa" + +mv "$IPA_PATH" "$VERIFY_PATH" + +BUCK_HTTP_CACHE="$BUCK_HTTP_CACHE" sh buildbox/build-telegram.sh verify + +python3 tools/ipadiff.py "$IPA_PATH" "$VERIFY_PATH" +retVal=$? +if [ $retVal -ne 0 ]; then + mkdir -p "$ERROR_OUTPUT_PATH" + cp "$IPA_PATH" "$ERROR_OUTPUT_PATH"/ + exit 1 +fi + + diff --git a/submodules/AccountContext/BUCK b/submodules/AccountContext/BUCK index 120fec2a7e..6a3a3d431f 100644 --- a/submodules/AccountContext/BUCK +++ b/submodules/AccountContext/BUCK @@ -16,7 +16,7 @@ static_library( "//submodules/Postbox:Postbox#shared", "//submodules/TelegramCore:TelegramCore#shared", "//submodules/SyncCore:SyncCore#shared", - "//submodules/WalletCore:WalletCore", + #"//submodules/WalletCore:WalletCore", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 4d16677106..87383884ce 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -8,7 +8,10 @@ import SwiftSignalKit import Display import DeviceLocationManager import TemporaryCachedPeerDataManager + +#if ENABLE_WALLET import WalletCore +#endif public final class TelegramApplicationOpenUrlCompletion { public let completion: (Bool) -> Void @@ -148,8 +151,14 @@ public struct ChatAvailableMessageActions { } public enum WallpaperUrlParameter { - case slug(String, WallpaperPresentationOptions, UIColor?, Int32?) + case slug(String, WallpaperPresentationOptions, UIColor?, UIColor?, Int32?, Int32?) case color(UIColor) + case gradient(UIColor, UIColor, Int32?) +} + +public enum ResolvedUrlSettingsSection { + case theme + case devices } public enum ResolvedUrl { @@ -169,7 +178,10 @@ public enum ResolvedUrl { case share(url: String?, text: String?, to: String?) case wallpaper(WallpaperUrlParameter) case theme(String) + #if ENABLE_WALLET case wallet(address: String, amount: Int64?, comment: String?) + #endif + case settings(ResolvedUrlSettingsSection) } public enum NavigateToChatKeepStack { @@ -190,12 +202,13 @@ public final class NavigateToChatControllerParams { public let keepStack: NavigateToChatKeepStack public let purposefulAction: (() -> Void)? public let scrollToEndIfExists: Bool + public let activateMessageSearch: Bool public let animated: Bool public let options: NavigationAnimationOptions public let parentGroupId: PeerGroupId? public let completion: () -> Void - public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping () -> Void = {}) { + public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: Bool = false, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping () -> Void = {}) { self.navigationController = navigationController self.chatController = chatController self.context = context @@ -207,6 +220,7 @@ public final class NavigateToChatControllerParams { self.keepStack = keepStack self.purposefulAction = purposefulAction self.scrollToEndIfExists = scrollToEndIfExists + self.activateMessageSearch = activateMessageSearch self.animated = animated self.options = options self.parentGroupId = parentGroupId @@ -245,6 +259,7 @@ public enum DeviceContactInfoSubject { public enum PeerInfoControllerMode { case generic case calls(messages: [Message]) + case nearbyPeer } public enum ContactListActionItemInlineIconPosition { @@ -371,10 +386,12 @@ public final class ContactSelectionControllerParams { } } +#if ENABLE_WALLET public enum OpenWalletContext { case generic case send(address: String, amount: Int64?, comment: String?) } +#endif public let defaultContactLabel: String = "_$!!$_" @@ -386,12 +403,16 @@ public enum CreateGroupMode { public protocol AppLockContext: class { var invalidAttempts: Signal { get } + var autolockDeadline: Signal { get } func lock() func unlock() func failedUnlockAttempt() } +public protocol RecentSessionsController: class { +} + public protocol SharedAccountContext: class { var basePath: String { get } var mainWindow: Window1? { get } @@ -403,6 +424,7 @@ public protocol SharedAccountContext: class { var currentAutomaticMediaDownloadSettings: Atomic { get } var automaticMediaDownloadSettings: Signal { get } + var currentAutodownloadSettings: Atomic { get } var immediateExperimentalUISettings: ExperimentalUISettings { get } var currentInAppNotificationSettings: Atomic { get } var currentMediaInputSettings: Atomic { get } @@ -428,13 +450,14 @@ public protocol SharedAccountContext: class { func openChatMessage(_ params: OpenChatMessageParams) -> Bool func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, account: Account, chatLocation: ChatLocation, tagMask: MessageTags?) -> Signal<(MessageIndex?, Bool), NoError> func makeOverlayAudioPlayerController(context: AccountContext, peerId: PeerId, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, parentNavigationController: NavigationController?) -> ViewController & OverlayAudioPlayerController - func makePeerInfoController(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode) -> ViewController? + func makePeerInfoController(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool) -> ViewController? func makeDeviceContactInfoController(context: AccountContext, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController func makePeersNearbyController(context: AccountContext) -> ViewController func makeComposeController(context: AccountContext) -> ViewController func makeChatListController(context: AccountContext, groupId: PeerGroupId, controlsHistoryPreload: Bool, hideNetworkActivityStatus: Bool, previewing: Bool, enableDebugActions: Bool) -> ChatListController func makeChatController(context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, botStart: ChatControllerInitialBotStart?, mode: ChatControllerPresentationMode) -> ChatController - func makeChatMessagePreviewItem(context: AccountContext, message: Message, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?) -> ListViewItem + func makeChatMessagePreviewItem(context: AccountContext, message: Message, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)?) -> ListViewItem + func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader func makePeerSharedMediaController(context: AccountContext, peerId: PeerId) -> ViewController? func makeContactSelectionController(_ params: ContactSelectionControllerParams) -> ContactSelectionController func makeContactMultiselectionController(_ params: ContactMultiselectionControllerParams) -> ContactMultiselectionController @@ -447,13 +470,17 @@ public protocol SharedAccountContext: class { func openExternalUrl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void) func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messageIds: Set) -> Signal func resolveUrl(account: Account, url: String) -> Signal - func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void) + func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) func openAddContact(context: AccountContext, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void) func openAddPersonContact(context: AccountContext, peerId: PeerId, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void) func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void) + #if ENABLE_WALLET func openWallet(context: AccountContext, walletContext: OpenWalletContext, present: @escaping (ViewController) -> Void) + #endif func openImagePicker(context: AccountContext, completion: @escaping (UIImage) -> Void, present: @escaping (ViewController) -> Void) + func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController + func navigateToCurrentCall() var hasOngoingCall: ValuePromise { get } var immediateHasOngoingCall: Bool { get } @@ -462,6 +489,7 @@ public protocol SharedAccountContext: class { func beginNewAuth(testingEnvironment: Bool) } +#if ENABLE_WALLET private final class TonInstanceData { var config: String? var blockchainName: String? @@ -526,10 +554,15 @@ public final class TonContext { } } +#endif + public protocol AccountContext: class { var sharedContext: SharedAccountContext { get } var account: Account { get } + + #if ENABLE_WALLET var tonContext: StoredTonContext? { get } + #endif var liveLocationManager: LiveLocationManager? { get } var fetchManager: FetchManager { get } @@ -537,10 +570,14 @@ public protocol AccountContext: class { var peerChannelMemberCategoriesContextsManager: PeerChannelMemberCategoriesContextsManager { get } var wallpaperUploadManager: WallpaperUploadManager? { get } var watchManager: WatchManager? { get } + + #if ENABLE_WALLET var hasWallets: Signal { get } var hasWalletAccess: Signal { get } + #endif var currentLimitsConfiguration: Atomic { get } + var currentContentSettings: Atomic { get } func storeSecureIdPassword(password: String) func getStoredSecureIdPassword() -> String? diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 9e21f14875..da9d89bc63 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -246,7 +246,7 @@ public enum ChatControllerSubject: Equatable { public enum ChatControllerPresentationMode: Equatable { case standard(previewing: Bool) - case overlay + case overlay(NavigationController?) case inline(NavigationController?) } diff --git a/submodules/AccountContext/Sources/DeviceContactData.swift b/submodules/AccountContext/Sources/DeviceContactData.swift index c219196ef8..674b035809 100644 --- a/submodules/AccountContext/Sources/DeviceContactData.swift +++ b/submodules/AccountContext/Sources/DeviceContactData.swift @@ -261,8 +261,9 @@ public final class DeviceContactExtendedData: Equatable { public let birthdayDate: Date? public let socialProfiles: [DeviceContactSocialProfileData] public let instantMessagingProfiles: [DeviceContactInstantMessagingProfileData] + public let note: String - public init(basicData: DeviceContactBasicData, middleName: String, prefix: String, suffix: String, organization: String, jobTitle: String, department: String, emailAddresses: [DeviceContactEmailAddressData], urls: [DeviceContactUrlData], addresses: [DeviceContactAddressData], birthdayDate: Date?, socialProfiles: [DeviceContactSocialProfileData], instantMessagingProfiles: [DeviceContactInstantMessagingProfileData]) { + public init(basicData: DeviceContactBasicData, middleName: String, prefix: String, suffix: String, organization: String, jobTitle: String, department: String, emailAddresses: [DeviceContactEmailAddressData], urls: [DeviceContactUrlData], addresses: [DeviceContactAddressData], birthdayDate: Date?, socialProfiles: [DeviceContactSocialProfileData], instantMessagingProfiles: [DeviceContactInstantMessagingProfileData], note: String) { self.basicData = basicData self.middleName = middleName self.prefix = prefix @@ -276,6 +277,7 @@ public final class DeviceContactExtendedData: Equatable { self.birthdayDate = birthdayDate self.socialProfiles = socialProfiles self.instantMessagingProfiles = instantMessagingProfiles + self.note = note } public static func ==(lhs: DeviceContactExtendedData, rhs: DeviceContactExtendedData) -> Bool { @@ -318,6 +320,9 @@ public final class DeviceContactExtendedData: Equatable { if lhs.instantMessagingProfiles != rhs.instantMessagingProfiles { return false } + if lhs.note != rhs.note { + return false + } return true } } @@ -420,7 +425,7 @@ public extension DeviceContactExtendedData { } let basicData = DeviceContactBasicData(firstName: contact.givenName, lastName: contact.familyName, phoneNumbers: phoneNumbers) - self.init(basicData: basicData, middleName: contact.middleName, prefix: contact.namePrefix, suffix: contact.nameSuffix, organization: contact.organizationName, jobTitle: contact.jobTitle, department: contact.departmentName, emailAddresses: emailAddresses, urls: urls, addresses: addresses, birthdayDate: birthdayDate, socialProfiles: socialProfiles, instantMessagingProfiles: instantMessagingProfiles) + self.init(basicData: basicData, middleName: contact.middleName, prefix: contact.namePrefix, suffix: contact.nameSuffix, organization: contact.organizationName, jobTitle: contact.jobTitle, department: contact.departmentName, emailAddresses: emailAddresses, urls: urls, addresses: addresses, birthdayDate: birthdayDate, socialProfiles: socialProfiles, instantMessagingProfiles: instantMessagingProfiles, note: "") } var isPrimitive: Bool { @@ -454,6 +459,9 @@ public extension DeviceContactExtendedData { if !self.instantMessagingProfiles.isEmpty { return false } + if !self.note.isEmpty { + return false + } return true } } @@ -467,7 +475,7 @@ public extension DeviceContactExtendedData { if let phone = user.phone, !phone.isEmpty { phoneNumbers.append(DeviceContactPhoneNumberData(label: "_$!!$_", value: phone)) } - self.init(basicData: DeviceContactBasicData(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumbers: phoneNumbers), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: []) + self.init(basicData: DeviceContactBasicData(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumbers: phoneNumbers), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") } } @@ -491,4 +499,41 @@ extension DeviceContactAddressData { } return dictionary } + + public var string: String { + var array: [String] = [] + if !self.street1.isEmpty { + array.append(self.street1) + } + if !self.city.isEmpty { + array.append(self.city) + } + if !self.state.isEmpty { + array.append(self.state) + } + if !self.country.isEmpty { + array.append(self.country) + } + if !self.postcode.isEmpty { + array.append(self.postcode) + } + return array.joined(separator: " ") + } + + public var displayString: String { + var array: [String] = [] + if !self.street1.isEmpty { + array.append(self.street1) + } + if !self.city.isEmpty { + array.append(self.city) + } + if !self.state.isEmpty { + array.append(self.state) + } + if !self.country.isEmpty { + array.append(self.country) + } + return array.joined(separator: ", ") + } } diff --git a/submodules/AccountContext/Sources/OpenChatMessage.swift b/submodules/AccountContext/Sources/OpenChatMessage.swift index 1e3507fc3c..4ff4350a6f 100644 --- a/submodules/AccountContext/Sources/OpenChatMessage.swift +++ b/submodules/AccountContext/Sources/OpenChatMessage.swift @@ -25,7 +25,7 @@ public final class OpenChatMessageParams { public let modal: Bool public let dismissInput: () -> Void public let present: (ViewController, Any?) -> Void - public let transitionNode: (MessageId, Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? + public let transitionNode: (MessageId, Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? public let addToTransitionSurface: (UIView) -> Void public let openUrl: (String) -> Void public let openPeer: (Peer, ChatControllerInteractionNavigateToPeer) -> Void @@ -46,7 +46,7 @@ public final class OpenChatMessageParams { modal: Bool = false, dismissInput: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void, - transitionNode: @escaping (MessageId, Media) -> (ASDisplayNode, () -> (UIView?, UIView?))?, + transitionNode: @escaping (MessageId, Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, addToTransitionSurface: @escaping (UIView) -> Void, openUrl: @escaping (String) -> Void, openPeer: @escaping (Peer, ChatControllerInteractionNavigateToPeer) -> Void, diff --git a/submodules/AccountContext/Sources/PeerSelectionController.swift b/submodules/AccountContext/Sources/PeerSelectionController.swift index fee175e265..5037f67c93 100644 --- a/submodules/AccountContext/Sources/PeerSelectionController.swift +++ b/submodules/AccountContext/Sources/PeerSelectionController.swift @@ -25,6 +25,8 @@ public struct ChatListNodePeersFilter: OptionSet { public static let excludeDisabled = ChatListNodePeersFilter(rawValue: 1 << 10) public static let includeSavedMessages = ChatListNodePeersFilter(rawValue: 1 << 11) + + public static let excludeChannels = ChatListNodePeersFilter(rawValue: 1 << 12) } public final class PeerSelectionControllerParams { @@ -32,12 +34,14 @@ public final class PeerSelectionControllerParams { public let filter: ChatListNodePeersFilter public let hasContactSelector: Bool public let title: String? + public let attemptSelection: ((Peer) -> Void)? - public init(context: AccountContext, filter: ChatListNodePeersFilter = [.onlyWriteable], hasContactSelector: Bool = true, title: String? = nil) { + public init(context: AccountContext, filter: ChatListNodePeersFilter = [.onlyWriteable], hasContactSelector: Bool = true, title: String? = nil, attemptSelection: ((Peer) -> Void)? = nil) { self.context = context self.filter = filter self.hasContactSelector = hasContactSelector self.title = title + self.attemptSelection = attemptSelection } } diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 17ee887c73..0e95feda69 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -16,6 +16,7 @@ public enum PresentationCallState: Equatable { case requesting(Bool) case connecting(Data?) case active(Double, Int32?, Data) + case reconnecting(Double, Int32?, Data) case terminating case terminated(CallId?, CallSessionTerminationReason?, Bool) } diff --git a/submodules/AccountUtils/BUCK b/submodules/AccountUtils/BUCK new file mode 100644 index 0000000000..f5cec5d49c --- /dev/null +++ b/submodules/AccountUtils/BUCK @@ -0,0 +1,19 @@ +load("//Config:buck_rule_macros.bzl", "static_library") + +static_library( + name = "AccountUtils", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared", + "//submodules/Postbox:Postbox#shared", + "//submodules/TelegramCore:TelegramCore#shared", + "//submodules/SyncCore:SyncCore#shared", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + ], + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + ], +) diff --git a/submodules/SettingsUI/Sources/AccountUtils.swift b/submodules/AccountUtils/Sources/AccountUtils.swift similarity index 81% rename from submodules/SettingsUI/Sources/AccountUtils.swift rename to submodules/AccountUtils/Sources/AccountUtils.swift index a33713f992..607d783966 100644 --- a/submodules/SettingsUI/Sources/AccountUtils.swift +++ b/submodules/AccountUtils/Sources/AccountUtils.swift @@ -6,7 +6,9 @@ import SyncCore import TelegramUIPreferences import AccountContext -func activeAccountsAndPeers(context: AccountContext) -> Signal<((Account, Peer)?, [(Account, Peer, Int32)]), NoError> { +public let maximumNumberOfAccounts = 3 + +public func activeAccountsAndPeers(context: AccountContext, includePrimary: Bool = false) -> Signal<((Account, Peer)?, [(Account, Peer, Int32)]), NoError> { let sharedContext = context.sharedContext return context.sharedContext.activeAccounts |> mapToSignal { primary, activeAccounts, _ -> Signal<((Account, Peer)?, [(Account, Peer, Int32)]), NoError> in @@ -37,7 +39,8 @@ func activeAccountsAndPeers(context: AccountContext) -> Signal<((Account, Peer)? if let first = accounts.filter({ $0?.0.id == primary?.id }).first, let (account, peer, _) = first { primaryRecord = (account, peer) } - return (primaryRecord, accounts.filter({ $0?.0.id != primary?.id }).compactMap({ $0 })) + let accountRecords: [(Account, Peer, Int32)] = (includePrimary ? accounts : accounts.filter({ $0?.0.id != primary?.id })).compactMap({ $0 }) + return (primaryRecord, accountRecords) } } } diff --git a/submodules/ActionSheetPeerItem/BUCK b/submodules/ActionSheetPeerItem/BUCK index 3057307d75..dcbc59dd47 100644 --- a/submodules/ActionSheetPeerItem/BUCK +++ b/submodules/ActionSheetPeerItem/BUCK @@ -13,6 +13,7 @@ static_library( "//submodules/Display:Display#shared", "//submodules/AvatarNode:AvatarNode", "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/AccountContext:AccountContext", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/ActionSheetPeerItem/Sources/ActionSheetPeerItem.swift b/submodules/ActionSheetPeerItem/Sources/ActionSheetPeerItem.swift index d3e81caa6a..07d521cf2f 100644 --- a/submodules/ActionSheetPeerItem/Sources/ActionSheetPeerItem.swift +++ b/submodules/ActionSheetPeerItem/Sources/ActionSheetPeerItem.swift @@ -7,9 +7,10 @@ import SyncCore import Postbox import TelegramPresentationData import AvatarNode +import AccountContext public class ActionSheetPeerItem: ActionSheetItem { - public let account: Account + public let context: AccountContext public let peer: Peer public let theme: PresentationTheme public let title: String @@ -17,8 +18,8 @@ public class ActionSheetPeerItem: ActionSheetItem { public let strings: PresentationStrings public let action: () -> Void - public init(account: Account, peer: Peer, title: String, isSelected: Bool, strings: PresentationStrings, theme: PresentationTheme, action: @escaping () -> Void) { - self.account = account + public init(context: AccountContext, peer: Peer, title: String, isSelected: Bool, strings: PresentationStrings, theme: PresentationTheme, action: @escaping () -> Void) { + self.context = context self.peer = peer self.title = title self.isSelected = isSelected @@ -48,7 +49,7 @@ private let avatarFont = avatarPlaceholderFont(size: 15.0) public class ActionSheetPeerItemNode: ActionSheetItemNode { private let theme: ActionSheetControllerTheme - public static let defaultFont: UIFont = Font.regular(20.0) + private let defaultFont: UIFont private var item: ActionSheetPeerItem? @@ -62,6 +63,8 @@ public class ActionSheetPeerItemNode: ActionSheetItemNode { override public init(theme: ActionSheetControllerTheme) { self.theme = theme + self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) + self.button = HighlightTrackingButton() self.button.isAccessibilityElement = false @@ -114,10 +117,12 @@ public class ActionSheetPeerItemNode: ActionSheetItemNode { func setItem(_ item: ActionSheetPeerItem) { self.item = item - let textColor: UIColor = self.theme.primaryTextColor - self.label.attributedText = NSAttributedString(string: item.title, font: ActionSheetButtonNode.defaultFont, textColor: textColor) + let defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) - self.avatarNode.setPeer(account: item.account, theme: item.theme, peer: item.peer) + let textColor: UIColor = self.theme.primaryTextColor + self.label.attributedText = NSAttributedString(string: item.title, font: defaultFont, textColor: textColor) + + self.avatarNode.setPeer(context: item.context, theme: item.theme, peer: item.peer) self.checkNode.isHidden = !item.isSelected diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index 92df089561..173646c596 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -42,9 +42,15 @@ public enum AnimatedStickerMode { case direct } +public enum AnimatedStickerPlaybackPosition { + case start + case end +} + public enum AnimatedStickerPlaybackMode { case once case loop + case still(AnimatedStickerPlaybackPosition) } private final class AnimatedStickerFrame { @@ -71,13 +77,25 @@ private protocol AnimatedStickerFrameSource: class { var frameRate: Int { get } var frameCount: Int { get } - func takeFrame() -> AnimatedStickerFrame + func takeFrame() -> AnimatedStickerFrame? + func skipToEnd() +} + +private final class AnimatedStickerFrameSourceWrapper { + let value: AnimatedStickerFrameSource + + init(_ value: AnimatedStickerFrameSource) { + self.value = value + } } @available(iOS 9.0, *) private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource { private let queue: Queue - private let data: Data + private var data: Data + private var dataComplete: Bool + private let notifyUpdated: () -> Void + private var scratchBuffer: Data let width: Int let bytesPerRow: Int @@ -90,9 +108,11 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource var decodeBuffer: Data var frameBuffer: Data - init?(queue: Queue, data: Data) { + init?(queue: Queue, data: Data, complete: Bool, notifyUpdated: @escaping () -> Void) { self.queue = queue self.data = data + self.dataComplete = complete + self.notifyUpdated = notifyUpdated self.scratchBuffer = Data(count: compression_decode_scratch_buffer_size(COMPRESSION_LZFSE)) var offset = 0 @@ -152,7 +172,7 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource assert(self.queue.isCurrent()) } - func takeFrame() -> AnimatedStickerFrame { + func takeFrame() -> AnimatedStickerFrame? { var frameData: Data? var isLastFrame = false @@ -163,8 +183,24 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource let frameIndex = self.frameIndex self.data.withUnsafeBytes { (bytes: UnsafePointer) -> Void in + if self.offset + 4 > dataLength { + if self.dataComplete { + self.frameIndex = 0 + self.offset = self.initialOffset + self.frameBuffer.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in + memset(bytes, 0, frameBufferLength) + } + } + return + } + var frameLength: Int32 = 0 memcpy(&frameLength, bytes.advanced(by: self.offset), 4) + + if self.offset + 4 + Int(frameLength) > dataLength { + return + } + self.offset += 4 self.scratchBuffer.withUnsafeMutableBytes { (scratchBytes: UnsafeMutablePointer) -> Void in @@ -194,7 +230,7 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource self.frameIndex += 1 self.offset += Int(frameLength) - if self.offset == dataLength { + if self.offset == dataLength && self.dataComplete { isLastFrame = true self.frameIndex = 0 self.offset = self.initialOffset @@ -204,7 +240,19 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource } } - return AnimatedStickerFrame(data: frameData!, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: isLastFrame) + if let frameData = frameData { + return AnimatedStickerFrame(data: frameData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: isLastFrame) + } else { + return nil + } + } + + func updateData(data: Data, complete: Bool) { + self.data = data + self.dataComplete = complete + } + + func skipToEnd() { } } @@ -241,7 +289,7 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource assert(self.queue.isCurrent()) } - func takeFrame() -> AnimatedStickerFrame { + func takeFrame() -> AnimatedStickerFrame? { let frameIndex = self.currentFrame % self.frameCount self.currentFrame += 1 var frameData = Data(count: self.bytesPerRow * self.height) @@ -251,6 +299,10 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource } return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1) } + + func skipToEnd() { + self.currentFrame = self.frameCount - 1 + } } private final class AnimatedStickerFrameQueue { @@ -271,15 +323,23 @@ private final class AnimatedStickerFrameQueue { func take() -> AnimatedStickerFrame? { if self.frames.isEmpty { - self.frames.append(self.source.takeFrame()) + if let frame = self.source.takeFrame() { + self.frames.append(frame) + } + } + if !self.frames.isEmpty { + let frame = self.frames.removeFirst() + return frame + } else { + return nil } - let frame = self.frames.removeFirst() - return frame } func generateFramesIfNeeded() { if self.frames.isEmpty { - self.frames.append(self.source.takeFrame()) + if let frame = self.source.takeFrame() { + self.frames.append(frame) + } } } } @@ -297,7 +357,7 @@ public struct AnimatedStickerStatus: Equatable { } public protocol AnimatedStickerNodeSource { - func cachedDataPath(width: Int, height: Int) -> Signal + func cachedDataPath(width: Int, height: Int) -> Signal<(String, Bool), NoError> func directDataPath() -> Signal } @@ -312,7 +372,7 @@ public final class AnimatedStickerNodeLocalFileSource: AnimatedStickerNodeSource return .single(self.path) } - public func cachedDataPath(width: Int, height: Int) -> Signal { + public func cachedDataPath(width: Int, height: Int) -> Signal<(String, Bool), NoError> { return .never() } } @@ -330,13 +390,14 @@ public final class AnimatedStickerNode: ASDisplayNode { private var reportedStarted = false private let timer = Atomic(value: nil) + private let frameSource = Atomic?>(value: nil) private var directData: (Data, String, Int, Int)? - private var cachedData: Data? + private var cachedData: (Data, Bool)? private var renderer: (AnimationRenderer & ASDisplayNode)? - private var isPlaying: Bool = false + public var isPlaying: Bool = false private var canDisplayFirstFrame: Bool = false private var playbackMode: AnimatedStickerPlaybackMode = .loop @@ -409,7 +470,9 @@ public final class AnimatedStickerNode: ASDisplayNode { if let directData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { strongSelf.directData = (directData, path, width, height) } - if strongSelf.isPlaying { + if case let .still(position) = playbackMode { + strongSelf.seekTo(position) + } else if strongSelf.isPlaying { strongSelf.play() } else if strongSelf.canDisplayFirstFrame { strongSelf.play(firstFrame: true) @@ -421,15 +484,30 @@ public final class AnimatedStickerNode: ASDisplayNode { })) case .cached: self.disposable.set((source.cachedDataPath(width: width, height: height) - |> deliverOnMainQueue).start(next: { [weak self] path in + |> deliverOnMainQueue).start(next: { [weak self] path, complete in guard let strongSelf = self else { return } - strongSelf.cachedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) - if strongSelf.isPlaying { - strongSelf.play() - } else if strongSelf.canDisplayFirstFrame { - strongSelf.play(firstFrame: true) + if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { + if let (_, currentComplete) = strongSelf.cachedData { + if !currentComplete { + strongSelf.cachedData = (data, complete) + strongSelf.frameSource.with { frameSource in + frameSource?.with { frameSource in + if let frameSource = frameSource.value as? AnimatedStickerCachedFrameSource { + frameSource.updateData(data: data, complete: complete) + } + } + } + } + } else { + strongSelf.cachedData = (data, complete) + if strongSelf.isPlaying { + strongSelf.play() + } else if strongSelf.canDisplayFirstFrame { + strongSelf.play(firstFrame: true) + } + } } })) } @@ -464,15 +542,24 @@ public final class AnimatedStickerNode: ASDisplayNode { let cachedData = self.cachedData let queue = self.queue let timerHolder = self.timer + let frameSourceHolder = self.frameSource self.queue.async { [weak self] in var maybeFrameSource: AnimatedStickerFrameSource? + var notifyUpdated: (() -> Void)? if let directData = directData { maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3) - } else if let cachedData = cachedData { + } else if let (cachedData, cachedDataComplete) = cachedData { if #available(iOS 9.0, *) { - maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData) + maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: { + notifyUpdated?() + }) } } + let _ = frameSourceHolder.swap(maybeFrameSource.flatMap { maybeFrameSource in + return QueueLocalObject(queue: queue, generate: { + return AnimatedStickerFrameSourceWrapper(maybeFrameSource) + }) + }) guard let frameSource = maybeFrameSource else { return } @@ -526,11 +613,11 @@ public final class AnimatedStickerNode: ASDisplayNode { self.reportedStarted = false self.timer.swap(nil)?.invalidate() if self.playToCompletionOnStop { - self.seekToStart() + self.seekTo(.start) } } - public func seekToStart() { + public func seekTo(_ position: AnimatedStickerPlaybackPosition) { self.isPlaying = false let directData = self.directData @@ -541,9 +628,12 @@ public final class AnimatedStickerNode: ASDisplayNode { var maybeFrameSource: AnimatedStickerFrameSource? if let directData = directData { maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3) - } else if let cachedData = cachedData { + if position == .end { + maybeFrameSource?.skipToEnd() + } + } else if let (cachedData, cachedDataComplete) = cachedData { if #available(iOS 9.0, *) { - maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData) + maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {}) } } guard let frameSource = maybeFrameSource else { diff --git a/submodules/AnimationUI/Sources/AnimationNode.swift b/submodules/AnimationUI/Sources/AnimationNode.swift index 026ad71701..9b896f983f 100644 --- a/submodules/AnimationUI/Sources/AnimationNode.swift +++ b/submodules/AnimationUI/Sources/AnimationNode.swift @@ -46,6 +46,35 @@ public final class AnimationNode : ASDisplayNode { }) } + public init(animationData: Data, colors: [String: UIColor]? = nil, scale: CGFloat = 1.0) { + self.scale = scale + + super.init() + + self.setViewBlock({ + if let json = try? JSONSerialization.jsonObject(with: animationData, options: []) as? [AnyHashable: Any] { + let composition = LOTComposition(json: json) + + let view = LOTAnimationView(model: composition, in: getAppBundle()) + view.animationSpeed = self.speed + view.backgroundColor = .clear + view.isOpaque = false + + if let colors = colors { + for (key, value) in colors { + let colorCallback = LOTColorValueCallback(color: value.cgColor) + self.colorCallbacks.append(colorCallback) + view.setValueDelegate(colorCallback, for: LOTKeypath(string: "\(key).Color")) + } + } + + return view + } else { + return LOTAnimationView() + } + }) + } + public func setAnimation(name: String) { if let url = getAppBundle().url(forResource: name, withExtension: "json"), let composition = LOTComposition(filePath: url.path) { self.animationView()?.sceneModel = composition diff --git a/submodules/AppIntents/Sources/AppIntents.swift b/submodules/AppIntents/Sources/AppIntents.swift deleted file mode 100644 index 676d2373cc..0000000000 --- a/submodules/AppIntents/Sources/AppIntents.swift +++ /dev/null @@ -1,67 +0,0 @@ -import Foundation -import UIKit -import Intents -import Display -import Postbox -import TelegramCore -import SyncCore -import SwiftSignalKit -import TelegramPresentationData -import AvatarNode -import AccountContext - -public func donateSendMessageIntent(account: Account, sharedContext: SharedAccountContext, peerIds: [PeerId]) { - if #available(iOSApplicationExtension 13.2, iOS 13.2, *) { - let _ = (account.postbox.transaction { transaction -> [Peer] in - var peers: [Peer] = [] - for peerId in peerIds { - if peerId.namespace == Namespaces.Peer.CloudUser && peerId != account.peerId, let peer = transaction.getPeer(peerId) { - peers.append(peer) - } - } - return peers - } - |> mapToSignal { peers -> Signal<[(Peer, UIImage?)], NoError> in - var signals: [Signal<(Peer, UIImage?), NoError>] = [] - for peer in peers { - let peerAndAvatar = (peerAvatarImage(account: account, peer: peer, authorOfMessage: nil, representation: peer.smallProfileImage, round: false) ?? .single(nil)) - |> map { avatarImage in - return (peer, avatarImage) - } - signals.append(peerAndAvatar) - } - return combineLatest(signals) - } - |> deliverOnMainQueue).start(next: { peers in - for (peer, avatarImage) in peers { - guard let peer = peer as? TelegramUser, peer.botInfo == nil && !peer.flags.contains(.isSupport) else { - continue - } - let presentationData = sharedContext.currentPresentationData.with { $0 } - - let recipientHandle = INPersonHandle(value: "tg\(peer.id.id)", type: .unknown) - var nameComponents = PersonNameComponents() - nameComponents.givenName = peer.firstName - nameComponents.familyName = peer.lastName - - let displayTitle = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - let recipient = INPerson(personHandle: recipientHandle, nameComponents: nameComponents, displayName: displayTitle, image: nil, contactIdentifier: nil, customIdentifier: "tg\(peer.id.id)") - - let intent = INSendMessageIntent(recipients: [recipient], content: nil, speakableGroupName: INSpeakableString(spokenPhrase: displayTitle), conversationIdentifier: "tg\(peer.id.id)", serviceName: nil, sender: nil) - if let avatarImage = avatarImage, let avatarImageData = avatarImage.jpegData(compressionQuality: 0.8) { - intent.setImage(INImage(imageData: avatarImageData), forParameterNamed: \.groupName) - } - let interaction = INInteraction(intent: intent, response: nil) - interaction.direction = .outgoing - interaction.groupIdentifier = "sendMessage_\(account.peerId.toInt64())" - interaction.donate() - } - }) - } -} - -public func deleteAllSendMessageIntents(accountPeerId: PeerId) { - if #available(iOS 10.0, *) { - INInteraction.delete(with: "sendMessage_\(accountPeerId.toInt64())") - } -} diff --git a/submodules/AppLock/Sources/AppLock.swift b/submodules/AppLock/Sources/AppLock.swift index c8cdc58649..e339ed19ad 100644 --- a/submodules/AppLock/Sources/AppLock.swift +++ b/submodules/AppLock/Sources/AppLock.swift @@ -44,19 +44,19 @@ private func getCoveringViewSnaphot(window: Window1) -> UIImage? { context.clear(CGRect(origin: CGPoint(), size: size)) context.scaleBy(x: scale, y: scale) UIGraphicsPushContext(context) - window.forEachViewController { controller in + window.forEachViewController({ controller in if let controller = controller as? PasscodeEntryController { controller.displayNode.alpha = 0.0 } return true - } + }) window.hostView.containerView.drawHierarchy(in: CGRect(origin: CGPoint(), size: unscaledSize), afterScreenUpdates: false) - window.forEachViewController { controller in + window.forEachViewController({ controller in if let controller = controller as? PasscodeEntryController { controller.displayNode.alpha = 1.0 } return true - } + }) UIGraphicsPopContext() }).flatMap(applyScreenshotEffectToImage) } @@ -69,6 +69,7 @@ public final class AppLockContextImpl: AppLockContext { private let accountManager: AccountManager private let presentationDataSignal: Signal private let window: Window1? + private let rootController: UIViewController? private var coveringView: LockedWindowCoveringView? private var passcodeController: PasscodeEntryController? @@ -79,6 +80,7 @@ public final class AppLockContextImpl: AppLockContext { private let currentState = Promise() private let autolockTimeout = ValuePromise(nil, ignoreRepeated: true) + private let autolockReportTimeout = ValuePromise(nil, ignoreRepeated: true) private let isCurrentlyLockedPromise = Promise() public var isCurrentlyLocked: Signal { @@ -89,7 +91,7 @@ public final class AppLockContextImpl: AppLockContext { private var lastActiveTimestamp: Double? private var lastActiveValue: Bool = false - public init(rootPath: String, window: Window1?, applicationBindings: TelegramApplicationBindings, accountManager: AccountManager, presentationDataSignal: Signal, lockIconInitialFrame: @escaping () -> CGRect?) { + public init(rootPath: String, window: Window1?, rootController: UIViewController?, applicationBindings: TelegramApplicationBindings, accountManager: AccountManager, presentationDataSignal: Signal, lockIconInitialFrame: @escaping () -> CGRect?) { assert(Queue.mainQueue().isCurrent()) self.applicationBindings = applicationBindings @@ -97,6 +99,7 @@ public final class AppLockContextImpl: AppLockContext { self.presentationDataSignal = presentationDataSignal self.rootPath = rootPath self.window = window + self.rootController = rootController if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: self.rootPath))), let current = try? JSONDecoder().decode(LockState.self, from: data) { self.currentStateValue = current @@ -146,10 +149,24 @@ public final class AppLockContextImpl: AppLockContext { } strongSelf.autolockTimeout.set(nil) + strongSelf.autolockReportTimeout.set(nil) } else { if let autolockTimeout = passcodeSettings.autolockTimeout, !appInForeground { shouldDisplayCoveringView = true } + + if !appInForeground { + if let autolockTimeout = passcodeSettings.autolockTimeout { + strongSelf.autolockReportTimeout.set(autolockTimeout) + } else if state.isManuallyLocked { + strongSelf.autolockReportTimeout.set(1) + } else { + strongSelf.autolockReportTimeout.set(nil) + } + } else { + strongSelf.autolockReportTimeout.set(nil) + } + strongSelf.autolockTimeout.set(passcodeSettings.autolockTimeout) if isLocked(passcodeSettings: passcodeSettings, state: state, isApplicationActive: appInForeground) { @@ -184,7 +201,14 @@ public final class AppLockContextImpl: AppLockContext { } } passcodeController.presentedOverCoveringView = true + passcodeController.isOpaqueWhenInOverlay = true strongSelf.passcodeController = passcodeController + if let rootViewController = strongSelf.rootController { + if let presentedViewController = rootViewController.presentedViewController as? UIActivityViewController { + } else { + rootViewController.dismiss(animated: false, completion: nil) + } + } strongSelf.window?.present(passcodeController, on: .passcode) } } else if let passcodeController = strongSelf.passcodeController { @@ -202,6 +226,13 @@ public final class AppLockContextImpl: AppLockContext { coveringView.updateSnapshot(getCoveringViewSnaphot(window: window)) strongSelf.coveringView = coveringView window.coveringView = coveringView + + if let rootViewController = strongSelf.rootController { + if let presentedViewController = rootViewController.presentedViewController as? UIActivityViewController { + } else { + rootViewController.dismiss(animated: false, completion: nil) + } + } } } else { if let coveringView = strongSelf.coveringView { @@ -281,6 +312,18 @@ public final class AppLockContextImpl: AppLockContext { } } + public var autolockDeadline: Signal { + return self.autolockReportTimeout.get() + |> distinctUntilChanged + |> map { value -> Int32? in + if let value = value { + return Int32(Date().timeIntervalSince1970) + value + } else { + return nil + } + } + } + public func lock() { self.updateLockState { state in var state = state diff --git a/submodules/ArchivedStickerPacksNotice/BUCK b/submodules/ArchivedStickerPacksNotice/BUCK new file mode 100644 index 0000000000..d544ffbad2 --- /dev/null +++ b/submodules/ArchivedStickerPacksNotice/BUCK @@ -0,0 +1,29 @@ +load("//Config:buck_rule_macros.bzl", "static_library") + +static_library( + name = "ArchivedStickerPacksNotice", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared", + "//submodules/AsyncDisplayKit:AsyncDisplayKit#shared", + "//submodules/Display:Display#shared", + "//submodules/Postbox:Postbox#shared", + "//submodules/TelegramCore:TelegramCore#shared", + "//submodules/SyncCore:SyncCore#shared", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/StickerResources:StickerResources", + "//submodules/AlertUI:AlertUI", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/MergeLists:MergeLists", + "//submodules/ItemListUI:ItemListUI", + "//submodules/ItemListStickerPackItem:ItemListStickerPackItem", + ], + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/UIKit.framework", + ], +) diff --git a/submodules/ArchivedStickerPacksNotice/Sources/ArchivedStickerPacksNoticeController.swift b/submodules/ArchivedStickerPacksNotice/Sources/ArchivedStickerPacksNoticeController.swift new file mode 100644 index 0000000000..4cc7acdcb2 --- /dev/null +++ b/submodules/ArchivedStickerPacksNotice/Sources/ArchivedStickerPacksNoticeController.swift @@ -0,0 +1,316 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import ActivityIndicator +import AccountContext +import AlertUI +import PresentationDataUtils +import MergeLists +import ItemListUI +import ItemListStickerPackItem + +private struct ArchivedStickersNoticeEntry: Comparable, Identifiable { + let index: Int + let info: StickerPackCollectionInfo + let topItem: StickerPackItem? + let count: String + + var stableId: ItemCollectionId { + return info.id + } + + static func ==(lhs: ArchivedStickersNoticeEntry, rhs: ArchivedStickersNoticeEntry) -> Bool { + return lhs.index == rhs.index && lhs.info.id == rhs.info.id && lhs.count == rhs.count + } + + static func <(lhs: ArchivedStickersNoticeEntry, rhs: ArchivedStickersNoticeEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(account: Account, presentationData: PresentationData) -> ListViewItem { + return ItemListStickerPackItem(presentationData: ItemListPresentationData(presentationData), account: account, packInfo: info, itemCount: self.count, topItem: topItem, unread: false, control: .none, editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false, reorderable: false), enabled: true, playAnimatedStickers: true, sectionId: 0, action: { + }, setPackIdWithRevealedOptions: { current, previous in + }, addPack: { + }, removePack: { + }) + } +} + +private struct ArchivedStickersNoticeTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] +} + +private func preparedTransition(from fromEntries: [ArchivedStickersNoticeEntry], to toEntries: [ArchivedStickersNoticeEntry], account: Account, presentationData: PresentationData) -> ArchivedStickersNoticeTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData), directionHint: nil) } + + return ArchivedStickersNoticeTransition(deletions: deletions, insertions: insertions, updates: updates) +} + + +private final class ArchivedStickersNoticeAlertContentNode: AlertContentNode { + private let presentationData: PresentationData + private let archivedStickerPacks: [(StickerPackCollectionInfo, StickerPackItem?)] + + private let textNode: ASTextNode + private let listView: ListView + + private var enqueuedTransitions: [ArchivedStickersNoticeTransition] = [] + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private let disposable = MetaDisposable() + + private var validLayout: CGSize? + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init(theme: AlertControllerTheme, account: Account, presentationData: PresentationData, archivedStickerPacks: [(StickerPackCollectionInfo, StickerPackItem?)], actions: [TextAlertAction]) { + self.presentationData = presentationData + self.archivedStickerPacks = archivedStickerPacks + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 4 + + self.listView = ListView() + self.listView.isOpaque = false + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.textNode) + self.addSubnode(self.listView) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + self.actionNodes.last?.actionEnabled = false + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.updateTheme(theme) + + var index: Int = 0 + var entries: [ArchivedStickersNoticeEntry] = [] + for pack in archivedStickerPacks { + entries.append(ArchivedStickersNoticeEntry(index: index, info: pack.0, topItem: pack.1, count: presentationData.strings.StickerPack_StickerCount(pack.0.count))) + index += 1 + } + + let transition = preparedTransition(from: [], to: entries, account: account, presentationData: presentationData) + self.enqueueTransition(transition) + } + + deinit { + self.disposable.dispose() + } + + private func enqueueTransition(_ transition: ArchivedStickersNoticeTransition) { + self.enqueuedTransitions.append(transition) + + if let _ = self.validLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + guard let layout = self.validLayout, let transition = self.enqueuedTransitions.first else { + return + } + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + + self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + }) + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.ArchivedPacksAlert_Title, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 270.0) + let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) + + let hadValidLayout = self.validLayout != nil + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + + let textSize = self.textNode.measure(measureSize) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + origin.y += textSize.height + 16.0 + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.measure(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + var contentWidth = max(textSize.width, minActionsWidth) + contentWidth = max(contentWidth, 234.0) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultWidth = contentWidth + insets.left + insets.right + + let listHeight: CGFloat = CGFloat(min(3, self.archivedStickerPacks.count)) * 56.0 + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: resultWidth, height: listHeight), insets: UIEdgeInsets(top: -35.0, left: 0.0, bottom: 0.0, right: 0.0), headerInsets: UIEdgeInsets(), scrollIndicatorInsets: UIEdgeInsets(), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: listHeight)) + + let resultSize = CGSize(width: resultWidth, height: textSize.height + actionsHeight + listHeight + 10.0 + insets.top + insets.bottom) + + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + if !hadValidLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + + return resultSize + } +} + +public func archivedStickerPacksNoticeController(context: AccountContext, archivedStickerPacks: [(StickerPackCollectionInfo, StickerPackItem?)]) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var dismissImpl: (() -> Void)? + + let disposable = MetaDisposable() + + let contentNode = ArchivedStickersNoticeAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), account: context.account, presentationData: presentationData, archivedStickerPacks: archivedStickerPacks, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + dismissImpl?() + })]) + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + let presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in + controller?.theme = AlertControllerTheme(presentationData: presentationData) + }) + controller.dismissed = { + presentationDataDisposable.dispose() + disposable.dispose() + } + dismissImpl = { [weak controller, weak contentNode] in + controller?.dismissAnimated() + } + return controller +} diff --git a/submodules/AuthTransferUI/BUCK b/submodules/AuthTransferUI/BUCK new file mode 100644 index 0000000000..69bd9a6a32 --- /dev/null +++ b/submodules/AuthTransferUI/BUCK @@ -0,0 +1,33 @@ +load("//Config:buck_rule_macros.bzl", "static_library") + +static_library( + name = "AuthTransferUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/TelegramCore:TelegramCore#shared", + "//submodules/SyncCore:SyncCore#shared", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared", + "//submodules/AsyncDisplayKit:AsyncDisplayKit#shared", + "//submodules/Display:Display#shared", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/AccountContext:AccountContext", + "//submodules/QrCode:QrCode", + "//submodules/Camera:Camera", + "//submodules/GlassButtonNode:GlassButtonNode", + "//submodules/AlertUI:AlertUI", + "//submodules/AppBundle:AppBundle", + "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", + "//submodules/OverlayStatusController:OverlayStatusController", + "//submodules/AnimatedStickerNode:AnimatedStickerNode", + "//submodules/Markdown:Markdown", + "//submodules/AnimationUI:AnimationUI", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/DeviceAccess:DeviceAccess", + "//submodules/UndoUI:UndoUI", + ], + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + ], +) diff --git a/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift b/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift new file mode 100644 index 0000000000..384ffae882 --- /dev/null +++ b/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift @@ -0,0 +1,400 @@ +import Foundation +import UIKit +import AppBundle +import AsyncDisplayKit +import Display +import SolidRoundedButtonNode +import SwiftSignalKit +import OverlayStatusController +import AnimationUI +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import TelegramCore +import Markdown +import DeviceAccess + +private let colorKeyRegex = try? NSRegularExpression(pattern: "\"k\":\\[[\\d\\.]+\\,[\\d\\.]+\\,[\\d\\.]+\\,[\\d\\.]+\\]") + +private func transformedWithTheme(data: Data, theme: PresentationTheme) -> Data { + if var string = String(data: data, encoding: .utf8) { + var colors: [UIColor] = [0x333333, 0xFFFFFF, 0x50A7EA, 0x212121].map { UIColor(rgb: $0) } + let replacementColors: [UIColor] = [theme.list.itemPrimaryTextColor.mixedWith(.white, alpha: 0.2), theme.list.plainBackgroundColor, theme.list.itemAccentColor, theme.list.plainBackgroundColor] + + func colorToString(_ color: UIColor) -> String { + var r: CGFloat = 0.0 + var g: CGFloat = 0.0 + var b: CGFloat = 0.0 + if color.getRed(&r, green: &g, blue: &b, alpha: nil) { + return "\"k\":[\(r),\(g),\(b),1]" + } + return "" + } + + func match(_ a: Double, _ b: Double, eps: Double) -> Bool { + return abs(a - b) < eps + } + + var replacements: [(NSTextCheckingResult, String)] = [] + + if let colorKeyRegex = colorKeyRegex { + let results = colorKeyRegex.matches(in: string, range: NSRange(string.startIndex..., in: string)) + for result in results.reversed() { + if let range = Range(result.range, in: string) { + let substring = String(string[range]) + let color = substring[substring.index(string.startIndex, offsetBy: "\"k\":[".count) ..< substring.index(before: substring.endIndex)] + let components = color.split(separator: ",") + if components.count == 4, let r = Double(components[0]), let g = Double(components[1]), let b = Double(components[2]), let a = Double(components[3]) { + if match(a, 1.0, eps: 0.01) { + for i in 0 ..< colors.count { + let color = colors[i] + var cr: CGFloat = 0.0 + var cg: CGFloat = 0.0 + var cb: CGFloat = 0.0 + if color.getRed(&cr, green: &cg, blue: &cb, alpha: nil) { + if match(r, Double(cr), eps: 0.01) && match(g, Double(cg), eps: 0.01) && match(b, Double(cb), eps: 0.01) { + replacements.append((result, colorToString(replacementColors[i]))) + } + } + } + } + } + } + } + } + + for (result, text) in replacements { + if let range = Range(result.range, in: string) { + string = string.replacingCharacters(in: range, with: text) + } + } + + return string.data(using: .utf8) ?? data + } else { + return data + } +} + +public final class AuthDataTransferSplashScreen: ViewController { + private let context: AccountContext + private let activeSessionsContext: ActiveSessionsContext + private var presentationData: PresentationData + + public init(context: AccountContext, activeSessionsContext: ActiveSessionsContext) { + self.context = context + self.activeSessionsContext = activeSessionsContext + + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let defaultTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme) + let navigationBarTheme = NavigationBarTheme(buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor) + + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close))) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + self.navigationPresentation = .modalInLargeLayout + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + self.navigationBar?.intrinsicCanTransitionInline = false + + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func loadDisplayNode() { + self.displayNode = AuthDataTransferSplashScreenNode(context: self.context, presentationData: self.presentationData, action: { [weak self] in + guard let strongSelf = self else { + return + } + + DeviceAccess.authorizeAccess(to: .camera, presentationData: strongSelf.presentationData, present: { c, a in + guard let strongSelf = self else { + return + } + c.presentationArguments = a + strongSelf.context.sharedContext.mainWindow?.present(c, on: .root) + }, openSettings: { + self?.context.sharedContext.applicationBindings.openSettings() + }, { granted in + guard let strongSelf = self else { + return + } + guard granted else { + return + } + (strongSelf.navigationController as? NavigationController)?.replaceController(strongSelf, with: AuthTransferScanScreen(context: strongSelf.context, activeSessionsContext: strongSelf.activeSessionsContext), animated: true) + }) + }) + + self.displayNodeDidLoad() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + (self.displayNode as! AuthDataTransferSplashScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition) + } +} + +private final class AuthDataTransferSplashScreenNode: ViewControllerTracingNode { + private var presentationData: PresentationData + + private var animationSize: CGSize = CGSize() + private var animationOffset: CGPoint = CGPoint() + private let animationNode: AnimationNode? + private let titleNode: ImmediateTextNode + private let badgeBackgroundNodes: [ASImageNode] + private let badgeTextNodes: [ImmediateTextNode] + private let textNodes: [ImmediateTextNode] + let buttonNode: SolidRoundedButtonNode + + private let hierarchyTrackingNode: HierarchyTrackingNode + + var inProgress: Bool = false { + didSet { + self.buttonNode.isUserInteractionEnabled = !self.inProgress + self.buttonNode.alpha = self.inProgress ? 0.6 : 1.0 + } + } + + private var validLayout: ContainerViewLayout? + + init(context: AccountContext, presentationData: PresentationData, action: @escaping () -> Void) { + self.presentationData = presentationData + + if let url = getAppBundle().url(forResource: "anim_qr", withExtension: "json"), let data = try? Data(contentsOf: url) { + self.animationNode = AnimationNode(animationData: transformedWithTheme(data: data, theme: presentationData.theme)) + } else { + self.animationNode = nil + } + + let buttonText: String + + let badgeFont = Font.with(size: 13.0, design: .round, traits: [.bold]) + let textFont = Font.regular(16.0) + let textColor = self.presentationData.theme.list.itemPrimaryTextColor + + var badgeBackgroundNodes: [ASImageNode] = [] + var badgeTextNodes: [ImmediateTextNode] = [] + var textNodes: [ImmediateTextNode] = [] + + let badgeBackground = generateFilledCircleImage(diameter: 20.0, color: self.presentationData.theme.list.itemCheckColors.fillColor) + + for i in 0 ..< 3 { + let badgeBackgroundNode = ASImageNode() + badgeBackgroundNode.displaysAsynchronously = false + badgeBackgroundNode.displayWithoutProcessing = true + badgeBackgroundNode.image = badgeBackground + badgeBackgroundNodes.append(badgeBackgroundNode) + + let badgeTextNode = ImmediateTextNode() + badgeTextNode.displaysAsynchronously = false + badgeTextNode.attributedText = NSAttributedString(string: "\(i + 1)", font: badgeFont, textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor) + badgeTextNode.maximumNumberOfLines = 0 + badgeTextNode.lineSpacing = 0.1 + badgeTextNodes.append(badgeTextNode) + + let string: String + switch i { + case 0: + string = self.presentationData.strings.AuthSessions_AddDeviceIntro_Text1 + case 1: + string = self.presentationData.strings.AuthSessions_AddDeviceIntro_Text2 + default: + string = self.presentationData.strings.AuthSessions_AddDeviceIntro_Text3 + } + + let body = MarkdownAttributeSet(font: textFont, textColor: textColor) + let link = MarkdownAttributeSet(font: textFont, textColor: self.presentationData.theme.list.itemAccentColor, additionalAttributes: ["URL": true as NSNumber]) + + let text = parseMarkdownIntoAttributedString(string, attributes: MarkdownAttributes(body: body, bold: body, link: link, linkAttribute: { _ in + return nil + })) + + let textNode = ImmediateTextNode() + textNode.displaysAsynchronously = false + textNode.attributedText = text + textNode.maximumNumberOfLines = 0 + textNode.lineSpacing = 0.1 + textNodes.append(textNode) + } + + self.badgeBackgroundNodes = badgeBackgroundNodes + self.badgeTextNodes = badgeTextNodes + self.textNodes = textNodes + + buttonText = self.presentationData.strings.AuthSessions_AddDeviceIntro_Action + + self.titleNode = ImmediateTextNode() + self.titleNode.displaysAsynchronously = false + self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_AddDeviceIntro_Title, font: Font.bold(24.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) + self.titleNode.maximumNumberOfLines = 0 + self.titleNode.textAlignment = .center + + self.buttonNode = SolidRoundedButtonNode(title: buttonText, theme: SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor), height: 50.0, cornerRadius: 10.0, gloss: false) + self.buttonNode.isHidden = buttonText.isEmpty + + var updateInHierarchy: ((Bool) -> Void)? + self.hierarchyTrackingNode = HierarchyTrackingNode({ value in + updateInHierarchy?(value) + }) + + super.init() + + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + self.addSubnode(self.hierarchyTrackingNode) + + if let animationNode = self.animationNode { + self.addSubnode(animationNode) + } + self.addSubnode(self.titleNode) + + self.badgeBackgroundNodes.forEach(self.addSubnode) + self.badgeTextNodes.forEach(self.addSubnode) + self.textNodes.forEach(self.addSubnode) + + self.addSubnode(self.buttonNode) + + self.buttonNode.pressed = { + action() + } + + for textNode in self.textNodes { + textNode.linkHighlightColor = self.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.5) + textNode.highlightAttributeAction = { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + } + textNode.tapAttributeAction = { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://desktop.telegram.org", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) + } + } + } + + updateInHierarchy = { [weak self] value in + if value { + self?.animationNode?.play() + } else { + self?.animationNode?.reset() + } + } + } + + override func didLoad() { + super.didLoad() + } + + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let firstTime = self.validLayout == nil + self.validLayout = layout + + let sideInset: CGFloat = 22.0 + let textSideInset: CGFloat = 54.0 + let buttonSideInset: CGFloat = 16.0 + let titleSpacing: CGFloat = 25.0 + let buttonHeight: CGFloat = 50.0 + let buttonSpacing: CGFloat = 16.0 + let textSpacing: CGFloat = 25.0 + let badgeSize: CGFloat = 20.0 + + let animationFitSize = CGSize(width: min(500.0, layout.size.width - sideInset + 20.0), height: 500.0) + let animationSize = self.animationNode?.preferredSize()?.fitted(animationFitSize) ?? animationFitSize + let iconSize: CGSize = animationSize + var iconOffset = CGPoint() + + let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height)) + + var badgeTextSizes: [CGSize] = [] + var textSizes: [CGSize] = [] + var textContentHeight: CGFloat = 0.0 + for i in 0 ..< self.badgeTextNodes.count { + let badgeTextSize = self.badgeTextNodes[i].updateLayout(CGSize(width: 100.0, height: .greatestFiniteMagnitude)) + badgeTextSizes.append(badgeTextSize) + let textSize = self.textNodes[i].updateLayout(CGSize(width: layout.size.width - sideInset * 2.0 - 40.0, height: .greatestFiniteMagnitude)) + textSizes.append(textSize) + + if i != 0 { + textContentHeight += textSpacing + } + textContentHeight += textSize.height + } + + var contentHeight = iconSize.height + titleSize.height + titleSpacing + textContentHeight + + let bottomInset = layout.intrinsicInsets.bottom + 20.0 + let contentTopInset = navigationHeight + let contentBottomInset = bottomInset + buttonHeight + buttonSpacing + + let iconSpacing: CGFloat = max(20.0, min(61.0, layout.size.height - contentTopInset - contentBottomInset - contentHeight - 40.0)) + + contentHeight += iconSpacing + + var contentVerticalOrigin = contentTopInset + floor((layout.size.height - contentTopInset - contentBottomInset - contentHeight) / 2.0) + + let buttonWidth = layout.size.width - buttonSideInset * 2.0 + + let buttonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonWidth) / 2.0), y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: buttonWidth, height: buttonHeight)) + transition.updateFrame(node: self.buttonNode, frame: buttonFrame) + self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition) + + var maxContentVerticalOrigin = buttonFrame.minY - 12.0 - contentHeight + + contentVerticalOrigin = min(contentVerticalOrigin, maxContentVerticalOrigin) + + var contentY = contentVerticalOrigin + let iconFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0) + self.animationOffset.x, y: contentY), size: iconSize).offsetBy(dx: iconOffset.x, dy: iconOffset.y) + contentY += iconSize.height + iconSpacing + if let animationNode = self.animationNode { + transition.updateFrameAdditive(node: animationNode, frame: iconFrame) + if iconFrame.minY < 0.0 { + transition.updateAlpha(node: animationNode, alpha: 0.0) + } else { + transition.updateAlpha(node: animationNode, alpha: 1.0) + } + } + + let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: contentY), size: titleSize) + transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame) + contentY += titleSize.height + titleSpacing + + for i in 0 ..< self.badgeTextNodes.count { + if i != 0 { + contentY += textSpacing + } + + let badgeTextSize = badgeTextSizes[i] + let textSize = textSizes[i] + + let textFrame = CGRect(origin: CGPoint(x: textSideInset, y: contentY), size: textSize) + transition.updateFrameAdditive(node: self.textNodes[i], frame: textFrame) + + let badgeFrame = CGRect(origin: CGPoint(x: sideInset, y: textFrame.minY), size: CGSize(width: badgeSize, height: badgeSize)) + transition.updateFrameAdditive(node: self.badgeBackgroundNodes[i], frame: badgeFrame) + + let badgeTextOffsetX: CGFloat + if i == 0 { + badgeTextOffsetX = 0.5 + } else { + badgeTextOffsetX = 1.0 + } + + transition.updateFrameAdditive(node: self.badgeTextNodes[i], frame: CGRect(origin: CGPoint(x: badgeFrame.minX + floor((badgeFrame.width - badgeTextSize.width) / 2.0) + badgeTextOffsetX, y: badgeFrame.minY + floor((badgeFrame.height - badgeTextSize.height) / 2.0) + 0.5), size: badgeTextSize)) + + contentY += textSize.height + } + + if firstTime { + self.animationNode?.play() + } + } +} diff --git a/submodules/AuthTransferUI/Sources/AuthTransferScanScreen.swift b/submodules/AuthTransferUI/Sources/AuthTransferScanScreen.swift new file mode 100644 index 0000000000..ed33f1b9a9 --- /dev/null +++ b/submodules/AuthTransferUI/Sources/AuthTransferScanScreen.swift @@ -0,0 +1,467 @@ +import Foundation +import UIKit +import AccountContext +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Camera +import GlassButtonNode +import CoreImage +import AlertUI +import TelegramPresentationData +import TelegramCore +import UndoUI + +private func parseAuthTransferUrl(_ url: URL) -> Data? { + var tokenString: String? + if let query = url.query, let components = URLComponents(string: "/?" + query), let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "token", !value.isEmpty { + tokenString = value + } + } + } + } + if var tokenString = tokenString { + tokenString = tokenString.replacingOccurrences(of: "-", with: "+") + tokenString = tokenString.replacingOccurrences(of: "_", with: "/") + while tokenString.count % 4 != 0 { + tokenString.append("=") + } + if let data = Data(base64Encoded: tokenString) { + return data + } + } + return nil +} + +private func generateFrameImage() -> UIImage? { + return generateImage(CGSize(width: 64.0, height: 64.0), contextGenerator: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + context.setStrokeColor(UIColor.white.cgColor) + context.setLineWidth(4.0) + context.setLineCap(.round) + + var path = CGMutablePath(); + path.move(to: CGPoint(x: 2.0, y: 2.0 + 26.0)) + path.addArc(tangent1End: CGPoint(x: 2.0, y: 2.0), tangent2End: CGPoint(x: 2.0 + 26.0, y: 2.0), radius: 6.0) + path.addLine(to: CGPoint(x: 2.0 + 26.0, y: 2.0)) + context.addPath(path) + context.strokePath() + + path.move(to: CGPoint(x: size.width - 2.0, y: 2.0 + 26.0)) + path.addArc(tangent1End: CGPoint(x: size.width - 2.0, y: 2.0), tangent2End: CGPoint(x: 2.0 + 26.0, y: 2.0), radius: 6.0) + path.addLine(to: CGPoint(x: size.width - 2.0 - 26.0, y: 2.0)) + context.addPath(path) + context.strokePath() + + path.move(to: CGPoint(x: 2.0, y: size.height - 2.0 - 26.0)) + path.addArc(tangent1End: CGPoint(x: 2.0, y: size.height - 2.0), tangent2End: CGPoint(x: 2.0 + 26.0, y: size.height - 2.0), radius: 6.0) + path.addLine(to: CGPoint(x: 2.0 + 26.0, y: size.height - 2.0)) + context.addPath(path) + context.strokePath() + + path.move(to: CGPoint(x: size.width - 2.0, y: size.height - 2.0 - 26.0)) + path.addArc(tangent1End: CGPoint(x: size.width - 2.0, y: size.height - 2.0), tangent2End: CGPoint(x: 2.0 + 26.0, y: size.height - 2.0), radius: 6.0) + path.addLine(to: CGPoint(x: size.width - 2.0 - 26.0, y: size.height - 2.0)) + context.addPath(path) + context.strokePath() + })?.stretchableImage(withLeftCapWidth: 32, topCapHeight: 32) +} + +public final class AuthTransferScanScreen: ViewController { + private let context: AccountContext + private let activeSessionsContext: ActiveSessionsContext + private var presentationData: PresentationData + + private var codeDisposable: Disposable? + private var inForegroundDisposable: Disposable? + private let approveDisposable = MetaDisposable() + + private var controllerNode: AuthTransferScanScreenNode { + return self.displayNode as! AuthTransferScanScreenNode + } + + public init(context: AccountContext, activeSessionsContext: ActiveSessionsContext) { + self.context = context + self.activeSessionsContext = activeSessionsContext + + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let navigationBarTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: .white, primaryTextColor: .white, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) + + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close))) + + self.statusBar.statusBarStyle = .White + + self.navigationPresentation = .modalInLargeLayout + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + self.navigationBar?.intrinsicCanTransitionInline = false + + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + self.inForegroundDisposable = (context.sharedContext.applicationBindings.applicationInForeground + |> deliverOnMainQueue).start(next: { [weak self] inForeground in + guard let strongSelf = self else { + return + } + (strongSelf.displayNode as! AuthTransferScanScreenNode).updateInForeground(inForeground) + }) + + #if DEBUG + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Test", style: .plain, target: self, action: #selector(self.testPressed)) + #endif + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.codeDisposable?.dispose() + self.inForegroundDisposable?.dispose() + self.approveDisposable.dispose() + } + + @objc private func testPressed() { + self.dismissWithSuccess(session: nil) + } + + private func dismissWithSuccess(session: RecentAccountSession?) { + if let navigationController = navigationController as? NavigationController { + let activeSessionsContext = self.activeSessionsContext + + self.present(UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: self.presentationData.strings.AuthSessions_AddedDeviceTitle, text: session?.appName ?? "Telegram for macOS", cancel: self.presentationData.strings.AuthSessions_AddedDeviceTerminate), elevatedLayout: false, animateInAsReplacement: false, action: { value in + if value == .undo, let session = session { + let _ = activeSessionsContext.remove(hash: session.hash).start() + return true + } else { + return false + } + }), in: .window(.root)) + + var viewControllers = navigationController.viewControllers + viewControllers = viewControllers.filter { controller in + if controller is RecentSessionsController { + return false + } + if controller === self { + return false + } + return true + } + viewControllers.append(self.context.sharedContext.makeRecentSessionsController(context: self.context, activeSessionsContext: activeSessionsContext)) + navigationController.setViewControllers(viewControllers, animated: true) + } else { + self.dismiss() + } + } + + override public func loadDisplayNode() { + self.displayNode = AuthTransferScanScreenNode(presentationData: self.presentationData) + + self.displayNodeDidLoad() + + self.codeDisposable = ((self.displayNode as! AuthTransferScanScreenNode).focusedCode.get() + |> map { code -> String? in + return code?.message + } + |> distinctUntilChanged + |> mapToSignal { code -> Signal in + return .single(code) + |> delay(0.5, queue: Queue.mainQueue()) + }).start(next: { [weak self] code in + guard let strongSelf = self else { + return + } + guard let code = code else { + return + } + if let url = URL(string: code), let parsedToken = parseAuthTransferUrl(url) { + strongSelf.approveDisposable.set((approveAuthTransferToken(account: strongSelf.context.account, token: parsedToken, activeSessionsContext: strongSelf.activeSessionsContext) + |> deliverOnMainQueue).start(next: { session in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.codeWithError = nil + let activeSessionsContext = strongSelf.activeSessionsContext + Queue.mainQueue().after(1.5, { + activeSessionsContext.loadMore() + }) + strongSelf.dismissWithSuccess(session: session) + }, error: { _ in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.codeWithError = code + strongSelf.controllerNode.updateFocusedRect(nil) + })) + } + }) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + (self.displayNode as! AuthTransferScanScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition) + } +} + +private final class AuthTransferScanScreenNode: ViewControllerTracingNode, UIScrollViewDelegate { + private var presentationData: PresentationData + + private let previewNode: CameraPreviewNode + private let fadeNode: ASDisplayNode + private let topDimNode: ASDisplayNode + private let bottomDimNode: ASDisplayNode + private let leftDimNode: ASDisplayNode + private let rightDimNode: ASDisplayNode + private let centerDimNode: ASDisplayNode + private let frameNode: ASImageNode + private let torchButtonNode: GlassButtonNode + private let titleNode: ImmediateTextNode + private let textNode: ImmediateTextNode + private let errorTextNode: ImmediateTextNode + + private let camera: Camera + private let codeDisposable = MetaDisposable() + + fileprivate let focusedCode = ValuePromise(ignoreRepeated: true) + private var focusedRect: CGRect? + + private var validLayout: (ContainerViewLayout, CGFloat)? + + var codeWithError: String? { + didSet { + if self.codeWithError != oldValue { + if self.codeWithError != nil { + self.errorTextNode.isHidden = false + } else { + self.errorTextNode.isHidden = true + } + } + } + } + + init(presentationData: PresentationData) { + self.presentationData = presentationData + + self.previewNode = CameraPreviewNode() + self.previewNode.backgroundColor = .black + + self.fadeNode = ASDisplayNode() + self.fadeNode.alpha = 0.0 + self.fadeNode.backgroundColor = .black + + self.topDimNode = ASDisplayNode() + self.topDimNode.alpha = 0.625 + self.topDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8) + + self.bottomDimNode = ASDisplayNode() + self.bottomDimNode.alpha = 0.625 + self.bottomDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8) + + self.leftDimNode = ASDisplayNode() + self.leftDimNode.alpha = 0.625 + self.leftDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8) + + self.rightDimNode = ASDisplayNode() + self.rightDimNode.alpha = 0.625 + self.rightDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8) + + self.centerDimNode = ASDisplayNode() + self.centerDimNode.alpha = 0.0 + self.centerDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8) + + self.frameNode = ASImageNode() + self.frameNode.image = generateFrameImage() + + self.torchButtonNode = GlassButtonNode(icon: UIImage(bundleImageName: "Wallet/CameraFlashIcon")!, label: nil) + + self.titleNode = ImmediateTextNode() + self.titleNode.displaysAsynchronously = false + self.titleNode.attributedText = NSAttributedString(string: presentationData.strings.AuthSessions_AddDevice_ScanTitle, font: Font.bold(32.0), textColor: .white) + self.titleNode.maximumNumberOfLines = 0 + self.titleNode.textAlignment = .center + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.attributedText = NSAttributedString(string: presentationData.strings.AuthSessions_AddDevice_ScanInfo, font: Font.regular(16.0), textColor: .white) + self.textNode.maximumNumberOfLines = 0 + self.textNode.textAlignment = .center + + self.errorTextNode = ImmediateTextNode() + self.errorTextNode.displaysAsynchronously = false + self.errorTextNode.attributedText = NSAttributedString(string: presentationData.strings.AuthSessions_AddDevice_InvalidQRCode, font: Font.medium(16.0), textColor: .white) + self.errorTextNode.maximumNumberOfLines = 0 + self.errorTextNode.textAlignment = .center + self.errorTextNode.isHidden = true + + self.camera = Camera(configuration: .init(preset: .hd1920x1080, position: .back, audio: false)) + + super.init() + + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + self.addSubnode(self.previewNode) + self.addSubnode(self.fadeNode) + self.addSubnode(self.topDimNode) + self.addSubnode(self.bottomDimNode) + self.addSubnode(self.leftDimNode) + self.addSubnode(self.rightDimNode) + self.addSubnode(self.centerDimNode) + self.addSubnode(self.frameNode) + self.addSubnode(self.torchButtonNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + self.addSubnode(self.errorTextNode) + + self.torchButtonNode.addTarget(self, action: #selector(self.torchPressed), forControlEvents: .touchUpInside) + } + + deinit { + self.codeDisposable.dispose() + self.camera.stopCapture(invalidate: true) + } + + fileprivate func updateInForeground(_ inForeground: Bool) { + if !inForeground { + self.camera.stopCapture(invalidate: false) + } else { + self.camera.startCapture() + } + } + + override func didLoad() { + super.didLoad() + + self.camera.attachPreviewNode(self.previewNode) + self.camera.startCapture() + + let throttledSignal = self.camera.detectedCodes + |> mapToThrottled { next -> Signal<[CameraCode], NoError> in + return .single(next) |> then(.complete() |> delay(0.3, queue: Queue.concurrentDefaultQueue())) + } + + self.codeDisposable.set((throttledSignal + |> deliverOnMainQueue).start(next: { [weak self] codes in + guard let strongSelf = self else { + return + } + let filteredCodes = codes.filter { $0.message.hasPrefix("tg://") } + if let code = filteredCodes.first, CGRect(x: 0.3, y: 0.3, width: 0.4, height: 0.4).contains(code.boundingBox.center) { + if strongSelf.codeWithError != code.message { + strongSelf.codeWithError = nil + } + if strongSelf.codeWithError == code.message { + strongSelf.focusedCode.set(nil) + strongSelf.updateFocusedRect(nil) + } else { + strongSelf.focusedCode.set(code) + strongSelf.updateFocusedRect(code.boundingBox) + } + } else { + strongSelf.codeWithError = nil + strongSelf.focusedCode.set(nil) + strongSelf.updateFocusedRect(nil) + } + })) + } + + func updateFocusedRect(_ rect: CGRect?) { + self.focusedRect = rect + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) + } + } + + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.validLayout = (layout, navigationHeight) + + let sideInset: CGFloat = 66.0 + let titleSpacing: CGFloat = 48.0 + let bounds = CGRect(origin: CGPoint(), size: layout.size) + + if case .tablet = layout.deviceMetrics.type { + if UIDevice.current.orientation == .landscapeLeft { + self.previewNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + } else if UIDevice.current.orientation == .landscapeRight { + self.previewNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + } else { + self.previewNode.transform = CATransform3DIdentity + } + } + transition.updateFrame(node: self.previewNode, frame: bounds) + transition.updateFrame(node: self.fadeNode, frame: bounds) + + let frameSide = max(240.0, layout.size.width - sideInset * 2.0) + let dimHeight = ceil((layout.size.height - frameSide) / 2.0) + let dimInset = (layout.size.width - frameSide) / 2.0 + + let dimAlpha: CGFloat + let dimRect: CGRect + let controlsAlpha: CGFloat + var centerDimAlpha: CGFloat = 0.0 + var frameAlpha: CGFloat = 1.0 + if let focusedRect = self.focusedRect { + controlsAlpha = 0.0 + dimAlpha = 1.0 + let side = max(bounds.width * focusedRect.width, bounds.height * focusedRect.height) * 0.6 + let center = CGPoint(x: (1.0 - focusedRect.center.y) * bounds.width, y: focusedRect.center.x * bounds.height) + dimRect = CGRect(x: center.x - side / 2.0, y: center.y - side / 2.0, width: side, height: side) + } else { + controlsAlpha = 1.0 + dimAlpha = 0.625 + dimRect = CGRect(x: dimInset, y: dimHeight, width: layout.size.width - dimInset * 2.0, height: layout.size.height - dimHeight * 2.0) + } + + transition.updateAlpha(node: self.topDimNode, alpha: dimAlpha) + transition.updateAlpha(node: self.bottomDimNode, alpha: dimAlpha) + transition.updateAlpha(node: self.leftDimNode, alpha: dimAlpha) + transition.updateAlpha(node: self.rightDimNode, alpha: dimAlpha) + transition.updateAlpha(node: self.centerDimNode, alpha: centerDimAlpha) + transition.updateAlpha(node: self.frameNode, alpha: frameAlpha) + + transition.updateFrame(node: self.topDimNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: dimRect.minY)) + transition.updateFrame(node: self.bottomDimNode, frame: CGRect(x: 0.0, y: dimRect.maxY, width: layout.size.width, height: max(0.0, layout.size.height - dimRect.maxY))) + transition.updateFrame(node: self.leftDimNode, frame: CGRect(x: 0.0, y: dimRect.minY, width: max(0.0, dimRect.minX), height: dimRect.height)) + transition.updateFrame(node: self.rightDimNode, frame: CGRect(x: dimRect.maxX, y: dimRect.minY, width: max(0.0, layout.size.width - dimRect.maxX), height: dimRect.height)) + transition.updateFrame(node: self.frameNode, frame: dimRect.insetBy(dx: -2.0, dy: -2.0)) + transition.updateFrame(node: self.centerDimNode, frame: dimRect) + + let buttonSize = CGSize(width: 72.0, height: 72.0) + var torchFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonSize.width) / 2.0), y: dimHeight + frameSide + 98.0), size: buttonSize) + let updatedTorchY = min(torchFrame.minY, layout.size.height - torchFrame.height - 10.0) + let additionalTorchOffset: CGFloat = updatedTorchY - torchFrame.minY + torchFrame.origin.y = updatedTorchY + transition.updateFrame(node: self.torchButtonNode, frame: torchFrame) + + transition.updateAlpha(node: self.textNode, alpha: controlsAlpha) + transition.updateAlpha(node: self.errorTextNode, alpha: controlsAlpha) + transition.updateAlpha(node: self.torchButtonNode, alpha: controlsAlpha) + + let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - 16.0, height: layout.size.height)) + let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - 16.0, height: layout.size.height)) + let errorTextSize = self.errorTextNode.updateLayout(CGSize(width: layout.size.width - 16.0, height: layout.size.height)) + let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: dimHeight - textSize.height - titleSpacing), size: textSize) + let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: textFrame.minY - 18.0 - titleSize.height), size: titleSize) + var errorTextFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - errorTextSize.width) / 2.0), y: dimHeight + frameSide + 48.0), size: errorTextSize) + errorTextFrame.origin.y += floor(additionalTorchOffset / 2.0) + if titleFrame.minY < navigationHeight { + transition.updateAlpha(node: self.titleNode, alpha: 0.0) + } else { + transition.updateAlpha(node: self.titleNode, alpha: controlsAlpha) + } + transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame) + transition.updateFrameAdditive(node: self.textNode, frame: textFrame) + transition.updateFrameAdditive(node: self.errorTextNode, frame: errorTextFrame) + } + + @objc private func torchPressed() { + self.torchButtonNode.isSelected = !self.torchButtonNode.isSelected + self.camera.setTorchActive(self.torchButtonNode.isSelected) + } +} + diff --git a/submodules/AvatarNode/BUCK b/submodules/AvatarNode/BUCK index e3c4a0f5a1..6e813eee49 100644 --- a/submodules/AvatarNode/BUCK +++ b/submodules/AvatarNode/BUCK @@ -13,6 +13,7 @@ static_library( "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/AnimationUI:AnimationUI", "//submodules/AppBundle:AppBundle", + "//submodules/AccountContext:AccountContext", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 0aed8109da..2f4e502e6a 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -9,6 +9,7 @@ import SwiftSignalKit import TelegramPresentationData import AnimationUI import AppBundle +import AccountContext private let deletedIcon = UIImage(bundleImageName: "Avatar/DeletedIcon")?.precomposed() private let savedMessagesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/SavedMessagesIcon"), color: .white) @@ -53,16 +54,6 @@ private class AvatarNodeParameters: NSObject { } } -private let gradientColors: [NSArray] = [ - [UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor], - [UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor], - [UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor], - [UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor], - [UIColor(rgb: 0x4acccd).cgColor, UIColor(rgb: 0x00fcfd).cgColor], - [UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor], - [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor], -] - private func generateGradientFilledCircleImage(diameter: CGFloat, colors: NSArray) -> UIImage? { return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) @@ -159,13 +150,29 @@ public final class AvatarEditOverlayNode: ASDisplayNode { context.setBlendMode(.normal) - if let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIcon"), color: .white) { - context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0), y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0)), size: editAvatarIcon.size)) + if bounds.width > 90.0 { + if let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIconLarge"), color: .white) { + context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0) + 0.5, y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0) + 1.0), size: editAvatarIcon.size)) + } + } else { + if let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIcon"), color: .white) { + context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0) + 0.5, y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0) + 1.0), size: editAvatarIcon.size)) + } } } } public final class AvatarNode: ASDisplayNode { + public static let gradientColors: [NSArray] = [ + [UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor], + [UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor], + [UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor], + [UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor], + [UIColor(rgb: 0x4acccd).cgColor, UIColor(rgb: 0x00fcfd).cgColor], + [UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor], + [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor], + ] + public var font: UIFont { didSet { if oldValue !== font { @@ -190,6 +197,8 @@ public final class AvatarNode: ASDisplayNode { private let imageReadyDisposable = MetaDisposable() private var state: AvatarNodeState = .empty + public var unroundedImage: UIImage? + private let imageReady = Promise(false) public var ready: Signal { let imageReady = self.imageReady @@ -282,7 +291,7 @@ public final class AvatarNode: ASDisplayNode { self.imageNode.isHidden = true } - public func setPeer(account: Account, theme: PresentationTheme, peer: Peer?, authorOfMessage: MessageReference? = nil, overrideImage: AvatarNodeImageOverride? = nil, emptyColor: UIColor? = nil, clipStyle: AvatarNodeClipStyle = .round, synchronousLoad: Bool = false) { + public func setPeer(context: AccountContext, theme: PresentationTheme, peer: Peer?, authorOfMessage: MessageReference? = nil, overrideImage: AvatarNodeImageOverride? = nil, emptyColor: UIColor? = nil, clipStyle: AvatarNodeClipStyle = .round, synchronousLoad: Bool = false, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), storeUnrounded: Bool = false) { var synchronousLoad = synchronousLoad var representation: TelegramMediaImageRepresentation? var icon = AvatarNodeIcon.none @@ -306,7 +315,7 @@ public final class AvatarNode: ASDisplayNode { representation = nil icon = .deletedIcon } - } else if peer?.restrictionText(platform: "ios") == nil { + } else if peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) == nil { representation = peer?.smallProfileImage } let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? PeerId(namespace: 0, id: 0), peer?.displayLetters ?? [], representation) @@ -317,11 +326,18 @@ public final class AvatarNode: ASDisplayNode { let parameters: AvatarNodeParameters - if let peer = peer, let signal = peerAvatarImage(account: account, peer: peer, authorOfMessage: authorOfMessage, representation: representation, emptyColor: emptyColor, synchronousLoad: synchronousLoad) { + if let peer = peer, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, emptyColor: emptyColor, synchronousLoad: synchronousLoad, provideUnrounded: storeUnrounded) { self.contents = nil self.displaySuspended = true self.imageReady.set(self.imageNode.ready) - self.imageNode.setSignal(signal) + self.imageNode.setSignal(signal |> beforeNext { [weak self] next in + Queue.mainQueue().async { + self?.unroundedImage = next?.1 + } + } + |> map { next -> UIImage? in + return next?.0 + }) if case .editAvatarIcon = icon { if self.editOverlayNode == nil { @@ -336,7 +352,7 @@ public final class AvatarNode: ASDisplayNode { self.editOverlayNode?.isHidden = true } - parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) + parameters = AvatarNodeParameters(theme: theme, accountPeerId: context.account.peerId, peerId: peer.id, letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle) } else { self.imageReady.set(.single(true)) self.displaySuspended = false @@ -345,7 +361,7 @@ public final class AvatarNode: ASDisplayNode { } self.editOverlayNode?.isHidden = true - parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer?.id ?? PeerId(namespace: 0, id: 0), letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle) + parameters = AvatarNodeParameters(theme: theme, accountPeerId: context.account.peerId, peerId: peer?.id ?? PeerId(namespace: 0, id: 0), letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle) } if self.parameters == nil || self.parameters != parameters { self.parameters = parameters @@ -415,7 +431,7 @@ public final class AvatarNode: ASDisplayNode { if peerId.namespace == -1 { colorIndex = -1 } else { - colorIndex = abs(Int(clamping: accountPeerId.id &+ peerId.id)) + colorIndex = abs(Int(clamping: peerId.id)) } } else { colorIndex = -1 @@ -448,9 +464,14 @@ public final class AvatarNode: ASDisplayNode { colorsArray = grayscaleColors } } else if colorIndex == -1 { - colorsArray = grayscaleColors + if let parameters = parameters as? AvatarNodeParameters, let theme = parameters.theme { + let colors = theme.chatList.unpinnedArchiveAvatarColor.backgroundColors.colors + colorsArray = [colors.1.cgColor, colors.0.cgColor] + } else { + colorsArray = grayscaleColors + } } else { - colorsArray = gradientColors[colorIndex % gradientColors.count] + colorsArray = AvatarNode.gradientColors[colorIndex % AvatarNode.gradientColors.count] } var locations: [CGFloat] = [1.0, 0.0] @@ -486,8 +507,10 @@ public final class AvatarNode: ASDisplayNode { context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) - if let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIcon"), color: theme.list.freeMonoIconColor) { - context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0), y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0)), size: editAvatarIcon.size)) + if bounds.width > 90.0, let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIconLarge"), color: theme.list.itemAccentColor) { + context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0) + 0.5, y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0) + 1.0), size: editAvatarIcon.size)) + } else if let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIcon"), color: theme.list.itemAccentColor) { + context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0) + 0.5, y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0) + 1.0), size: editAvatarIcon.size)) } } else if case .archivedChatsIcon = parameters.icon { let factor = bounds.size.width / 60.0 @@ -520,10 +543,10 @@ public final class AvatarNode: ASDisplayNode { } } - static func asyncLayout(_ node: AvatarNode?) -> (_ account: Account, _ peer: Peer, _ font: UIFont) -> () -> AvatarNode? { + static func asyncLayout(_ node: AvatarNode?) -> (_ context: AccountContext, _ peer: Peer, _ font: UIFont) -> () -> AvatarNode? { let currentState = node?.state let createNode = node == nil - return { [weak node] account, peer, font in + return { [weak node] context, peer, font in let state: AvatarNodeState = .peerAvatar(peer.id, peer.displayLetters, peer.smallProfileImage) if currentState != state { @@ -549,7 +572,7 @@ public final class AvatarNode: ASDisplayNode { } } -public func drawPeerAvatarLetters(context: CGContext, size: CGSize, font: UIFont, letters: [String], accountPeerId: PeerId, peerId: PeerId) { +public func drawPeerAvatarLetters(context: CGContext, size: CGSize, font: UIFont, letters: [String], peerId: PeerId) { context.beginPath() context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) @@ -559,14 +582,14 @@ public func drawPeerAvatarLetters(context: CGContext, size: CGSize, font: UIFont if peerId.namespace == -1 { colorIndex = -1 } else { - colorIndex = abs(Int(clamping: accountPeerId.id &+ peerId.id)) + colorIndex = Int(abs(peerId.id)) } let colorsArray: NSArray if colorIndex == -1 { colorsArray = grayscaleColors } else { - colorsArray = gradientColors[colorIndex % gradientColors.count] + colorsArray = AvatarNode.gradientColors[colorIndex % AvatarNode.gradientColors.count] } var locations: [CGFloat] = [1.0, 0.0] @@ -576,6 +599,8 @@ public func drawPeerAvatarLetters(context: CGContext, size: CGSize, font: UIFont context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + context.resetClip() + context.setBlendMode(.normal) let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1])) @@ -591,7 +616,9 @@ public func drawPeerAvatarLetters(context: CGContext, size: CGSize, font: UIFont context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + let textPosition = context.textPosition context.translateBy(x: lineOrigin.x, y: lineOrigin.y) CTLineDraw(line, context) context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) + context.textPosition = textPosition } diff --git a/submodules/AvatarNode/Sources/PeerAvatar.swift b/submodules/AvatarNode/Sources/PeerAvatar.swift index 751ecfbd8c..5419985f9f 100644 --- a/submodules/AvatarNode/Sources/PeerAvatar.swift +++ b/submodules/AvatarNode/Sources/PeerAvatar.swift @@ -21,7 +21,7 @@ private let roundCorners = { () -> UIImage in return image }() -public func peerAvatarImageData(account: Account, peer: Peer, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, synchronousLoad: Bool) -> Signal? { +public func peerAvatarImageData(account: Account, peerReference: PeerReference?, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, synchronousLoad: Bool) -> Signal? { if let smallProfileImage = representation { let resourceData = account.postbox.mediaBox.resourceData(smallProfileImage.resource, attemptSynchronously: synchronousLoad) let imageData = resourceData @@ -44,7 +44,7 @@ public func peerAvatarImageData(account: Account, peer: Peer, authorOfMessage: M subscriber.putCompletion() }) var fetchedDataDisposable: Disposable? - if let peerReference = PeerReference(peer) { + if let peerReference = peerReference { fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .avatar(peer: peerReference, resource: smallProfileImage.resource), statsCategory: .generic).start() } else if let authorOfMessage = authorOfMessage { fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .messageAuthorAvatar(message: authorOfMessage, resource: smallProfileImage.resource), statsCategory: .generic).start() @@ -64,23 +64,31 @@ public func peerAvatarImageData(account: Account, peer: Peer, authorOfMessage: M } } -public func peerAvatarImage(account: Account, peer: Peer, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), round: Bool = true, inset: CGFloat = 0.0, emptyColor: UIColor? = nil, synchronousLoad: Bool = false) -> Signal? { - if let imageData = peerAvatarImageData(account: account, peer: peer, authorOfMessage: authorOfMessage, representation: representation, synchronousLoad: synchronousLoad) { +public func peerAvatarImage(account: Account, peerReference: PeerReference?, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), round: Bool = true, inset: CGFloat = 0.0, emptyColor: UIColor? = nil, synchronousLoad: Bool = false, provideUnrounded: Bool = false) -> Signal<(UIImage, UIImage)?, NoError>? { + if let imageData = peerAvatarImageData(account: account, peerReference: peerReference, authorOfMessage: authorOfMessage, representation: representation, synchronousLoad: synchronousLoad) { return imageData - |> mapToSignal { data -> Signal in - let generate = deferred { () -> Signal in + |> mapToSignal { data -> Signal<(UIImage, UIImage)?, NoError> in + let generate = deferred { () -> Signal<(UIImage, UIImage)?, NoError> in if emptyColor == nil && data == nil { return .single(nil) } - return .single(generateImage(displayDimensions, contextGenerator: { size, context -> Void in + let roundedImage = generateImage(displayDimensions, contextGenerator: { size, context -> Void in if let data = data { if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { context.clear(CGRect(origin: CGPoint(), size: displayDimensions)) context.setBlendMode(.copy) + + if round && displayDimensions.width != 60.0 { + context.addEllipse(in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) + context.clip() + } + context.draw(dataImage, in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) if round { - context.setBlendMode(.destinationOut) - context.draw(roundCorners.cgImage!, in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) + if displayDimensions.width == 60.0 { + context.setBlendMode(.destinationOut) + context.draw(roundCorners.cgImage!, in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) + } } } else { if let emptyColor = emptyColor { @@ -102,7 +110,41 @@ public func peerAvatarImage(account: Account, peer: Peer, authorOfMessage: Messa context.fill(CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) } } - })) + }) + let unroundedImage: UIImage? + if provideUnrounded { + unroundedImage = generateImage(displayDimensions, contextGenerator: { size, context -> Void in + if let data = data { + if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + context.clear(CGRect(origin: CGPoint(), size: displayDimensions)) + context.setBlendMode(.copy) + + context.draw(dataImage, in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) + } else { + if let emptyColor = emptyColor { + context.clear(CGRect(origin: CGPoint(), size: displayDimensions)) + context.setFillColor(emptyColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) + } + } + } else if let emptyColor = emptyColor { + context.clear(CGRect(origin: CGPoint(), size: displayDimensions)) + context.setFillColor(emptyColor.cgColor) + if round { + context.fillEllipse(in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) + } else { + context.fill(CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) + } + } + }) + } else { + unroundedImage = roundedImage + } + if let roundedImage = roundedImage, let unroundedImage = unroundedImage { + return .single((roundedImage, unroundedImage)) + } else { + return .single(nil) + } } if synchronousLoad { return generate diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift index 4a04199444..c79efa6d0a 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift @@ -87,7 +87,7 @@ public final class BotCheckoutController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition, additionalInsets: UIEdgeInsets()) } @objc private func cancelPressed() { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index 0b5a64ac53..51c459351e 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -162,7 +162,7 @@ enum BotCheckoutEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! BotCheckoutControllerArguments switch self { case let .header(theme, invoice, botName): @@ -170,27 +170,27 @@ enum BotCheckoutEntry: ItemListNodeEntry { case let .price(_, theme, text, value, isFinal): return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, sectionId: self.section) case let .paymentMethod(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openPaymentMethod() }) case let .shippingInfo(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openInfo(.address(.street1)) }) case let .shippingMethod(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openShippingMethod() }) case let .nameInfo(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openInfo(.name) }) case let .emailInfo(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openInfo(.email) }) case let .phoneInfo(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openInfo(.phone) }) } @@ -335,7 +335,8 @@ private func formSupportApplePay(_ paymentForm: BotPaymentForm) -> Bool { "stripe", "sberbank", "yandex", - "privatbank" + "privatbank", + "tranzzo" ]) if !applePayProviders.contains(nativeProvider.name) { return false @@ -418,12 +419,12 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz openShippingMethodImpl?() }) - let signal: Signal<(PresentationTheme, (ItemListNodeState, Any)), NoError> = combineLatest(context.sharedContext.presentationData, self.state.get(), paymentFormAndInfo.get(), context.account.postbox.loadedPeerWithId(messageId.peerId)) - |> map { presentationData, state, paymentFormAndInfo, botPeer -> (PresentationTheme, (ItemListNodeState, Any)) in - let nodeState = ItemListNodeState(entries: botCheckoutControllerEntries(presentationData: presentationData, state: state, invoice: invoice, paymentForm: paymentFormAndInfo?.0, formInfo: paymentFormAndInfo?.1, validatedFormInfo: paymentFormAndInfo?.2, currentShippingOptionId: paymentFormAndInfo?.3, currentPaymentMethod: paymentFormAndInfo?.4, botPeer: botPeer), style: .plain, focusItemTag: nil, emptyStateItem: nil, animateChanges: false) - - return (presentationData.theme, (nodeState, arguments)) - } + let signal: Signal<(ItemListPresentationData, (ItemListNodeState, Any)), NoError> = combineLatest(context.sharedContext.presentationData, self.state.get(), paymentFormAndInfo.get(), context.account.postbox.loadedPeerWithId(messageId.peerId)) + |> map { presentationData, state, paymentFormAndInfo, botPeer -> (ItemListPresentationData, (ItemListNodeState, Any)) in + let nodeState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: botCheckoutControllerEntries(presentationData: presentationData, state: state, invoice: invoice, paymentForm: paymentFormAndInfo?.0, formInfo: paymentFormAndInfo?.1, validatedFormInfo: paymentFormAndInfo?.2, currentShippingOptionId: paymentFormAndInfo?.3, currentPaymentMethod: paymentFormAndInfo?.4, botPeer: botPeer), style: .plain, focusItemTag: nil, emptyStateItem: nil, animateChanges: false) + + return (ItemListPresentationData(presentationData), (nodeState, arguments)) + } self.actionButton = BotCheckoutActionButton(inactiveFillColor: self.presentationData.theme.list.plainBackgroundColor, activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor) self.actionButton.setState(.loading) @@ -705,10 +706,10 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz } } - override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) { var updatedInsets = layout.intrinsicInsets updatedInsets.bottom += BotCheckoutActionButton.diameter + 20.0 - super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition) + super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition, additionalInsets: additionalInsets) let actionButtonFrame = CGRect(origin: CGPoint(x: 10.0, y: layout.size.height - 10.0 - BotCheckoutActionButton.diameter - layout.intrinsicInsets.bottom), size: CGSize(width: layout.size.width - 20.0, height: BotCheckoutActionButton.diameter)) transition.updateFrame(node: self.actionButton, frame: actionButtonFrame) diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift index c3096d8260..3b09d3f131 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift @@ -303,7 +303,7 @@ private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { func botCheckoutPasswordEntryController(context: AccountContext, strings: PresentationStrings, cartTitle: String, period: Int32, requiresBiometrics: Bool, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) -> AlertController { var dismissImpl: (() -> Void)? let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = AlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), contentNode: BotCheckoutPasswordAlertContentNode(context: context, theme: presentationData.theme, strings: strings, cardTitle: cartTitle, period: period, requiresBiometrics: requiresBiometrics, cancel: { + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: BotCheckoutPasswordAlertContentNode(context: context, theme: presentationData.theme, strings: strings, cardTitle: cartTitle, period: period, requiresBiometrics: requiresBiometrics, cancel: { dismissImpl?() }, completion: { token in completion(token) diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodSheet.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodSheet.swift index f67d0f2c32..58d07f8c43 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodSheet.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodSheet.swift @@ -42,11 +42,11 @@ final class BotCheckoutPaymentMethodSheetController: ActionSheetController { let theme = presentationData.theme let strings = presentationData.strings - super.init(theme: ActionSheetControllerTheme(presentationTheme: theme)) + super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationTheme: presentationData.theme) + strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) } }) @@ -137,7 +137,7 @@ public class BotCheckoutPaymentMethodItem: ActionSheetItem { } public class BotCheckoutPaymentMethodItemNode: ActionSheetItemNode { - public static let defaultFont: UIFont = Font.regular(20.0) + private let defaultFont: UIFont private let theme: ActionSheetControllerTheme @@ -150,6 +150,7 @@ public class BotCheckoutPaymentMethodItemNode: ActionSheetItemNode { public override init(theme: ActionSheetControllerTheme) { self.theme = theme + self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) self.button = HighlightTrackingButton() @@ -201,7 +202,7 @@ public class BotCheckoutPaymentMethodItemNode: ActionSheetItemNode { func setItem(_ item: BotCheckoutPaymentMethodItem) { self.item = item - self.titleNode.attributedText = NSAttributedString(string: item.title, font: BotCheckoutPaymentMethodItemNode.defaultFont, textColor: self.theme.primaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: item.title, font: self.defaultFont, textColor: self.theme.primaryTextColor) self.iconNode.image = item.icon if let value = item.value { self.checkNode.isHidden = !value diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentShippingOptionSheetController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentShippingOptionSheetController.swift index 5150821641..495a5e674d 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentShippingOptionSheetController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentShippingOptionSheetController.swift @@ -16,11 +16,11 @@ final class BotCheckoutPaymentShippingOptionSheetController: ActionSheetControll let theme = presentationData.theme let strings = presentationData.strings - super.init(theme: ActionSheetControllerTheme(presentationTheme: theme)) + super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationTheme: presentationData.theme) + strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) } }) @@ -113,7 +113,7 @@ public class BotCheckoutPaymentShippingOptionItem: ActionSheetItem { } public class BotCheckoutPaymentShippingOptionItemNode: ActionSheetItemNode { - public static let defaultFont: UIFont = Font.regular(20.0) + private let defaultFont: UIFont private let theme: ActionSheetControllerTheme @@ -126,6 +126,7 @@ public class BotCheckoutPaymentShippingOptionItemNode: ActionSheetItemNode { public override init(theme: ActionSheetControllerTheme) { self.theme = theme + self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) self.button = HighlightTrackingButton() @@ -178,8 +179,8 @@ public class BotCheckoutPaymentShippingOptionItemNode: ActionSheetItemNode { func setItem(_ item: BotCheckoutPaymentShippingOptionItem) { self.item = item - self.titleNode.attributedText = NSAttributedString(string: item.title, font: BotCheckoutPaymentShippingOptionItemNode.defaultFont, textColor: self.theme.primaryTextColor) - self.labelNode.attributedText = NSAttributedString(string: item.label, font: BotCheckoutPaymentShippingOptionItemNode.defaultFont, textColor: self.theme.primaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: item.title, font: self.defaultFont, textColor: self.theme.primaryTextColor) + self.labelNode.attributedText = NSAttributedString(string: item.label, font: self.defaultFont, textColor: self.theme.primaryTextColor) if let value = item.value { self.checkNode.isHidden = !value } else { diff --git a/submodules/BotPaymentsUI/Sources/BotReceiptController.swift b/submodules/BotPaymentsUI/Sources/BotReceiptController.swift index 9661ef66f9..35522c55cb 100644 --- a/submodules/BotPaymentsUI/Sources/BotReceiptController.swift +++ b/submodules/BotPaymentsUI/Sources/BotReceiptController.swift @@ -81,7 +81,7 @@ public final class BotReceiptController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition, additionalInsets: UIEdgeInsets()) } @objc private func cancelPressed() { diff --git a/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift index bc4a52031f..4f89100154 100644 --- a/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift @@ -149,7 +149,7 @@ enum BotReceiptEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! BotReceiptControllerArguments switch self { case let .header(theme, invoice, botName): @@ -157,17 +157,17 @@ enum BotReceiptEntry: ItemListNodeEntry { case let .price(_, theme, text, value, isFinal): return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, sectionId: self.section) case let .paymentMethod(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) case let .shippingInfo(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) case let .shippingMethod(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) case let .nameInfo(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) case let .emailInfo(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) case let .phoneInfo(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) } } } @@ -275,11 +275,11 @@ final class BotReceiptControllerNode: ItemListControllerNode { let arguments = BotReceiptControllerArguments(account: context.account) - let signal: Signal<(PresentationTheme, (ItemListNodeState, Any)), NoError> = combineLatest(context.sharedContext.presentationData, receiptData.get(), context.account.postbox.loadedPeerWithId(messageId.peerId)) - |> map { presentationData, receiptData, botPeer -> (PresentationTheme, (ItemListNodeState, Any)) in - let nodeState = ItemListNodeState(entries: botReceiptControllerEntries(presentationData: presentationData, invoice: invoice, formInvoice: receiptData?.0, formInfo: receiptData?.1, shippingOption: receiptData?.2, paymentMethodTitle: receiptData?.3, botPeer: botPeer), style: .plain, focusItemTag: nil, emptyStateItem: nil, animateChanges: false) - - return (presentationData.theme, (nodeState, arguments)) + let signal: Signal<(ItemListPresentationData, (ItemListNodeState, Any)), NoError> = combineLatest(context.sharedContext.presentationData, receiptData.get(), context.account.postbox.loadedPeerWithId(messageId.peerId)) + |> map { presentationData, receiptData, botPeer -> (ItemListPresentationData, (ItemListNodeState, Any)) in + let nodeState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: botReceiptControllerEntries(presentationData: presentationData, invoice: invoice, formInvoice: receiptData?.0, formInfo: receiptData?.1, shippingOption: receiptData?.2, paymentMethodTitle: receiptData?.3, botPeer: botPeer), style: .plain, focusItemTag: nil, emptyStateItem: nil, animateChanges: false) + + return (ItemListPresentationData(presentationData), (nodeState, arguments)) } self.actionButton = BotCheckoutActionButton(inactiveFillColor: self.presentationData.theme.list.plainBackgroundColor, activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.plainBackgroundColor) @@ -301,10 +301,10 @@ final class BotReceiptControllerNode: ItemListControllerNode { self.dataRequestDisposable?.dispose() } - override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) { var updatedInsets = layout.intrinsicInsets updatedInsets.bottom += BotCheckoutActionButton.diameter + 20.0 - super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition) + super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition, additionalInsets: additionalInsets) let actionButtonFrame = CGRect(origin: CGPoint(x: 10.0, y: layout.size.height - 10.0 - BotCheckoutActionButton.diameter - layout.intrinsicInsets.bottom), size: CGSize(width: layout.size.width - 20.0, height: BotCheckoutActionButton.diameter)) transition.updateFrame(node: self.actionButton, frame: actionButtonFrame) diff --git a/submodules/BuildConfig/BUCK b/submodules/BuildConfig/BUCK index 6298c2a05c..91dd26cf19 100644 --- a/submodules/BuildConfig/BUCK +++ b/submodules/BuildConfig/BUCK @@ -9,7 +9,7 @@ static_library( compiler_flags = [ '-DAPP_CONFIG_API_ID=' + appConfig()["apiId"], '-DAPP_CONFIG_API_HASH="' + appConfig()["apiHash"] + '"', - '-DAPP_CONFIG_HOCKEYAPP_ID="' + appConfig()["hockeyAppId"] + '"', + '-DAPP_CONFIG_APP_CENTER_ID="' + appConfig()["appCenterId"] + '"', '-DAPP_CONFIG_IS_INTERNAL_BUILD=' + appConfig()["isInternalBuild"], '-DAPP_CONFIG_IS_APPSTORE_BUILD=' + appConfig()["isAppStoreBuild"], '-DAPP_CONFIG_APPSTORE_ID=' + appConfig()["appStoreId"], diff --git a/submodules/BuildConfig/Sources/BuildConfig.h b/submodules/BuildConfig/Sources/BuildConfig.h index d13c28dc3f..562a63ce27 100644 --- a/submodules/BuildConfig/Sources/BuildConfig.h +++ b/submodules/BuildConfig/Sources/BuildConfig.h @@ -11,7 +11,7 @@ - (instancetype _Nonnull)initWithBaseAppBundleId:(NSString * _Nonnull)baseAppBundleId; -@property (nonatomic, strong, readonly) NSString * _Nullable hockeyAppId; +@property (nonatomic, strong, readonly) NSString * _Nullable appCenterId; @property (nonatomic, readonly) int32_t apiId; @property (nonatomic, strong, readonly) NSString * _Nonnull apiHash; @property (nonatomic, readonly) bool isInternalBuild; diff --git a/submodules/BuildConfig/Sources/BuildConfig.m b/submodules/BuildConfig/Sources/BuildConfig.m index 6c17f8f9eb..f4808b26cb 100644 --- a/submodules/BuildConfig/Sources/BuildConfig.m +++ b/submodules/BuildConfig/Sources/BuildConfig.m @@ -72,7 +72,7 @@ API_AVAILABLE(ios(10)) NSData * _Nullable _bundleData; int32_t _apiId; NSString * _Nonnull _apiHash; - NSString * _Nullable _hockeyAppId; + NSString * _Nullable _appCenterId; NSMutableDictionary * _Nonnull _dataDict; } @@ -129,7 +129,7 @@ API_AVAILABLE(ios(10)) if (self != nil) { _apiId = APP_CONFIG_API_ID; _apiHash = @(APP_CONFIG_API_HASH); - _hockeyAppId = @(APP_CONFIG_HOCKEYAPP_ID); + _appCenterId = @(APP_CONFIG_APP_CENTER_ID); _dataDict = [[NSMutableDictionary alloc] init]; @@ -163,8 +163,8 @@ API_AVAILABLE(ios(10)) return _apiHash; } -- (NSString * _Nullable)hockeyAppId { - return _hockeyAppId; +- (NSString * _Nullable)appCenterId { + return _appCenterId; } - (bool)isInternalBuild { diff --git a/submodules/CallListUI/Sources/CallListCallItem.swift b/submodules/CallListUI/Sources/CallListCallItem.swift index 0df9fc8f7a..8e2860add3 100644 --- a/submodules/CallListUI/Sources/CallListCallItem.swift +++ b/submodules/CallListUI/Sources/CallListCallItem.swift @@ -11,10 +11,7 @@ import ItemListUI import PresentationDataUtils import AvatarNode import TelegramStringFormatting - -private let titleFont = Font.regular(17.0) -private let statusFont = Font.regular(14.0) -private let dateFont = Font.regular(15.0) +import AccountContext private func callDurationString(strings: PresentationStrings, duration: Int32) -> String { if duration < 60 { @@ -67,10 +64,9 @@ private func callListNeighbors(item: ListViewItem, topItem: ListViewItem?, botto } class CallListCallItem: ListViewItem { - let theme: PresentationTheme - let strings: PresentationStrings + let presentationData: ItemListPresentationData let dateTimeFormat: PresentationDateTimeFormat - let account: Account + let context: AccountContext let style: ItemListStyle let topMessage: Message let messages: [Message] @@ -82,11 +78,10 @@ class CallListCallItem: ListViewItem { let headerAccessoryItem: ListViewAccessoryItem? let header: ListViewItemHeader? - init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, account: Account, style: ItemListStyle, topMessage: Message, messages: [Message], editing: Bool, revealed: Bool, interaction: CallListNodeInteraction) { - self.theme = theme - self.strings = strings + init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, context: AccountContext, style: ItemListStyle, topMessage: Message, messages: [Message], editing: Bool, revealed: Bool, interaction: CallListNodeInteraction) { + self.presentationData = presentationData self.dateTimeFormat = dateTimeFormat - self.account = account + self.context = context self.style = style self.topMessage = topMessage self.messages = messages @@ -110,7 +105,7 @@ class CallListCallItem: ListViewItem { Queue.mainQueue().async { completion(node, { return (nil, { _ in - nodeApply().1(false) + nodeApply(synchronousLoads).1(false) }) }) } @@ -130,7 +125,7 @@ class CallListCallItem: ListViewItem { } Queue.mainQueue().async { completion(nodeLayout, { _ in - apply().1(animated) + apply(false).1(animated) }) } } @@ -193,7 +188,6 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { private let accessibilityArea: AccessibilityAreaNode - private var avatarState: (Account, Peer?)? private var layoutParams: (CallListCallItem, ListViewItemLayoutParams, Bool, Bool, Bool)? required init() { @@ -256,7 +250,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { let (nodeLayout, nodeApply) = makeLayout(item, params, first, last, firstWithHeader, callListNeighbors(item: item, topItem: previousItem, bottomItem: nextItem)) self.contentSize = nodeLayout.contentSize self.insets = nodeLayout.insets - let _ = nodeApply() + let _ = nodeApply(false) } } @@ -290,7 +284,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { } } - func asyncLayout() -> (_ item: CallListCallItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> (Signal?, (Bool) -> Void)) { + func asyncLayout() -> (_ item: CallListCallItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> (Signal?, (Bool) -> Void)) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) let makeDateLayout = TextNode.asyncLayout(self.dateNode) @@ -301,23 +295,28 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { var updatedTheme: PresentationTheme? var updatedInfoIcon = false - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme updatedInfoIcon = true } + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + let statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + let dateFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + let avatarDiameter = min(40.0, floor(item.presentationData.fontSize.itemListBaseFontSize * 40.0 / 17.0)) + let editingOffset: CGFloat - var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? + var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)? if item.editing { - let sizeAndApply = editableControlLayout(50.0, item.theme, false) + let sizeAndApply = editableControlLayout(item.presentationData.theme, false) editableControlSizeAndApply = sizeAndApply - editingOffset = sizeAndApply.0.width + editingOffset = sizeAndApply.0 } else { editingOffset = 0.0 } - var leftInset: CGFloat = 86.0 + params.leftInset + var leftInset: CGFloat = 46.0 + avatarDiameter + params.leftInset let rightInset: CGFloat = 13.0 + params.rightInset var infoIconRightInset: CGFloat = rightInset - 1.0 @@ -328,12 +327,12 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { switch item.style { case .plain: - itemBackgroundColor = item.theme.list.plainBackgroundColor - itemSeparatorColor = item.theme.list.itemPlainSeparatorColor + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor insets = itemListNeighborsPlainInsets(neighbors) case .blocks: - itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor - itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor insets = itemListNeighborsGroupedInsets(neighbors) } @@ -347,7 +346,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { var titleAttributedString: NSAttributedString? var statusAttributedString: NSAttributedString? - var titleColor = item.theme.list.itemPrimaryTextColor + var titleColor = item.presentationData.theme.list.itemPrimaryTextColor var hasMissed = false var hasIncoming = false var hasOutgoing = false @@ -363,7 +362,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { hasIncoming = true if let discardReason = discardReason, case .missed = discardReason { - titleColor = item.theme.list.itemDestructiveColor + titleColor = item.presentationData.theme.list.itemDestructiveColor hasMissed = true } } else { @@ -397,7 +396,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { } else if let lastName = user.lastName, !lastName.isEmpty { titleAttributedString = NSAttributedString(string: lastName, font: titleFont, textColor: titleColor) } else { - titleAttributedString = NSAttributedString(string: item.strings.User_DeletedAccount, font: titleFont, textColor: titleColor) + titleAttributedString = NSAttributedString(string: item.presentationData.strings.User_DeletedAccount, font: titleFont, textColor: titleColor) } } else if let group = peer as? TelegramGroup { titleAttributedString = NSAttributedString(string: group.title, font: titleFont, textColor: titleColor) @@ -406,20 +405,20 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { } if hasMissed { - statusAttributedString = NSAttributedString(string: item.strings.Notification_CallMissedShort, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + statusAttributedString = NSAttributedString(string: item.presentationData.strings.Notification_CallMissedShort, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) } else if hasIncoming && hasOutgoing { - statusAttributedString = NSAttributedString(string: item.strings.Notification_CallOutgoingShort + ", " + item.strings.Notification_CallIncomingShort, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + statusAttributedString = NSAttributedString(string: item.presentationData.strings.Notification_CallOutgoingShort + ", " + item.presentationData.strings.Notification_CallIncomingShort, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) } else if hasIncoming { if let callDuration = callDuration, callDuration != 0 { - statusAttributedString = NSAttributedString(string: item.strings.Notification_CallTimeFormat(item.strings.Notification_CallIncomingShort, callDurationString(strings: item.strings, duration: callDuration)).0, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + statusAttributedString = NSAttributedString(string: item.presentationData.strings.Notification_CallTimeFormat(item.presentationData.strings.Notification_CallIncomingShort, callDurationString(strings: item.presentationData.strings, duration: callDuration)).0, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) } else { - statusAttributedString = NSAttributedString(string: item.strings.Notification_CallIncomingShort, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + statusAttributedString = NSAttributedString(string: item.presentationData.strings.Notification_CallIncomingShort, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) } } else { if let callDuration = callDuration, callDuration != 0 { - statusAttributedString = NSAttributedString(string: item.strings.Notification_CallTimeFormat(item.strings.Notification_CallOutgoingShort, callDurationString(strings: item.strings, duration: callDuration)).0, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + statusAttributedString = NSAttributedString(string: item.presentationData.strings.Notification_CallTimeFormat(item.presentationData.strings.Notification_CallOutgoingShort, callDurationString(strings: item.presentationData.strings, duration: callDuration)).0, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) } else { - statusAttributedString = NSAttributedString(string: item.strings.Notification_CallOutgoingShort, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + statusAttributedString = NSAttributedString(string: item.presentationData.strings.Notification_CallOutgoingShort, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) } } } @@ -429,29 +428,32 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { localtime_r(&t, &timeinfo) let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - let dateText = stringForRelativeTimestamp(strings: item.strings, relativeTimestamp: item.topMessage.timestamp, relativeTo: timestamp, dateTimeFormat: item.dateTimeFormat) + let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.topMessage.timestamp, relativeTo: timestamp, dateTimeFormat: item.dateTimeFormat) - let (dateLayout, dateApply) = makeDateLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: dateText, font: dateFont, textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (dateLayout, dateApply) = makeDateLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: dateText, font: dateFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - dateRightInset - dateLayout.size.width - (item.editing ? -30.0 : 10.0)), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 50.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) + let titleSpacing: CGFloat = -1.0 + let verticalInset: CGFloat = 6.0 - let outgoingIcon = PresentationResourcesCallList.outgoingIcon(item.theme) - let infoIcon = PresentationResourcesCallList.infoButton(item.theme) + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: titleLayout.size.height + titleSpacing + statusLayout.size.height + verticalInset * 2.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) + + let outgoingIcon = PresentationResourcesCallList.outgoingIcon(item.presentationData.theme) + let infoIcon = PresentationResourcesCallList.infoButton(item.presentationData.theme) let contentSize = nodeLayout.contentSize - return (nodeLayout, { [weak self] in + return (nodeLayout, { [weak self] synchronousLoads in if let strongSelf = self { if let peer = item.topMessage.peers[item.topMessage.id.peerId] { var overrideImage: AvatarNodeImageOverride? if peer.isDeleted { overrideImage = .deletedIcon } - strongSelf.avatarNode.setPeer(account: item.account, theme: item.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.theme.list.mediaPlaceholderColor) + strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads) } return (strongSelf.avatarNode.ready, { [weak strongSelf] animated in @@ -471,7 +473,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { strongSelf.topStripeNode.backgroundColor = itemSeparatorColor strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } switch item.style { @@ -519,9 +521,9 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { } if let editableControlSizeAndApply = editableControlSizeAndApply { - let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: editableControlSizeAndApply.0) + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: nodeLayout.contentSize.height)) if strongSelf.editableControlNode == nil { - let editableControlNode = editableControlSizeAndApply.1() + let editableControlNode = editableControlSizeAndApply.1(nodeLayout.contentSize.height) editableControlNode.tapped = { if let strongSelf = self { strongSelf.setRevealOptionsOpened(true, animated: true) @@ -547,13 +549,13 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { }) } - transition.updateFrameAdditive(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 52.0, y: 5.0), size: CGSize(width: 40.0, height: 40.0))) + transition.updateFrameAdditive(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 52.0, y: floor((contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter))) let _ = titleApply() - transition.updateFrameAdditive(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 6.0), size: titleLayout.size)) + transition.updateFrameAdditive(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: verticalInset), size: titleLayout.size)) let _ = statusApply() - transition.updateFrameAdditive(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 27.0), size: statusLayout.size)) + transition.updateFrameAdditive(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size)) let _ = dateApply() transition.updateFrameAdditive(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + params.width - dateRightInset - dateLayout.size.width, y: floor((nodeLayout.contentSize.height - dateLayout.size.height) / 2.0) + 2.0), size: dateLayout.size)) @@ -585,11 +587,11 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { strongSelf.accessibilityArea.accessibilityValue = statusAttributedString?.string strongSelf.accessibilityArea.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize) - strongSelf.infoButtonNode.accessibilityLabel = item.strings.Conversation_Info + strongSelf.infoButtonNode.accessibilityLabel = item.presentationData.strings.Conversation_Info - strongSelf.view.accessibilityCustomActions = [UIAccessibilityCustomAction(name: item.strings.Common_Delete, target: strongSelf, selector: #selector(strongSelf.performLocalAccessibilityCustomAction(_:)))] + strongSelf.view.accessibilityCustomActions = [UIAccessibilityCustomAction(name: item.presentationData.strings.Common_Delete, target: strongSelf, selector: #selector(strongSelf.performLocalAccessibilityCustomAction(_:)))] - strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)])) strongSelf.setRevealOptionsOpened(item.revealed, animated: animated) } }) @@ -667,9 +669,9 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { transition.updateFrameAdditive(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 52.0, y: 5.0), size: CGSize(width: 40.0, height: 40.0))) - transition.updateFrameAdditive(node: self.titleNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 6.0), size: self.titleNode.bounds.size)) + transition.updateFrameAdditive(node: self.titleNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size)) - transition.updateFrameAdditive(node: self.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 27.0), size: self.statusNode.bounds.size)) + transition.updateFrameAdditive(node: self.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: self.statusNode.frame.minY), size: self.statusNode.bounds.size)) transition.updateFrameAdditive(node: self.dateNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + self.bounds.size.width - dateRightInset - self.dateNode.bounds.size.width, y: self.dateNode.frame.minY), size: self.dateNode.bounds.size)) diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index 6385301617..573847c575 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -59,7 +59,12 @@ public final class CallListController: ViewController { if case .tab = self.mode { self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed)) - let icon = UIImage(bundleImageName: "Chat List/Tabs/IconCalls") + let icon: UIImage? + if useSpecialTabBarIcons() { + icon = UIImage(bundleImageName: "Chat List/Tabs/Holiday/IconCalls") + } else { + icon = UIImage(bundleImageName: "Chat List/Tabs/IconCalls") + } self.tabBarItem.title = self.presentationData.strings.Calls_TabTitle self.tabBarItem.image = icon self.tabBarItem.selectedImage = icon @@ -134,7 +139,7 @@ public final class CallListController: ViewController { self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) if self.isNodeLoaded { - self.controllerNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, disableAnimations: self.presentationData.disableAnimations) + self.controllerNode.updateThemeAndStrings(presentationData: self.presentationData) } } @@ -149,7 +154,7 @@ public final class CallListController: ViewController { let _ = (strongSelf.context.account.postbox.loadedPeerWithId(peerId) |> take(1) |> deliverOnMainQueue).start(next: { peer in - if let strongSelf = self, let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .calls(messages: messages)) { + if let strongSelf = self, let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .calls(messages: messages), avatarInitiallyExpanded: false, fromChat: false) { (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) } }) diff --git a/submodules/CallListUI/Sources/CallListControllerNode.swift b/submodules/CallListUI/Sources/CallListControllerNode.swift index 90f865e94f..96c2880606 100644 --- a/submodules/CallListUI/Sources/CallListControllerNode.swift +++ b/submodules/CallListUI/Sources/CallListControllerNode.swift @@ -74,30 +74,26 @@ final class CallListNodeInteraction { } struct CallListNodeState: Equatable { - let theme: PresentationTheme - let strings: PresentationStrings + let presentationData: ItemListPresentationData let dateTimeFormat: PresentationDateTimeFormat let disableAnimations: Bool let editing: Bool let messageIdWithRevealedOptions: MessageId? - func withUpdatedPresentationData(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, disableAnimations: Bool) -> CallListNodeState { - return CallListNodeState(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, disableAnimations: disableAnimations, editing: self.editing, messageIdWithRevealedOptions: self.messageIdWithRevealedOptions) + func withUpdatedPresentationData(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, disableAnimations: Bool) -> CallListNodeState { + return CallListNodeState(presentationData: presentationData, dateTimeFormat: dateTimeFormat, disableAnimations: disableAnimations, editing: self.editing, messageIdWithRevealedOptions: self.messageIdWithRevealedOptions) } func withUpdatedEditing(_ editing: Bool) -> CallListNodeState { - return CallListNodeState(theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, disableAnimations: self.disableAnimations, editing: editing, messageIdWithRevealedOptions: self.messageIdWithRevealedOptions) + return CallListNodeState(presentationData: self.presentationData, dateTimeFormat: self.dateTimeFormat, disableAnimations: self.disableAnimations, editing: editing, messageIdWithRevealedOptions: self.messageIdWithRevealedOptions) } func withUpdatedMessageIdWithRevealedOptions(_ messageIdWithRevealedOptions: MessageId?) -> CallListNodeState { - return CallListNodeState(theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, disableAnimations: self.disableAnimations, editing: self.editing, messageIdWithRevealedOptions: messageIdWithRevealedOptions) + return CallListNodeState(presentationData: self.presentationData, dateTimeFormat: self.dateTimeFormat, disableAnimations: self.disableAnimations, editing: self.editing, messageIdWithRevealedOptions: messageIdWithRevealedOptions) } static func ==(lhs: CallListNodeState, rhs: CallListNodeState) -> Bool { - if lhs.theme !== rhs.theme { - return false - } - if lhs.strings !== rhs.strings { + if lhs.presentationData != rhs.presentationData { return false } if lhs.dateTimeFormat != rhs.dateTimeFormat { @@ -113,42 +109,42 @@ struct CallListNodeState: Equatable { } } -private func mappedInsertEntries(account: Account, showSettings: Bool, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { +private func mappedInsertEntries(context: AccountContext, presentationData: ItemListPresentationData, showSettings: Bool, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { case let .displayTab(theme, text, value): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(theme: theme, title: text, value: value, enabled: true, sectionId: 0, style: .blocks, updated: { value in + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, sectionId: 0, style: .blocks, updated: { value in nodeInteraction.updateShowCallsTab(value) }), directionHint: entry.directionHint) case let .displayTabInfo(theme, text): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(theme: theme, text: .plain(text), sectionId: 0), directionHint: entry.directionHint) + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint) case let .messageEntry(topMessage, messages, theme, strings, dateTimeFormat, editing, hasActiveRevealControls): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, account: account, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, interaction: nodeInteraction), directionHint: entry.directionHint) + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, context: context, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, interaction: nodeInteraction), directionHint: entry.directionHint) case let .holeEntry(_, theme): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListHoleItem(theme: theme), directionHint: entry.directionHint) } } } -private func mappedUpdateEntries(account: Account, showSettings: Bool, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { +private func mappedUpdateEntries(context: AccountContext, presentationData: ItemListPresentationData, showSettings: Bool, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { case let .displayTab(theme, text, value): - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(theme: theme, title: text, value: value, enabled: true, sectionId: 0, style: .blocks, updated: { value in + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, sectionId: 0, style: .blocks, updated: { value in nodeInteraction.updateShowCallsTab(value) }), directionHint: entry.directionHint) case let .displayTabInfo(theme, text): - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(theme: theme, text: .plain(text), sectionId: 0), directionHint: entry.directionHint) + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint) case let .messageEntry(topMessage, messages, theme, strings, dateTimeFormat, editing, hasActiveRevealControls): - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, account: account, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, interaction: nodeInteraction), directionHint: entry.directionHint) + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, context: context, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, interaction: nodeInteraction), directionHint: entry.directionHint) case let .holeEntry(_, theme): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListHoleItem(theme: theme), directionHint: entry.directionHint) } } } -private func mappedCallListNodeViewListTransition(account: Account, showSettings: Bool, nodeInteraction: CallListNodeInteraction, transition: CallListNodeViewTransition) -> CallListNodeListViewTransition { - return CallListNodeListViewTransition(callListView: transition.callListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, showSettings: showSettings, nodeInteraction: nodeInteraction, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, showSettings: showSettings, nodeInteraction: nodeInteraction, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange) +private func mappedCallListNodeViewListTransition(context: AccountContext, presentationData: ItemListPresentationData, showSettings: Bool, nodeInteraction: CallListNodeInteraction, transition: CallListNodeViewTransition) -> CallListNodeListViewTransition { + return CallListNodeListViewTransition(callListView: transition.callListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, presentationData: presentationData, showSettings: showSettings, nodeInteraction: nodeInteraction, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, presentationData: presentationData, showSettings: showSettings, nodeInteraction: nodeInteraction, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange) } private final class CallListOpaqueTransactionState { @@ -209,7 +205,7 @@ final class CallListControllerNode: ASDisplayNode { self.openInfo = openInfo self.emptyStateUpdated = emptyStateUpdated - self.currentState = CallListNodeState(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: presentationData.disableAnimations, editing: false, messageIdWithRevealedOptions: nil) + self.currentState = CallListNodeState(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: presentationData.disableAnimations, editing: false, messageIdWithRevealedOptions: nil) self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) self.listNode = ListView() @@ -258,7 +254,7 @@ final class CallListControllerNode: ASDisplayNode { self?.openInfo(peerId, messages) }, delete: { [weak self] messageIds in if let strongSelf = self { - let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: messageIds, type: .forLocalPeer).start() + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messageIds, type: .forLocalPeer).start() } }, updateShowCallsTab: { [weak self] value in if let strongSelf = self { @@ -292,15 +288,16 @@ final class CallListControllerNode: ASDisplayNode { let showCallsTab = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.callListSettings]) |> map { sharedData -> Bool in - var value = true + var value = CallListSettings.defaultSettings.showTab if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.callListSettings] as? CallListSettings { value = settings.showTab } return value } - let callListNodeViewTransition = combineLatest(callListViewUpdate, self.statePromise.get(), showCallsTab) |> mapToQueue { (update, state, showCallsTab) -> Signal in - let processedView = CallListNodeView(originalView: update.view, filteredEntries: callListNodeEntriesForView(update.view, state: state, showSettings: showSettings, showCallsTab: showCallsTab)) + let callListNodeViewTransition = combineLatest(callListViewUpdate, self.statePromise.get(), showCallsTab) + |> mapToQueue { (update, state, showCallsTab) -> Signal in + let processedView = CallListNodeView(originalView: update.view, filteredEntries: callListNodeEntriesForView(update.view, state: state, showSettings: showSettings, showCallsTab: showCallsTab), presentationData: state.presentationData) let previous = previousView.swap(processedView) let reason: CallListNodeViewTransitionReason @@ -343,7 +340,7 @@ final class CallListControllerNode: ASDisplayNode { } return preparedCallListNodeViewTransition(from: previous, to: processedView, reason: reason, disableAnimations: false, account: context.account, scrollPosition: update.scrollPosition) - |> map({ mappedCallListNodeViewListTransition(account: context.account, showSettings: showSettings, nodeInteraction: nodeInteraction, transition: $0) }) + |> map({ mappedCallListNodeViewListTransition(context: context, presentationData: state.presentationData, showSettings: showSettings, nodeInteraction: nodeInteraction, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue) } @@ -381,7 +378,7 @@ final class CallListControllerNode: ASDisplayNode { self.emptyStateDisposable.set((combineLatest(emptySignal, typeSignal, self.statePromise.get()) |> deliverOnMainQueue).start(next: { [weak self] isEmpty, type, state in if let strongSelf = self { - strongSelf.updateEmptyPlaceholder(theme: state.theme, strings: state.strings, type: type, hidden: !isEmpty) + strongSelf.updateEmptyPlaceholder(theme: state.presentationData.theme, strings: state.presentationData.strings, type: type, hidden: !isEmpty) } })) } @@ -391,23 +388,25 @@ final class CallListControllerNode: ASDisplayNode { self.emptyStateDisposable.dispose() } - func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, disableAnimations: Bool) { - if theme !== self.currentState.theme || strings !== self.currentState.strings || disableAnimations != self.currentState.disableAnimations { - self.leftOverlayNode.backgroundColor = theme.list.blocksBackgroundColor - self.rightOverlayNode.backgroundColor = theme.list.blocksBackgroundColor + func updateThemeAndStrings(presentationData: PresentationData) { + if presentationData.theme !== self.currentState.presentationData.theme || presentationData.strings !== self.currentState.presentationData.strings || presentationData.disableAnimations != self.currentState.disableAnimations { + self.presentationData = presentationData + + self.leftOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor + self.rightOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor switch self.mode { case .tab: - self.backgroundColor = theme.chatList.backgroundColor - self.listNode.backgroundColor = theme.chatList.backgroundColor + self.backgroundColor = presentationData.theme.chatList.backgroundColor + self.listNode.backgroundColor = presentationData.theme.chatList.backgroundColor case .navigation: - self.backgroundColor = theme.list.blocksBackgroundColor - self.listNode.backgroundColor = theme.list.blocksBackgroundColor + self.backgroundColor = presentationData.theme.list.blocksBackgroundColor + self.listNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor } - self.updateEmptyPlaceholder(theme: theme, strings: strings, type: self.currentLocationAndType.type, hidden: self.emptyTextNode.isHidden) + self.updateEmptyPlaceholder(theme: presentationData.theme, strings: presentationData.strings, type: self.currentLocationAndType.type, hidden: self.emptyTextNode.isHidden) self.updateState { - return $0.withUpdatedPresentationData(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, disableAnimations: disableAnimations) + return $0.withUpdatedPresentationData(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: presentationData.disableAnimations) } } } @@ -563,29 +562,8 @@ final class CallListControllerNode: ASDisplayNode { self.updateLayout(layout, navigationBarHeight: navigationBarHeight, transition: transition) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) diff --git a/submodules/CallListUI/Sources/CallListNodeEntries.swift b/submodules/CallListUI/Sources/CallListNodeEntries.swift index ac60b72bde..5080c7c156 100644 --- a/submodules/CallListUI/Sources/CallListNodeEntries.swift +++ b/submodules/CallListUI/Sources/CallListNodeEntries.swift @@ -183,14 +183,14 @@ func callListNodeEntriesForView(_ view: CallListView, state: CallListNodeState, for entry in view.entries { switch entry { case let .message(topMessage, messages): - result.append(.messageEntry(topMessage: topMessage, messages: messages, theme: state.theme, strings: state.strings, dateTimeFormat: state.dateTimeFormat, editing: state.editing, hasActiveRevealControls: state.messageIdWithRevealedOptions == topMessage.id)) + result.append(.messageEntry(topMessage: topMessage, messages: messages, theme: state.presentationData.theme, strings: state.presentationData.strings, dateTimeFormat: state.dateTimeFormat, editing: state.editing, hasActiveRevealControls: state.messageIdWithRevealedOptions == topMessage.id)) case let .hole(index): - result.append(.holeEntry(index: index, theme: state.theme)) + result.append(.holeEntry(index: index, theme: state.presentationData.theme)) } } if showSettings { - result.append(.displayTabInfo(state.theme, state.strings.CallSettings_TabIconDescription)) - result.append(.displayTab(state.theme, state.strings.CallSettings_TabIcon, showCallsTab)) + result.append(.displayTabInfo(state.presentationData.theme, state.presentationData.strings.CallSettings_TabIconDescription)) + result.append(.displayTab(state.presentationData.theme, state.presentationData.strings.CallSettings_TabIcon, showCallsTab)) } return result } diff --git a/submodules/CallListUI/Sources/CallListViewTransition.swift b/submodules/CallListUI/Sources/CallListViewTransition.swift index 8d72eebda2..09f7e03c96 100644 --- a/submodules/CallListUI/Sources/CallListViewTransition.swift +++ b/submodules/CallListUI/Sources/CallListViewTransition.swift @@ -6,10 +6,12 @@ import SyncCore import SwiftSignalKit import Display import MergeLists +import ItemListUI struct CallListNodeView { let originalView: CallListView let filteredEntries: [CallListNodeEntry] + let presentationData: ItemListPresentationData } enum CallListNodeViewTransitionReason { @@ -49,7 +51,7 @@ enum CallListNodeViewScrollPosition { func preparedCallListNodeViewTransition(from fromView: CallListNodeView?, to toView: CallListNodeView, reason: CallListNodeViewTransitionReason, disableAnimations: Bool, account: Account, scrollPosition: CallListNodeViewScrollPosition?) -> Signal { return Signal { subscriber in - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries) + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries, allUpdated: fromView?.presentationData != toView.presentationData) var adjustedDeleteIndices: [ListViewDeleteItem] = [] let previousCount: Int @@ -70,11 +72,21 @@ func preparedCallListNodeViewTransition(from fromView: CallListNodeView?, to toV var stationaryItemRange: (Int, Int)? var scrollToItem: ListViewScrollToItem? + var wasEmpty = false + if let fromView = fromView, fromView.originalView.entries.isEmpty { + wasEmpty = true + } + switch reason { - case .initial: - let _ = options.insert(.LowLatency) + case .initial: + let _ = options.insert(.LowLatency) + let _ = options.insert(.Synchronous) + let _ = options.insert(.PreferSynchronousResourceLoading) + case .interactiveChanges: + if wasEmpty { let _ = options.insert(.Synchronous) - case .interactiveChanges: + let _ = options.insert(.PreferSynchronousResourceLoading) + } else { let _ = options.insert(.AnimateAlpha) if !disableAnimations { let _ = options.insert(.AnimateInsertion) @@ -86,12 +98,14 @@ func preparedCallListNodeViewTransition(from fromView: CallListNodeView?, to toV maxAnimatedInsertionIndex += 1 } } - case .reload: - break - case .reloadAnimated: - let _ = options.insert(.LowLatency) - let _ = options.insert(.Synchronous) - let _ = options.insert(.AnimateCrossfade) + } + case .reload: + break + case .reloadAnimated: + let _ = options.insert(.LowLatency) + let _ = options.insert(.Synchronous) + let _ = options.insert(.AnimateCrossfade) + let _ = options.insert(.PreferSynchronousResourceLoading) } for (index, entry, previousIndex) in indicesAndItems { diff --git a/submodules/Charts/BUCK b/submodules/Charts/BUCK new file mode 100644 index 0000000000..79bf8180b8 --- /dev/null +++ b/submodules/Charts/BUCK @@ -0,0 +1,31 @@ +load("//Config:buck_rule_macros.bzl", "static_library") + +static_library( + name = "Charts", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared", + "//submodules/AsyncDisplayKit:AsyncDisplayKit#shared", + "//submodules/Display:Display#shared", + "//submodules/Postbox:Postbox#shared", + "//submodules/TelegramCore:TelegramCore#shared", + "//submodules/SyncCore:SyncCore#shared", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/AccountContext:AccountContext", + "//submodules/ItemListUI:ItemListUI", + "//submodules/AvatarNode:AvatarNode", + "//submodules/TelegramStringFormatting:TelegramStringFormatting", + "//submodules/AlertUI:AlertUI", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/TelegramNotices:TelegramNotices", + "//submodules/MergeLists:MergeLists", + "//submodules/AppBundle:AppBundle", + ], + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/UIKit.framework", + ], +) diff --git a/submodules/AppIntents/Info.plist b/submodules/Charts/Info.plist similarity index 100% rename from submodules/AppIntents/Info.plist rename to submodules/Charts/Info.plist diff --git a/submodules/Charts/Sources/Chart Screen/ChartDetailsView.swift b/submodules/Charts/Sources/Chart Screen/ChartDetailsView.swift new file mode 100644 index 0000000000..7232c594c3 --- /dev/null +++ b/submodules/Charts/Sources/Chart Screen/ChartDetailsView.swift @@ -0,0 +1,258 @@ +// +// ChartDetailsView.swift +// GraphTest +// +// Created by Andrew Solovey on 14/03/2019. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +private let cornerRadius: CGFloat = 5 +private let verticalMargins: CGFloat = 8 +private var labelHeight: CGFloat = 18 +private var margin: CGFloat = 10 +private var prefixLabelWidth: CGFloat = 27 +private var textLabelWidth: CGFloat = 60 +private var valueLabelWidth: CGFloat = 65 + +struct ChartDetailsViewModel { + struct Value { + let prefix: String? + let title: String + let value: String + let color: UIColor + let visible: Bool + } + + var title: String + var showArrow: Bool + var showPrefixes: Bool + var values: [Value] + var totalValue: Value? + var tapAction: (() -> Void)? + + static let blank = ChartDetailsViewModel(title: "", showArrow: false, showPrefixes: false, values: [], totalValue: nil, tapAction: nil) +} + +class ChartDetailsView: UIControl { + let titleLabel = UILabel() + let arrowView = UIImageView() + + var prefixViews: [UILabel] = [] + var labelsViews: [UILabel] = [] + var valuesViews: [UILabel] = [] + + private var viewModel: ChartDetailsViewModel? + private var colorMode: ColorMode = .day + + static func fromNib() -> ChartDetailsView { + return Bundle.main.loadNibNamed("ChartDetailsView", owner: nil, options: nil)?.first as! ChartDetailsView + } + + override init(frame: CGRect) { + super.init(frame: frame) + + layer.cornerRadius = cornerRadius + clipsToBounds = true + + addTarget(self, action: #selector(didTap), for: .touchUpInside) + titleLabel.font = UIFont.systemFont(ofSize: 12, weight: .bold) + arrowView.image = UIImage.arrowRight + arrowView.contentMode = .scaleAspectFill + + addSubview(titleLabel) + addSubview(arrowView) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setup(viewModel: ChartDetailsViewModel, animated: Bool) { + self.viewModel = viewModel + + titleLabel.setText(viewModel.title, animated: animated) + titleLabel.setVisible(!viewModel.title.isEmpty, animated: animated) + arrowView.setVisible(viewModel.showArrow, animated: animated) + + let width: CGFloat = margin * 2 + (viewModel.showPrefixes ? (prefixLabelWidth + margin) : 0) + textLabelWidth + valueLabelWidth + var y: CGFloat = verticalMargins + + if (!viewModel.title.isEmpty || viewModel.showArrow) { + titleLabel.frame = CGRect(x: margin, y: y, width: width, height: labelHeight) + arrowView.frame = CGRect(x: width - 6 - margin, y: margin, width: 6, height: 10) + y += labelHeight + } + let labelsCount: Int = viewModel.values.count + ((viewModel.totalValue == nil) ? 0 : 1) + + setLabelsCount(array: &prefixViews, + count: viewModel.showPrefixes ? labelsCount : 0, + font: UIFont.systemFont(ofSize: 12, weight: .bold)) + setLabelsCount(array: &labelsViews, + count: labelsCount, + font: UIFont.systemFont(ofSize: 12, weight: .regular), + textAlignment: .left) + setLabelsCount(array: &valuesViews, + count: labelsCount, + font: UIFont.systemFont(ofSize: 12, weight: .bold)) + + UIView.perform(animated: animated, animations: { + for (index, value) in viewModel.values.enumerated() { + var x: CGFloat = margin + if viewModel.showPrefixes { + let prefixLabel = self.prefixViews[index] + prefixLabel.textColor = self.colorMode.chartDetailsTextColor + prefixLabel.setText(value.prefix, animated: false) + prefixLabel.frame = CGRect(x: x, y: y, width: prefixLabelWidth, height: labelHeight) + x += prefixLabelWidth + margin + prefixLabel.alpha = value.visible ? 1 : 0 + } + let titleLabel = self.labelsViews[index] + titleLabel.setTextColor(self.colorMode.chartDetailsTextColor, animated: false) + titleLabel.setText(value.title, animated: false) + titleLabel.frame = CGRect(x: x, y: y, width: textLabelWidth, height: labelHeight) + titleLabel.alpha = value.visible ? 1 : 0 + x += textLabelWidth + + let valueLabel = self.valuesViews[index] + valueLabel.setTextColor(value.color, animated: false) + valueLabel.setText(value.value, animated: false) + valueLabel.frame = CGRect(x: x, y: y, width: valueLabelWidth, height: labelHeight) + valueLabel.alpha = value.visible ? 1 : 0 + + if value.visible { + y += labelHeight + } + } + if let value = viewModel.totalValue { + var x: CGFloat = margin + if viewModel.showPrefixes { + let prefixLabel = self.prefixViews[viewModel.values.count] + prefixLabel.textColor = self.colorMode.chartDetailsTextColor + prefixLabel.setText(value.prefix, animated: false) + prefixLabel.frame = CGRect(x: x, y: y, width: prefixLabelWidth, height: labelHeight) + prefixLabel.alpha = value.visible ? 1 : 0 + x += prefixLabelWidth + margin + } + let titleLabel = self.labelsViews[viewModel.values.count] + titleLabel.setTextColor(self.colorMode.chartDetailsTextColor, animated: false) + titleLabel.setText(value.title, animated: false) + titleLabel.frame = CGRect(x: x, y: y, width: textLabelWidth, height: labelHeight) + titleLabel.alpha = value.visible ? 1 : 0 + x += textLabelWidth + + let valueLabel = self.valuesViews[viewModel.values.count] + valueLabel.setTextColor(self.colorMode.chartDetailsTextColor, animated: false) + valueLabel.setText(value.value, animated: false) + valueLabel.frame = CGRect(x: x, y: y, width: valueLabelWidth, height: labelHeight) + valueLabel.alpha = value.visible ? 1 : 0 + } + }) + } + + override var intrinsicContentSize: CGSize { + if let viewModel = viewModel { + let height = ((!viewModel.title.isEmpty || viewModel.showArrow) ? labelHeight : 0) + + (CGFloat(viewModel.values.filter({ $0.visible }).count) * labelHeight) + + (viewModel.totalValue?.visible == true ? labelHeight : 0) + + verticalMargins * 2 + let width: CGFloat = margin * 2 + + (viewModel.showPrefixes ? (prefixLabelWidth + margin) : 0) + + textLabelWidth + + valueLabelWidth + + return CGSize(width: width, + height: height) + } else { + return CGSize(width: 140, + height: labelHeight + verticalMargins) + } + } + + @objc private func didTap() { + viewModel?.tapAction?() + } + + func setLabelsCount(array: inout [UILabel], + count: Int, + font: UIFont, + textAlignment: NSTextAlignment = .right) { + while array.count > count { + let subview = array.removeLast() + subview.removeFromSuperview() + } + while array.count < count { + let label = UILabel() + label.font = font + label.adjustsFontSizeToFitWidth = true + label.minimumScaleFactor = 0.5 + label.textAlignment = textAlignment + addSubview(label) + array.append(label) + } + } +} + +extension ChartDetailsView: ColorModeContainer { + func apply(colorMode: ColorMode, animated: Bool) { + self.colorMode = colorMode + self.titleLabel.setTextColor(colorMode.chartDetailsTextColor, animated: animated) + if let viewModel = self.viewModel { + self.setup(viewModel: viewModel, animated: animated) + } + UIView.perform(animated: animated) { + self.arrowView.tintColor = colorMode.chartDetailsArrowColor + self.backgroundColor = colorMode.chartDetailsViewColor + } + } +} + +// MARK: UIStackView+removeAllArrangedSubviews +public extension UIStackView { + func setLabelsCount(_ count: Int, + font: UIFont, + huggingPriority: UILayoutPriority, + textAlignment: NSTextAlignment = .right) { + while arrangedSubviews.count > count { + let subview = arrangedSubviews.last! + removeArrangedSubview(subview) + subview.removeFromSuperview() + } + while arrangedSubviews.count < count { + let label = UILabel() + label.font = font + label.textAlignment = textAlignment + label.setContentHuggingPriority(huggingPriority, for: .horizontal) + label.setContentHuggingPriority(huggingPriority, for: .vertical) + label.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 999), for: .horizontal) + label.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 999), for: .vertical) + addArrangedSubview(label) + } + } + + func label(at index: Int) -> UILabel { + return arrangedSubviews[index] as! UILabel + } + + func removeAllArrangedSubviews() { + for subview in arrangedSubviews { + removeArrangedSubview(subview) + subview.removeFromSuperview() + } + } +} + +// MARK: UIStackView+addArrangedSubviews +public extension UIStackView { + func addArrangedSubviews(_ views: [UIView]) { + views.forEach({ addArrangedSubview($0) }) + } +} + +// MARK: UIStackView+insertArrangedSubviews +public extension UIStackView { + func insertArrangedSubviews(_ views: [UIView], at index: Int) { + views.reversed().forEach({ insertArrangedSubview($0, at: index) }) + } +} diff --git a/submodules/Charts/Sources/Chart Screen/ChartStackSection.swift b/submodules/Charts/Sources/Chart Screen/ChartStackSection.swift new file mode 100644 index 0000000000..bef1a574f4 --- /dev/null +++ b/submodules/Charts/Sources/Chart Screen/ChartStackSection.swift @@ -0,0 +1,199 @@ +// +// ChartStackSection.swift +// GraphTest +// +// Created by Andrei Salavei on 4/13/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +private enum Constants { + static let chartViewHeightFraction: CGFloat = 0.55 +} + +class ChartStackSection: UIView, ColorModeContainer { + var chartView: ChartView + var rangeView: RangeChartView + var visibilityView: ChartVisibilityView + var sectionContainerView: UIView + var separators: [UIView] = [] + + var headerLabel: UILabel! + var titleLabel: UILabel! + var backButton: UIButton! + + var controller: BaseChartController! + + init() { + sectionContainerView = UIView() + chartView = ChartView() + rangeView = RangeChartView() + visibilityView = ChartVisibilityView() + headerLabel = UILabel() + titleLabel = UILabel() + backButton = UIButton() + + super.init(frame: CGRect()) + + self.addSubview(sectionContainerView) + sectionContainerView.addSubview(chartView) + sectionContainerView.addSubview(rangeView) + sectionContainerView.addSubview(visibilityView) + + headerLabel.font = UIFont.systemFont(ofSize: 14, weight: .regular) + titleLabel.font = UIFont.systemFont(ofSize: 14, weight: .bold) + visibilityView.clipsToBounds = true + backButton.isExclusiveTouch = true + + backButton.setVisible(false, animated: false) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func awakeFromNib() { + super.awakeFromNib() + + headerLabel.font = UIFont.systemFont(ofSize: 14, weight: .regular) + titleLabel.font = UIFont.systemFont(ofSize: 14, weight: .bold) + visibilityView.clipsToBounds = true + backButton.isExclusiveTouch = true + + backButton.setVisible(false, animated: false) + } + + func apply(colorMode: ColorMode, animated: Bool) { + UIView.perform(animated: animated && self.isVisibleInWindow) { + self.backgroundColor = colorMode.tableBackgroundColor + + self.sectionContainerView.backgroundColor = colorMode.chartBackgroundColor + self.rangeView.backgroundColor = colorMode.chartBackgroundColor + self.visibilityView.backgroundColor = colorMode.chartBackgroundColor + + self.backButton.tintColor = colorMode.actionButtonColor + self.backButton.setTitleColor(colorMode.actionButtonColor, for: .normal) + + for separator in self.separators { + separator.backgroundColor = colorMode.tableSeparatorColor + } + } + + if rangeView.isVisibleInWindow || chartView.isVisibleInWindow { + chartView.loadDetailsViewIfNeeded() + chartView.apply(colorMode: colorMode, animated: animated && chartView.isVisibleInWindow) + controller.apply(colorMode: colorMode, animated: animated) + rangeView.apply(colorMode: colorMode, animated: animated && rangeView.isVisibleInWindow) + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval.random(in: 0...0.1)) { + self.chartView.loadDetailsViewIfNeeded() + self.controller.apply(colorMode: colorMode, animated: false) + self.chartView.apply(colorMode: colorMode, animated: false) + self.rangeView.apply(colorMode: colorMode, animated: false) + } + } + + self.titleLabel.setTextColor(colorMode.chartTitleColor, animated: animated && titleLabel.isVisibleInWindow) + self.headerLabel.setTextColor(colorMode.sectionTitleColor, animated: animated && headerLabel.isVisibleInWindow) + } + + @IBAction func didTapBackButton() { + controller.didTapZoomOut() + } + + func setBackButtonVisible(_ visible: Bool, animated: Bool) { + backButton.setVisible(visible, animated: animated) + layoutIfNeeded(animated: animated) + } + + func updateToolViews(animated: Bool) { + rangeView.setRange(controller.currentChartHorizontalRangeFraction, animated: animated) + rangeView.setRangePaging(enabled: controller.isChartRangePagingEnabled, + minimumSize: controller.minimumSelectedChartRange) + visibilityView.setVisible(controller.drawChartVisibity, animated: animated) + if controller.drawChartVisibity { + visibilityView.isExpanded = true + visibilityView.items = controller.actualChartsCollection.chartValues.map { value in + return ChartVisibilityItem(title: value.name, color: value.color) + } + visibilityView.setItemsSelection(controller.actualChartVisibility) + visibilityView.setNeedsLayout() + visibilityView.layoutIfNeeded() + } else { + visibilityView.isExpanded = false + } + superview?.superview?.layoutIfNeeded(animated: animated) + } + + override func layoutSubviews() { + super.layoutSubviews() + + let bounds = self.bounds + self.sectionContainerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: 350.0)) + self.chartView.frame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: 250.0)) + self.rangeView.frame = CGRect(origin: CGPoint(x: 0.0, y: 250.0), size: CGSize(width: bounds.width, height: 48.0)) + self.visibilityView.frame = CGRect(origin: CGPoint(x: 0.0, y: 308.0), size: CGSize(width: bounds.width, height: 122.0)) + } + + func setup(controller: BaseChartController, title: String) { + self.controller = controller + self.headerLabel.text = title + + // Chart + chartView.renderers = controller.mainChartRenderers + chartView.userDidSelectCoordinateClosure = { [unowned self] point in + self.controller.chartInteractionDidBegin(point: point) + } + chartView.userDidDeselectCoordinateClosure = { [unowned self] in + self.controller.chartInteractionDidEnd() + } + controller.cartViewBounds = { [unowned self] in + return self.chartView.bounds + } + controller.chartFrame = { [unowned self] in + return self.chartView.chartFrame + } + controller.setDetailsViewModel = { [unowned self] viewModel, animated in + self.chartView.setDetailsViewModel(viewModel: viewModel, animated: animated) + } + controller.setDetailsChartVisibleClosure = { [unowned self] visible, animated in + self.chartView.setDetailsChartVisible(visible, animated: animated) + } + controller.setDetailsViewPositionClosure = { [unowned self] position in + self.chartView.detailsViewPosition = position + } + controller.setChartTitleClosure = { [unowned self] title, animated in + self.titleLabel.setText(title, animated: animated) + } + controller.setBackButtonVisibilityClosure = { [unowned self] visible, animated in + self.setBackButtonVisible(visible, animated: animated) + } + controller.refreshChartToolsClosure = { [unowned self] animated in + self.updateToolViews(animated: animated) + } + + // Range view + rangeView.chartView.renderers = controller.navigationRenderers + rangeView.rangeDidChangeClosure = { range in + controller.updateChartRange(range) + } + rangeView.touchedOutsideClosure = { + controller.cancelChartInteraction() + } + controller.chartRangeUpdatedClosure = { [unowned self] (range, animated) in + self.rangeView.setRange(range, animated: animated) + } + controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in + self.rangeView.setRangePaging(enabled: isEnabled, minimumSize: pageSize) + } + + // Visibility view + visibilityView.selectionCallbackClosure = { [unowned self] visibility in + self.controller.updateChartsVisibility(visibility: visibility, animated: true) + } + + controller.initializeChart() + updateToolViews(animated: false) + } +} diff --git a/submodules/Charts/Sources/Chart Screen/ChartStackSection.xib b/submodules/Charts/Sources/Chart Screen/ChartStackSection.xib new file mode 100644 index 0000000000..fc883dd96c --- /dev/null +++ b/submodules/Charts/Sources/Chart Screen/ChartStackSection.xib @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/Charts/Sources/Chart Screen/ChartView.swift b/submodules/Charts/Sources/Chart Screen/ChartView.swift new file mode 100644 index 0000000000..39b93d9fa0 --- /dev/null +++ b/submodules/Charts/Sources/Chart Screen/ChartView.swift @@ -0,0 +1,158 @@ +// +// ChartView.swift +// GraphTest +// +// Created by Andrei Salavei on 4/7/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +public protocol ChartViewRenderer: class { + var containerViews: [UIView] { get set } + func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) +} + +class ChartView: UIView { + override init(frame: CGRect) { + super.init(frame: frame) + + setupView() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + setupView() + } + + var chartInsets: UIEdgeInsets = UIEdgeInsets(top: 40, left: 16, bottom: 35, right: 16) { + didSet { + setNeedsDisplay() + } + } + + var renderers: [ChartViewRenderer] = [] { + willSet { + renderers.forEach { $0.containerViews.removeAll(where: { $0 == self }) } + } + didSet { + renderers.forEach { $0.containerViews.append(self) } + setNeedsDisplay() + } + } + + var chartFrame: CGRect { + let chartBound = self.bounds + return CGRect(x: chartInsets.left, + y: chartInsets.top, + width: max(1, chartBound.width - chartInsets.left - chartInsets.right), + height: max(1, chartBound.height - chartInsets.top - chartInsets.bottom)) + } + + override func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext() else { return } + let chartBounds = self.bounds + let chartFrame = self.chartFrame + + for renderer in renderers { + renderer.render(context: context, bounds: chartBounds, chartFrame: chartFrame) + } + } + + var userDidSelectCoordinateClosure: ((CGPoint) -> Void)? + var userDidDeselectCoordinateClosure: (() -> Void)? + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + if let point = touches.first?.location(in: self) { + let fractionPoint = CGPoint(x: (point.x - chartFrame.origin.x) / chartFrame.width, + y: (point.y - chartFrame.origin.y) / chartFrame.height) + userDidSelectCoordinateClosure?(fractionPoint) + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + if let point = touches.first?.location(in: self) { + let fractionPoint = CGPoint(x: (point.x - chartFrame.origin.x) / chartFrame.width, + y: (point.y - chartFrame.origin.y) / chartFrame.height) + userDidSelectCoordinateClosure?(fractionPoint) + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + userDidDeselectCoordinateClosure?() + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent?) { + userDidDeselectCoordinateClosure?() + } + + // MARK: Details View + + private var detailsView: ChartDetailsView! + private var maxDetailsViewWidth: CGFloat = 0 + func loadDetailsViewIfNeeded() { + if detailsView == nil { + let detailsView = ChartDetailsView(frame: bounds) + addSubview(detailsView) + detailsView.alpha = 0 + self.detailsView = detailsView + } + } + + private var detailsTableTopOffset: CGFloat = 5 + private var detailsTableLeftOffset: CGFloat = 8 + private var isDetailsViewVisible: Bool = false + + var detailsViewPosition: CGFloat = 0 { + didSet { + loadDetailsViewIfNeeded() + let detailsViewSize = detailsView.intrinsicContentSize + maxDetailsViewWidth = max(maxDetailsViewWidth, detailsViewSize.width) + if maxDetailsViewWidth + detailsTableLeftOffset > detailsViewPosition { + detailsView.frame = CGRect(x: min(detailsViewPosition + detailsTableLeftOffset, bounds.width - maxDetailsViewWidth), + y: chartInsets.top + detailsTableTopOffset, + width: maxDetailsViewWidth, + height: detailsViewSize.height) + } else { + detailsView.frame = CGRect(x: detailsViewPosition - maxDetailsViewWidth - detailsTableLeftOffset, + y: chartInsets.top + detailsTableTopOffset, + width: maxDetailsViewWidth, + height: detailsViewSize.height) + } + } + } + + func setDetailsChartVisible(_ visible: Bool, animated: Bool) { + guard isDetailsViewVisible != visible else { + return + } + isDetailsViewVisible = visible + loadDetailsViewIfNeeded() + detailsView.setVisible(visible, animated: animated) + if !visible { + maxDetailsViewWidth = 0 + } + } + + func setDetailsViewModel(viewModel: ChartDetailsViewModel, animated: Bool) { + loadDetailsViewIfNeeded() + detailsView.setup(viewModel: viewModel, animated: animated) + UIView.perform(animated: animated, animations: { + let position = self.detailsViewPosition + self.detailsViewPosition = position + }) + } + + func setupView() { + backgroundColor = .clear + layer.drawsAsynchronously = true + } +} + + +extension ChartView: ColorModeContainer { + func apply(colorMode: ColorMode, animated: Bool) { + detailsView?.apply(colorMode: colorMode, animated: animated && (detailsView?.isVisibleInWindow ?? false)) + } +} diff --git a/submodules/Charts/Sources/Chart Screen/ChartVisibilityItemView.swift b/submodules/Charts/Sources/Chart Screen/ChartVisibilityItemView.swift new file mode 100644 index 0000000000..2476526f1a --- /dev/null +++ b/submodules/Charts/Sources/Chart Screen/ChartVisibilityItemView.swift @@ -0,0 +1,95 @@ +// +// ChartVisibilityItemCell.swift +// GraphTest +// +// Created by Andrei Salavei on 4/7/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class ChartVisibilityItemView: UIView { + static let textFont = UIFont.systemFont(ofSize: 14, weight: .medium) + + let checkButton: UIButton = UIButton(type: .system) + + override init(frame: CGRect) { + super.init(frame: frame) + + setupView() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + override func awakeFromNib() { + super.awakeFromNib() + setupView() + } + + func setupView() { + checkButton.frame = bounds + checkButton.titleLabel?.font = ChartVisibilityItemView.textFont + checkButton.layer.cornerRadius = 6 + checkButton.layer.masksToBounds = true + checkButton.addTarget(self, action: #selector(didTapButton), for: .touchUpInside) + let pressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(didRecognizedLongPress(recognizer:))) + pressRecognizer.cancelsTouchesInView = true + checkButton.addGestureRecognizer(pressRecognizer) + addSubview(checkButton) + } + + var tapClosure: (() -> Void)? + var longTapClosure: (() -> Void)? + + private func updateStyle(animated: Bool) { + guard let item = item else { + return + } + UIView.perform(animated: animated, animations: { + if self.isChecked { + self.checkButton.setTitleColor(.white, for: .normal) + self.checkButton.backgroundColor = item.color + self.checkButton.layer.borderColor = nil + self.checkButton.layer.borderWidth = 0 + self.checkButton.setTitle("✓ " + item.title, for: .normal) + } else { + self.checkButton.backgroundColor = .clear + self.checkButton.layer.borderColor = item.color.cgColor + self.checkButton.layer.borderWidth = 1 + self.checkButton.setTitleColor(item.color, for: .normal) + self.checkButton.setTitle(item.title, for: .normal) + } + + }) + } + + override func layoutSubviews() { + super.layoutSubviews() + + checkButton.frame = bounds + } + + @objc private func didTapButton() { + tapClosure?() + } + + @objc private func didRecognizedLongPress(recognizer: UIGestureRecognizer) { + if recognizer.state == .began { + longTapClosure?() + } + } + + var item: ChartVisibilityItem? = nil { + didSet { + updateStyle(animated: false) + } + } + + private(set) var isChecked: Bool = true + func setChecked(isChecked: Bool, animated: Bool) { + self.isChecked = isChecked + updateStyle(animated: true) + } +} diff --git a/submodules/Charts/Sources/Chart Screen/ChartVisibilityView.swift b/submodules/Charts/Sources/Chart Screen/ChartVisibilityView.swift new file mode 100644 index 0000000000..fda3c3901f --- /dev/null +++ b/submodules/Charts/Sources/Chart Screen/ChartVisibilityView.swift @@ -0,0 +1,147 @@ +// +// ChartVisibilityView.swift +// GraphTest +// +// Created by Andrei Salavei on 4/13/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +private enum Constants { + static let itemHeight: CGFloat = 30 + static let itemSpacing: CGFloat = 8 + static let labelTextApproxInsets: CGFloat = 40 + static let insets = UIEdgeInsets(top: 0, left: 16, bottom: 16, right: 16) +} + +struct ChartVisibilityItem { + var title: String + var color: UIColor +} + +class ChartVisibilityView: UIView { + var items: [ChartVisibilityItem] = [] { + didSet { + selectedItems = items.map { _ in true } + while selectionViews.count > selectedItems.count { + selectionViews.last?.removeFromSuperview() + selectionViews.removeLast() + } + while selectionViews.count < selectedItems.count { + let view = ChartVisibilityItemView(frame: bounds) + addSubview(view) + selectionViews.append(view) + } + + for (index, item) in items.enumerated() { + let view = selectionViews[index] + view.item = item + view.tapClosure = { [weak self] in + guard let self = self else { return } + self.setItemSelected(!self.selectedItems[index], at: index, animated: true) + self.notifyItemSelection() + } + + view.longTapClosure = { [weak self] in + guard let self = self else { return } + let hasSelectedItem = self.selectedItems.enumerated().contains(where: { $0.element && $0.offset != index }) + if hasSelectedItem { + for (itemIndex, _) in self.items.enumerated() { + self.setItemSelected(itemIndex == index, at: itemIndex, animated: true) + } + } else { + for (itemIndex, _) in self.items.enumerated() { + self.setItemSelected(true, at: itemIndex, animated: true) + } + } + self.notifyItemSelection() + } + } + } + } + + private (set) var selectedItems: [Bool] = [] + var isExpanded: Bool = true { + didSet { + invalidateIntrinsicContentSize() + setNeedsUpdateConstraints() + } + } + + private var selectionViews: [ChartVisibilityItemView] = [] + + private func generateItemsFrames(frame: CGRect) -> [CGRect] { + var previousPoint = CGPoint(x: Constants.insets.left, y: Constants.insets.top) + var frames: [CGRect] = [] + + for item in items { + let labelSize = (item.title as NSString).size(withAttributes: [.font: ChartVisibilityItemView.textFont]) + let width = (labelSize.width + Constants.labelTextApproxInsets).rounded(.up) + if previousPoint.x + width < (frame.width - Constants.insets.left - Constants.insets.right) { + frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight))) + } else if previousPoint.x <= Constants.insets.left { + frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight))) + } else { + previousPoint.y += Constants.itemHeight + Constants.itemSpacing + previousPoint.x = Constants.insets.left + frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight))) + } + previousPoint.x += width + Constants.itemSpacing + } + + return frames + } + + var selectionCallbackClosure: (([Bool]) -> Void)? + + func setItemSelected(_ selected: Bool, at index: Int, animated: Bool) { + self.selectedItems[index] = selected + self.selectionViews[index].setChecked(isChecked: selected, animated: animated) + } + + func setItemsSelection(_ selection: [Bool]) { + assert(selection.count == items.count) + self.selectedItems = selection + for (index, selected) in self.selectedItems.enumerated() { + selectionViews[index].setChecked(isChecked: selected, animated: false) + } + } + + private func notifyItemSelection() { + selectionCallbackClosure?(selectedItems) + } + + override func layoutSubviews() { + super.layoutSubviews() + + updateFrames() + } + + private func updateFrames() { + for (index, frame) in generateItemsFrames(frame: bounds).enumerated() { + selectionViews[index].frame = frame + } + } + + override var intrinsicContentSize: CGSize { + guard isExpanded else { + var size = self.bounds.size + size.height = 0 + return size + } + let frames = generateItemsFrames(frame: UIScreen.main.bounds) + guard let lastFrame = frames.last else { return .zero } + let size = CGSize(width: frame.width, height: lastFrame.maxY + Constants.insets.bottom) + return size + } +} + +extension ChartVisibilityView: ColorModeContainer { + func apply(colorMode: ColorMode, animated: Bool) { + UIView.perform(animated: animated) { + self.backgroundColor = colorMode.chartBackgroundColor + self.tintColor = colorMode.descriptionActionColor + } + } +} diff --git a/submodules/Charts/Sources/Chart Screen/ChartsDataLoader.swift b/submodules/Charts/Sources/Chart Screen/ChartsDataLoader.swift new file mode 100644 index 0000000000..085e4689bd --- /dev/null +++ b/submodules/Charts/Sources/Chart Screen/ChartsDataLoader.swift @@ -0,0 +1,51 @@ +// +// ChartsDataLoader.swift +// GraphTest +// +// Created by Andrei Salavei on 4/8/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import Foundation + +enum ChartsDataType: String { + case generalLines = "1" + case twoAxisLines = "2" + case stackedBars = "3" + case dailyBars = "4" + case percentPie = "5" +} + +private enum Constants { + static let overviewFilename = "overview.json" + static let dataDir = "data" +} + +class ChartsDataLoader { + static var documentDirectoryURL: URL { + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + let documentsDirectory = paths[0] + return documentsDirectory + } + + static func overviewData(type: ChartsDataType, extraCopiesCount: Int = 0, sync: Bool = false, success: @escaping (ChartsCollection) -> Void) { + let path = Bundle.main.bundleURL + .appendingPathComponent(Constants.dataDir) + .appendingPathComponent(type.rawValue) + .appendingPathComponent(Constants.overviewFilename) + ChartsDataManager().readChart(file: path, extraCopiesCount: extraCopiesCount, sync: sync, success: success, failure: { _ in }) + } + + static func detaildData(type: ChartsDataType, extraCopiesCount: Int = 0, date: Date, success: @escaping (ChartsCollection) -> Void, failure: @escaping (Error) -> Void) { + let dateComponents = Calendar.utc.dateComponents([.day, .month, .year], from: date) + let yearMonth = String(format: "%04d-%02d", dateComponents.year ?? 0, dateComponents.month ?? 0) + let day = String(format: "%02d.json", dateComponents.day ?? 0) + + let path = Bundle.main.bundleURL + .appendingPathComponent(Constants.dataDir) + .appendingPathComponent(type.rawValue) + .appendingPathComponent(yearMonth) + .appendingPathComponent(day) + ChartsDataManager().readChart(file: path, extraCopiesCount: extraCopiesCount, sync: false, success: success, failure: failure) + } +} diff --git a/submodules/Charts/Sources/Chart Screen/ChartsStackViewController.swift b/submodules/Charts/Sources/Chart Screen/ChartsStackViewController.swift new file mode 100644 index 0000000000..e8c22e6453 --- /dev/null +++ b/submodules/Charts/Sources/Chart Screen/ChartsStackViewController.swift @@ -0,0 +1,222 @@ +// +// ChartsStackViewController.swift +// GraphTest +// +// Created by Andrei Salavei on 4/13/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class ChartsStackViewController: UIViewController { + @IBOutlet private var stackView: UIStackView! + @IBOutlet private var scrollView: UIScrollView! + @IBOutlet private var psLabel: UILabel! + @IBOutlet private var ppsLabel: UILabel! + @IBOutlet private var animationButton: ChartVisibilityItemView! + + private var sections: [ChartStackSection] = [] + + private var colorMode: ColorMode = .night + private var colorModeButton: UIBarButtonItem! + private var performFastAnimation: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + + title = "Statistics" + colorModeButton = UIBarButtonItem(title: colorMode.switchTitle, style: .plain, target: self, action: #selector(didTapSwitchColorMode)) + navigationItem.rightBarButtonItem = colorModeButton + + apply(colorMode: colorMode, animated: false) + + self.navigationController?.navigationBar.barStyle = .black + self.navigationController?.navigationBar.isTranslucent = false + + self.view.isUserInteractionEnabled = false + animationButton.backgroundColor = .clear + animationButton.tapClosure = { [weak self] in + guard let self = self else { return } + self.setSlowAnimationEnabled(!self.animationButton.isChecked) + } + self.setSlowAnimationEnabled(false) + + loadChart1() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + DispatchQueue.main.async { + self.view.setNeedsUpdateConstraints() + self.view.setNeedsLayout() + } + } + + func loadChart1() { + ChartsDataLoader.overviewData(type: .generalLines, sync: true, success: { collection in + let generalLinesChartController = GeneralLinesChartController(chartsCollection: collection) + self.addSection(controller: generalLinesChartController, title: "FOLLOWERS") + generalLinesChartController.getDetailsData = { date, completion in + ChartsDataLoader.detaildData(type: .generalLines, date: date, success: { collection in + completion(collection) + }, failure: { error in + completion(nil) + }) + } + DispatchQueue.main.async { + self.loadChart2() + } + }) + } + + func loadChart2() { + ChartsDataLoader.overviewData(type: .twoAxisLines, success: { collection in + let twoAxisLinesChartController = TwoAxisLinesChartController(chartsCollection: collection) + self.addSection(controller: twoAxisLinesChartController, title: "INTERACTIONS") + twoAxisLinesChartController.getDetailsData = { date, completion in + ChartsDataLoader.detaildData(type: .twoAxisLines, date: date, success: { collection in + completion(collection) + }, failure: { error in + completion(nil) + }) + } + DispatchQueue.main.async { + self.loadChart3() + } + }) + } + + func loadChart3() { + ChartsDataLoader.overviewData(type: .stackedBars, success: { collection in + let stackedBarsChartController = StackedBarsChartController(chartsCollection: collection) + self.addSection(controller: stackedBarsChartController, title: "FRUITS") + stackedBarsChartController.getDetailsData = { date, completion in + ChartsDataLoader.detaildData(type: .stackedBars, date: date, success: { collection in + completion(collection) + }, failure: { error in + completion(nil) + }) + } + DispatchQueue.main.async { + self.loadChart4() + } + }) + } + + func loadChart4() { + ChartsDataLoader.overviewData(type: .dailyBars, success: { collection in + let dailyBarsChartController = DailyBarsChartController(chartsCollection: collection) + self.addSection(controller: dailyBarsChartController, title: "VIEWS") + dailyBarsChartController.getDetailsData = { date, completion in + ChartsDataLoader.detaildData(type: .dailyBars, date: date, success: { collection in + completion(collection) + }, failure: { error in + completion(nil) + }) + } + DispatchQueue.main.async { + self.loadChart5() + } + }) + } + + func loadChart5() { + ChartsDataLoader.overviewData(type: .percentPie, success: { collection in + let percentPieChartController = PercentPieChartController(chartsCollection: collection) + self.addSection(controller: percentPieChartController, title: "MORE FRUITS") + self.finalizeChartsLoading() + }) + } + + func setSlowAnimationEnabled(_ isEnabled: Bool) { + animationButton.setChecked(isChecked: isEnabled, animated: true) + if isEnabled { + TimeInterval.animationDurationMultipler = 5 + } else { + TimeInterval.animationDurationMultipler = 1 + } + } + + func finalizeChartsLoading() { + self.view.isUserInteractionEnabled = true + } + + func addSection(controller: BaseChartController, title: String) { + let section = Bundle.main.loadNibNamed("ChartStackSection", owner: nil, options: nil)?.first as! ChartStackSection + section.frame = UIScreen.main.bounds + section.layoutIfNeeded() + section.setup(controller: controller, title: title) + section.apply(colorMode: colorMode, animated: false) + stackView.addArrangedSubview(section) + sections.append(section) + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return (colorMode == .day) ? .default : .lightContent + } + + override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { + return .fade + } + + @objc private func didTapSwitchColorMode() { + self.colorMode = self.colorMode == .day ? .night : .day + apply(colorMode: self.colorMode, animated: !performFastAnimation) + colorModeButton.title = colorMode.switchTitle + } +} + +extension ChartsStackViewController: UIScrollViewDelegate { + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + performFastAnimation = decelerate + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + performFastAnimation = false + } +} + +extension ChartsStackViewController: ColorModeContainer { + func apply(colorMode: ColorMode, animated: Bool) { + + UIView.perform(animated: animated) { + self.psLabel.setTextColor(colorMode.sectionTitleColor, animated: animated && self.psLabel.isVisibleInWindow) + self.ppsLabel.setTextColor(colorMode.sectionTitleColor, animated: animated && self.ppsLabel.isVisibleInWindow) + self.animationButton.item = ChartVisibilityItem(title: "Enable slow animations", + color: colorMode.sectionTitleColor) + + self.view.backgroundColor = colorMode.tableBackgroundColor + + if (animated) { + let animation = CATransition() + animation.timingFunction = CAMediaTimingFunction.init(name: .linear) + animation.type = .fade + animation.duration = .defaultDuration + self.navigationController?.navigationBar.layer.add(animation, forKey: "kCATransitionColorFade") + } + + self.navigationController?.navigationBar.tintColor = colorMode.actionButtonColor + self.navigationController?.navigationBar.barTintColor = colorMode.chartBackgroundColor + self.navigationController?.navigationBar.titleTextAttributes = [.font: UIFont.systemFont(ofSize: 17, weight: .medium), + .foregroundColor: colorMode.viewTintColor] + self.view.layoutIfNeeded() + } + self.setNeedsStatusBarAppearanceUpdate() + + for section in sections { + section.apply(colorMode: colorMode, animated: animated && section.isVisibleInWindow) + } + } +} + +extension ColorMode { + var switchTitle: String { + switch self { + case .day: + return "Night Mode" + case .night: + return "Day Mode" + } + } +} diff --git a/submodules/Charts/Sources/Chart Screen/RangeChartView.swift b/submodules/Charts/Sources/Chart Screen/RangeChartView.swift new file mode 100644 index 0000000000..91089c1de8 --- /dev/null +++ b/submodules/Charts/Sources/Chart Screen/RangeChartView.swift @@ -0,0 +1,291 @@ +// +// RangeChartView.swift +// GraphTest +// +// Created by Andrei Salavei on 3/11/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// +import UIKit + +private enum Constants { + static let cropIndocatorLineWidth: CGFloat = 1 + static let markerSelectionRange: CGFloat = 25 + static let defaultMinimumRangeDistance: CGFloat = 0.05 + static let titntAreaWidth: CGFloat = 10 + static let horizontalContentMargin: CGFloat = 16 + static let cornerRadius: CGFloat = 5 +} + +class RangeChartView: UIControl { + private enum Marker { + case lower + case upper + case center + } + public var lowerBound: CGFloat = 0 { + didSet { + setNeedsLayout() + } + } + public var upperBound: CGFloat = 1 { + didSet { + setNeedsLayout() + } + } + public var selectionColor: UIColor = .blue + public var defaultColor: UIColor = .lightGray + + public var minimumRangeDistance: CGFloat = Constants.defaultMinimumRangeDistance + + private let lowerBoundTintView = UIView() + private let upperBoundTintView = UIView() + private let cropFrameView = UIImageView() + + private var selectedMarker: Marker? + private var selectedMarkerHorizontalOffet: CGFloat = 0 + private var isBoundCropHighlighted: Bool = false + private var isRangePagingEnabled: Bool = false + + public let chartView = ChartView() + + override init(frame: CGRect) { + super.init(frame: frame) + + layoutMargins = UIEdgeInsets(top: Constants.cropIndocatorLineWidth, + left: Constants.horizontalContentMargin, + bottom: Constants.cropIndocatorLineWidth, + right: Constants.horizontalContentMargin) + + self.setup() + } + + func setup() { + isMultipleTouchEnabled = false + + chartView.chartInsets = .zero + chartView.backgroundColor = .clear + + addSubview(chartView) + addSubview(lowerBoundTintView) + addSubview(upperBoundTintView) + addSubview(cropFrameView) + cropFrameView.isUserInteractionEnabled = false + chartView.isUserInteractionEnabled = false + lowerBoundTintView.isUserInteractionEnabled = false + upperBoundTintView.isUserInteractionEnabled = false + + chartView.layer.cornerRadius = 5 + upperBoundTintView.layer.cornerRadius = 5 + lowerBoundTintView.layer.cornerRadius = 5 + + chartView.layer.masksToBounds = true + upperBoundTintView.layer.masksToBounds = true + lowerBoundTintView.layer.masksToBounds = true + + layoutViews() + } + + override func awakeFromNib() { + super.awakeFromNib() + + self.setup() + } + + public var rangeDidChangeClosure: ((ClosedRange) -> Void)? + public var touchedOutsideClosure: (() -> Void)? + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + func setRangePaging(enabled: Bool, minimumSize: CGFloat) { + isRangePagingEnabled = enabled + minimumRangeDistance = minimumSize + } + + func setRange(_ range: ClosedRange, animated: Bool) { + UIView.perform(animated: animated) { + self.lowerBound = range.lowerBound + self.upperBound = range.upperBound + self.layoutIfNeeded() + } + } + + override func layoutSubviews() { + super.layoutSubviews() + + layoutViews() + } + + override var isEnabled: Bool { + get { + return super.isEnabled + } + set { + if newValue == false { + selectedMarker = nil + } + super.isEnabled = newValue + } + } + + // MARK: - Touches + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + guard isEnabled else { return } + guard let point = touches.first?.location(in: self) else { return } + + if abs(locationInView(for: upperBound) - point.x + Constants.markerSelectionRange / 2) < Constants.markerSelectionRange { + selectedMarker = .upper + selectedMarkerHorizontalOffet = point.x - locationInView(for: upperBound) + isBoundCropHighlighted = true + } else if abs(locationInView(for: lowerBound) - point.x - Constants.markerSelectionRange / 2) < Constants.markerSelectionRange { + selectedMarker = .lower + selectedMarkerHorizontalOffet = point.x - locationInView(for: lowerBound) + isBoundCropHighlighted = true + } else if point.x > locationInView(for: lowerBound) && point.x < locationInView(for: upperBound) { + selectedMarker = .center + selectedMarkerHorizontalOffet = point.x - locationInView(for: lowerBound) + isBoundCropHighlighted = true + } else { + selectedMarker = nil + return + } + + sendActions(for: .touchDown) + } + + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + guard isEnabled else { return } + guard let selectedMarker = selectedMarker else { return } + guard let point = touches.first?.location(in: self) else { return } + + let horizontalPosition = point.x - selectedMarkerHorizontalOffet + let fraction = fractionFor(offsetX: horizontalPosition) + updateMarkerOffset(selectedMarker, fraction: fraction) + + sendActions(for: .valueChanged) + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + guard isEnabled else { return } + guard let selectedMarker = selectedMarker else { + touchedOutsideClosure?() + return + } + guard let point = touches.first?.location(in: self) else { return } + + let horizontalPosition = point.x - selectedMarkerHorizontalOffet + let fraction = fractionFor(offsetX: horizontalPosition) + updateMarkerOffset(selectedMarker, fraction: fraction) + + self.selectedMarker = nil + self.isBoundCropHighlighted = false + if bounds.contains(point) { + sendActions(for: .touchUpInside) + } else { + sendActions(for: .touchUpOutside) + } + rangeDidChangeClosure?(lowerBound...upperBound) + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent?) { + self.selectedMarker = nil + self.isBoundCropHighlighted = false + sendActions(for: .touchCancel) + } +} + +private extension RangeChartView { + var contentFrame: CGRect { + return CGRect(x: layoutMargins.right, + y: layoutMargins.top, + width: (bounds.width - layoutMargins.right - layoutMargins.left), + height: bounds.height - layoutMargins.top - layoutMargins.bottom) + } + + func locationInView(for fraction: CGFloat) -> CGFloat { + return contentFrame.minX + contentFrame.width * fraction + } + + func locationInView(for fraction: Double) -> CGFloat { + return locationInView(for: CGFloat(fraction)) + } + + func fractionFor(offsetX: CGFloat) -> CGFloat { + guard contentFrame.width > 0 else { + return 0 + } + + return crop(0, CGFloat((offsetX - contentFrame.minX ) / contentFrame.width), 1) + } + + private func updateMarkerOffset(_ marker: Marker, fraction: CGFloat, notifyDelegate: Bool = true) { + let fractionToCount: CGFloat + if isRangePagingEnabled { + guard let minValue = stride(from: CGFloat(0.0), through: CGFloat(1.0), by: minimumRangeDistance).min(by: { abs($0 - fraction) < abs($1 - fraction) }) else { return } + fractionToCount = minValue + } else { + fractionToCount = fraction + } + + switch marker { + case .lower: + lowerBound = min(fractionToCount, upperBound - minimumRangeDistance) + case .upper: + upperBound = max(fractionToCount, lowerBound + minimumRangeDistance) + case .center: + let distance = upperBound - lowerBound + lowerBound = max(0, min(fractionToCount, 1 - distance)) + upperBound = lowerBound + distance + } + if notifyDelegate { + rangeDidChangeClosure?(lowerBound...upperBound) + } + UIView.animate(withDuration: isRangePagingEnabled ? 0.1 : 0) { + self.layoutIfNeeded() + } + } + + // MARK: - Layout + + func layoutViews() { + cropFrameView.frame = CGRect(x: locationInView(for: lowerBound), + y: contentFrame.minY - Constants.cropIndocatorLineWidth, + width: locationInView(for: upperBound) - locationInView(for: lowerBound), + height: contentFrame.height + Constants.cropIndocatorLineWidth * 2) + + if chartView.frame != contentFrame { + chartView.frame = contentFrame + } + + lowerBoundTintView.frame = CGRect(x: contentFrame.minX, + y: contentFrame.minY, + width: max(0, locationInView(for: lowerBound) - contentFrame.minX + Constants.titntAreaWidth), + height: contentFrame.height) + + upperBoundTintView.frame = CGRect(x: locationInView(for: upperBound) - Constants.titntAreaWidth, + y: contentFrame.minY, + width: max(0, contentFrame.maxX - locationInView(for: upperBound) + Constants.titntAreaWidth), + height: contentFrame.height) + } +} + +extension RangeChartView: ColorModeContainer { + func apply(colorMode: ColorMode, animated: Bool) { + let colusre = { + self.lowerBoundTintView.backgroundColor = colorMode.rangeViewTintColor + self.upperBoundTintView.backgroundColor = colorMode.rangeViewTintColor + } + + self.cropFrameView.setImage(colorMode.rangeCropImage, animated: animated) + + // self.chartView.apply(colorMode: colorMode, animated: animated) + + if animated { + UIView.animate(withDuration: .defaultDuration, animations: colusre) + } else { + colusre() + } + } +} diff --git a/submodules/Charts/Sources/ChartNode.swift b/submodules/Charts/Sources/ChartNode.swift new file mode 100644 index 0000000000..b6c6da436c --- /dev/null +++ b/submodules/Charts/Sources/ChartNode.swift @@ -0,0 +1,48 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import AppBundle + +public final class ChartNode: ASDisplayNode { + private var chartView: ChartStackSection { + return self.view as! ChartStackSection + } + + public override init() { + super.init() + + self.setViewBlock({ + return ChartStackSection() + }) + } + + public override func didLoad() { + super.didLoad() + + self.view.disablesInteractiveTransitionGestureRecognizer = true + } + + public func setup(_ data: String, bar: Bool = false) { + if let data = data.data(using: .utf8) { + ChartsDataManager().readChart(data: data, extraCopiesCount: 0, sync: true, success: { [weak self] collection in + let controller: BaseChartController + if bar { + controller = DailyBarsChartController(chartsCollection: collection) + } else { + controller = GeneralLinesChartController(chartsCollection: collection) + } + if let strongSelf = self { + strongSelf.chartView.setup(controller: controller, title: "") + strongSelf.chartView.apply(colorMode: .day, animated: false) + } + }) { error in + + } + } + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/submodules/Charts/Sources/Charts Reader/ChartsCollection.swift b/submodules/Charts/Sources/Charts Reader/ChartsCollection.swift new file mode 100644 index 0000000000..f3ecd8215d --- /dev/null +++ b/submodules/Charts/Sources/Charts Reader/ChartsCollection.swift @@ -0,0 +1,91 @@ +// +// ChardData.swift +// GraphTest +// +// Created by Andrei Salavei on 3/11/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import Foundation +import UIKit + +struct ChartsCollection { + struct Chart { + var color: UIColor + var name: String + var values: [Double] + } + + var axisValues: [Date] + var chartValues: [Chart] + + static let blank = ChartsCollection(axisValues: [], chartValues: []) + var isBlank: Bool { + return axisValues.isEmpty || chartValues.isEmpty + } +} + +extension ChartsCollection { + public init(from decodedData: [String: Any]) throws { + guard let columns = decodedData["columns"] as? [[Any]] else { + throw ChartsError.generalConversion("Unable to get columns from: \(decodedData)") + } + guard let types = decodedData["types"] as? [String: String] else { + throw ChartsError.generalConversion("Unable to get types from: \(decodedData)") + } + guard let names = decodedData["names"] as? [String: String] else { + throw ChartsError.generalConversion("Unable to get names from: \(decodedData)") + } + guard let colors = decodedData["colors"] as? [String: String] else { + throw ChartsError.generalConversion("Unable to get colors from: \(decodedData)") + } + +// chart.colors – Color for each variable in 6-hex-digit format (e.g. "#AAAAAA"). +// chart.names – Name for each variable. +// chart.percentage – true for percentage based values. +// chart.stacked – true for values stacking on top of each other. +// chart.y_scaled – true for charts with 2 Y axes. + + var axixValuesToSetup: [Date] = [] + var chartToSetup: [Chart] = [] + for column in columns { + guard let columnId = column.first as? String else { + throw ChartsError.generalConversion("Unable to get column name from: \(column)") + } + guard let typeString = types[columnId], let type = ColumnType(rawValue: typeString) else { + throw ChartsError.generalConversion("Unable to get column type from: \(types) - \(columnId)") + } + switch type { + case .axix: + axixValuesToSetup = try column.dropFirst().map { Date(timeIntervalSince1970: try Convert.doubleFrom($0) / 1000) } + case .chart, .bar, .area: + guard let colorString = colors[columnId], + let color = UIColor(hexString: colorString) else { + throw ChartsError.generalConversion("Unable to get color name from: \(colors) - \(columnId)") + } + guard let name = names[columnId] else { + throw ChartsError.generalConversion("Unable to get column name from: \(names) - \(columnId)") + } + let values = try column.dropFirst().map { try Convert.doubleFrom($0) } + chartToSetup.append(Chart(color: color, + name: name, + values: values)) + } + } + + guard axixValuesToSetup.isEmpty == false, + chartToSetup.isEmpty == false, + chartToSetup.firstIndex(where: { $0.values.count != axixValuesToSetup.count }) == nil else { + throw ChartsError.generalConversion("Saniazing: Invalid number of items: \(axixValuesToSetup), \(chartToSetup)") + } + self.axisValues = axixValuesToSetup + self.chartValues = chartToSetup + } +} + +private enum ColumnType: String { + case axix = "x" + case chart = "line" + case area = "area" + case bar = "bar" +} diff --git a/submodules/Charts/Sources/Charts Reader/ChartsDataManager.swift b/submodules/Charts/Sources/Charts Reader/ChartsDataManager.swift new file mode 100644 index 0000000000..1a32f9bcee --- /dev/null +++ b/submodules/Charts/Sources/Charts Reader/ChartsDataManager.swift @@ -0,0 +1,191 @@ +// +// ChartsDataManager.swift +// GraphTest +// +// Created by Andrei Salavei on 3/11/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import Foundation + +class ChartsDataManager { + func readChart(item: [String: Any], extraCopiesCount: Int = 0, sync: Bool, success: @escaping (ChartsCollection) -> Void, failure: @escaping (Error) -> Void) { + let workItem: (() -> Void) = { + do { + var collection = try ChartsCollection(from: item) + for _ in 0.. Void, failure: @escaping (Error) -> Void) { + let workItem: (() -> Void) = { + do { + let decoded = try JSONSerialization.jsonObject(with: data, options: []) + guard let item = decoded as? [String: Any] else { + throw ChartsError.invalidJson + } + var collection = try ChartsCollection(from: item) + for _ in 0.. Void, failure: @escaping (Error) -> Void) { + let workItem: (() -> Void) = { + do { + let data = try Data(contentsOf: file) + let decoded = try JSONSerialization.jsonObject(with: data, options: []) + guard let item = decoded as? [String: Any] else { + throw ChartsError.invalidJson + } + var collection = try ChartsCollection(from: item) + for _ in 0.. Void, failure: @escaping (Error) -> Void) { + let workItem: (() -> Void) = { + do { + let data = try Data(contentsOf: file) + let decoded = try JSONSerialization.jsonObject(with: data, options: []) + guard let items = decoded as? [[String: Any]] else { + throw ChartsError.invalidJson + } + var collections = try items.map { try ChartsCollection(from: $0) } + for _ in 0.. Double { + guard let double = try doubleFrom(value, lenientCast: false) else { + throw ChartsError.generalConversion("Unable to cast \(String(describing: value)) to \(Double.self)") + } + return double + } + + public static func doubleFrom(_ value: Any?, lenientCast: Bool = false) throws -> Double? { + guard let value = value else { + return nil + } + if let intValue = value as? Int { + return Double(intValue) + } else if let floatValue = value as? Float { + return Double(floatValue) + } else if let int64Value = value as? Int64 { + return Double(int64Value) + } else if let intValue = value as? Int { + return Double(intValue) + } else if let stringValue = value as? String { + if let doubleValue = Double(stringValue) { + return doubleValue + } + } + if lenientCast { + return nil + } else { + throw ChartsError.generalConversion("Unable to cast \(String(describing: value)) to \(Double.self)") + } + } +} diff --git a/submodules/Charts/Sources/Charts.h b/submodules/Charts/Sources/Charts.h new file mode 100644 index 0000000000..89573b3f16 --- /dev/null +++ b/submodules/Charts/Sources/Charts.h @@ -0,0 +1,19 @@ +// +// StatisticsUI.h +// StatisticsUI +// +// Created by Peter on 8/13/19. +// Copyright © 2019 Telegram Messenger LLP. All rights reserved. +// + +#import + +//! Project version number for StatisticsUI. +FOUNDATION_EXPORT double StatisticsUIVersionNumber; + +//! Project version string for StatisticsUI. +FOUNDATION_EXPORT const unsigned char StatisticsUIVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/submodules/Charts/Sources/Charts/Controllers/BaseChartController.swift b/submodules/Charts/Sources/Charts/Controllers/BaseChartController.swift new file mode 100644 index 0000000000..7f91e50ea5 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Controllers/BaseChartController.swift @@ -0,0 +1,166 @@ +// +// BaseChartController.swift +// GraphTest +// +// Created by Andrei Salavei on 4/7/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +enum BaseConstants { + static let defaultRange: ClosedRange = 0...1 + static let minimumAxisYLabelsDistance: CGFloat = 90 + static let monthDayDateFormatter = DateFormatter.utc(format: "MMM d") + static let timeDateFormatter = DateFormatter.utc(format: "HH:mm") + static let headerFullRangeFormatter: DateFormatter = { + let formatter = DateFormatter.utc() + formatter.calendar = Calendar.utc + formatter.dateStyle = .long + return formatter + }() + static let headerMediumRangeFormatter: DateFormatter = { + let formatter = DateFormatter.utc() + formatter.dateStyle = .medium + return formatter + }() + static let headerFullZoomedFormatter: DateFormatter = { + let formatter = DateFormatter.utc() + formatter.dateStyle = .full + return formatter + }() + + static let verticalBaseAnchors: [CGFloat] = [8, 5, 2.5, 2, 1] + static let defaultVerticalBaseAnchor: CGFloat = 1 + + static let mainChartLineWidth: CGFloat = 2 + static let previewChartLineWidth: CGFloat = 1 + + static let previewLinesChartOptimizationLevel: CGFloat = 1.5 + static let linesChartOptimizationLevel: CGFloat = 1.0 + static let barsChartOptimizationLevel: CGFloat = 0.75 + + static let defaultRangePresetLength = TimeInterval.day * 60 + + static let chartNumberFormatter: ScalesNumberFormatter = { + let numberFormatter = ScalesNumberFormatter() + numberFormatter.allowsFloats = true + numberFormatter.numberStyle = .decimal + numberFormatter.usesGroupingSeparator = true + numberFormatter.groupingSeparator = " " + numberFormatter.minimumIntegerDigits = 1 + numberFormatter.minimumFractionDigits = 0 + numberFormatter.maximumFractionDigits = 2 + return numberFormatter + }() + + static let detailsNumberFormatter: NumberFormatter = { + let detailsNumberFormatter = NumberFormatter() + detailsNumberFormatter.allowsFloats = false + detailsNumberFormatter.numberStyle = .decimal + detailsNumberFormatter.usesGroupingSeparator = true + detailsNumberFormatter.groupingSeparator = " " + return detailsNumberFormatter + }() +} + +class BaseChartController: ColorModeContainer { + //let performanceRenderer = PerformanceRenderer() + var initialChartsCollection: ChartsCollection + var isZoomed: Bool = false + + var chartTitle: String = "" + + init(chartsCollection: ChartsCollection) { + self.initialChartsCollection = chartsCollection + } + + var mainChartRenderers: [ChartViewRenderer] { + fatalError("Abstract") + } + + var navigationRenderers: [ChartViewRenderer] { + fatalError("Abstract") + } + + var cartViewBounds: (() -> CGRect) = { fatalError() } + var chartFrame: (() -> CGRect) = { fatalError() } + + func initializeChart() { + fatalError("Abstract") + } + + func chartInteractionDidBegin(point: CGPoint) { + fatalError("Abstract") + } + + func chartInteractionDidEnd() { + fatalError("Abstract") + } + + func cancelChartInteraction() { + fatalError("Abstract") + } + + func didTapZoomOut() { + fatalError("Abstract") + } + + func updateChartsVisibility(visibility: [Bool], animated: Bool) { + fatalError("Abstract") + } + + var currentHorizontalRange: ClosedRange { + fatalError("Abstract") + } + + var isChartRangePagingEnabled: Bool = false + var minimumSelectedChartRange: CGFloat = 0.05 + var chartRangePagingClosure: ((Bool, CGFloat) -> Void)? // isEnabled, PageSize + func setChartRangePagingEnabled(isEnabled: Bool, minimumSelectionSize: CGFloat) { + isChartRangePagingEnabled = isEnabled + minimumSelectedChartRange = minimumSelectionSize + chartRangePagingClosure?(isChartRangePagingEnabled, minimumSelectedChartRange) + } + + var chartRangeUpdatedClosure: ((ClosedRange, Bool) -> Void)? + var currentChartHorizontalRangeFraction: ClosedRange { + fatalError("Abstract") + } + + func updateChartRange(_ rangeFraction: ClosedRange) { + fatalError("Abstract") + } + + var actualChartVisibility: [Bool] { + fatalError("Abstract") + } + + var actualChartsCollection: ChartsCollection { + fatalError("Abstract") + } + + var drawChartVisibity: Bool { + return true + } + + var drawChartNavigation: Bool { + return true + } + + var setDetailsViewPositionClosure: ((CGFloat) -> Void)? + var setDetailsChartVisibleClosure: ((Bool, Bool) -> Void)? + var setDetailsViewModel: ((ChartDetailsViewModel, Bool) -> Void)? + var getDetailsData: ((Date, @escaping (ChartsCollection?) -> Void) -> Void)? + var setChartTitleClosure: ((String, Bool) -> Void)? + var setBackButtonVisibilityClosure: ((Bool, Bool) -> Void)? + var refreshChartToolsClosure: ((Bool) -> Void)? + + func didTapZoomIn(date: Date) { + fatalError("Abstract") + } + + func apply(colorMode: ColorMode, animated: Bool) { + + } +} diff --git a/submodules/Charts/Sources/Charts/Controllers/GeneralChartComponentController.swift b/submodules/Charts/Sources/Charts/Controllers/GeneralChartComponentController.swift new file mode 100644 index 0000000000..284be92d3b --- /dev/null +++ b/submodules/Charts/Sources/Charts/Controllers/GeneralChartComponentController.swift @@ -0,0 +1,328 @@ +// +// GeneralChartComponentController.swift +// GraphTest +// +// Created by Andrei Salavei on 4/11/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +enum GeneralChartComponentConstants { + static let defaultInitialRangeLength = CGFloat(TimeInterval.day * 60) + static let defaultZoomedRangeLength = CGFloat(TimeInterval.day) +} + +class GeneralChartComponentController: ColorModeContainer { + var chartsCollection: ChartsCollection = ChartsCollection.blank + var chartVisibility: [Bool] = [] + var lastChartInteractionPoint: CGPoint = .zero + var isChartInteractionBegun: Bool = false + var isChartInteracting: Bool = false + let isZoomed: Bool + + var colorMode: ColorMode = .day + var totalHorizontalRange: ClosedRange = BaseConstants.defaultRange + var totalVerticalRange: ClosedRange = BaseConstants.defaultRange + var initialHorizontalRange: ClosedRange = BaseConstants.defaultRange + var initialVerticalRange: ClosedRange = BaseConstants.defaultRange + + var cartViewBounds: (() -> CGRect) = { fatalError() } + var chartFrame: (() -> CGRect) = { fatalError() } + + init(isZoomed: Bool) { + self.isZoomed = isZoomed + } + + func initialize(chartsCollection: ChartsCollection, + initialDate: Date, + totalHorizontalRange: ClosedRange, + totalVerticalRange: ClosedRange) { + self.chartsCollection = chartsCollection + self.chartVisibility = Array(repeating: true, count: chartsCollection.chartValues.count) + self.totalHorizontalRange = totalHorizontalRange + self.totalVerticalRange = totalVerticalRange + self.initialHorizontalRange = totalHorizontalRange + self.initialVerticalRange = totalVerticalRange + + didLoad() + setupInitialChartRange(initialDate: initialDate) + } + + func didLoad() { + hideDetailsView(animated: false) + } + func willAppear(animated: Bool) { + updateChartRangeTitle(animated: animated) + setupChartRangePaging() + } + func willDisappear(animated: Bool) { + } + + func setupInitialChartRange(initialDate: Date) { + guard let first = chartsCollection.axisValues.first?.timeIntervalSince1970, + let last = chartsCollection.axisValues.last?.timeIntervalSince1970 else { return } + + let rangeStart = CGFloat(first) + let rangeEnd = CGFloat(last) + + if isZoomed { + let initalDate = CGFloat(initialDate.timeIntervalSince1970) + + initialHorizontalRange = max(initalDate, rangeStart)...min(initalDate + GeneralChartComponentConstants.defaultZoomedRangeLength, rangeEnd) + initialVerticalRange = totalVerticalRange + } else { + initialHorizontalRange = max(rangeStart, rangeEnd - GeneralChartComponentConstants.defaultInitialRangeLength)...rangeEnd + initialVerticalRange = totalVerticalRange + } + } + func setupChartRangePaging() { + chartRangePagingClosure?(false, 0.05) + } + + var visibleHorizontalMainChartRange: ClosedRange { + return currentMainRangeRenderer.verticalRange.current + } + var visibleVerticalMainChartRange: ClosedRange { + return currentMainRangeRenderer.verticalRange.current + } + var currentHorizontalMainChartRange: ClosedRange { + return currentMainRangeRenderer.horizontalRange.end + } + var currentVerticalMainChartRange: ClosedRange { + return currentMainRangeRenderer.verticalRange.end + } + var currentMainRangeRenderer: BaseChartRenderer { + fatalError("Abstract") + } + + var visiblePreviewHorizontalRange: ClosedRange { + return currentPreviewRangeRenderer.verticalRange.current + } + var visiblePreviewVerticalRange: ClosedRange { + return currentPreviewRangeRenderer.verticalRange.current + } + var currentPreviewHorizontalRange: ClosedRange { + return currentPreviewRangeRenderer.horizontalRange.end + } + var currentPreviewVerticalRange: ClosedRange { + return currentPreviewRangeRenderer.verticalRange.end + } + var currentPreviewRangeRenderer: BaseChartRenderer { + fatalError("Abstract") + } + + var mainChartRenderers: [ChartViewRenderer] { + fatalError("Abstract") + } + var previewRenderers: [ChartViewRenderer] { + fatalError("Abstract") + } + + func updateChartsVisibility(visibility: [Bool], animated: Bool) { + self.chartVisibility = visibility + if isChartInteractionBegun { + chartInteractionDidBegin(point: lastChartInteractionPoint) + } + } + + var currentChartHorizontalRangeFraction: ClosedRange { + let lowerPercent = (currentHorizontalMainChartRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance + let upperPercent = (currentHorizontalMainChartRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance + return lowerPercent...upperPercent + } + + func chartRangeFractionDidUpdated(_ rangeFraction: ClosedRange) { + let horizontalRange = ClosedRange(uncheckedBounds: + (lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance, + upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance)) + chartRangeDidUpdated(horizontalRange) + updateChartRangeTitle(animated: true) + } + + func chartRangeDidUpdated(_ updatedRange: ClosedRange) { + hideDetailsView(animated: true) + + if isChartInteractionBegun { + chartInteractionDidBegin(point: lastChartInteractionPoint) + } + } + + // MARK: - Details & Interaction + func findClosestDateTo(dateToFind: Date) -> (Date, Int)? { + guard chartsCollection.axisValues.count > 0 else { return nil } + var closestDate = chartsCollection.axisValues[0] + var minIndex = 0 + for (index, date) in chartsCollection.axisValues.enumerated() { + if abs(dateToFind.timeIntervalSince(date)) < abs(dateToFind.timeIntervalSince(closestDate)) { + closestDate = date + minIndex = index + } + } + return (closestDate, minIndex) + } + + func chartInteractionDidBegin(point: CGPoint) { + let chartFrame = self.chartFrame() + guard chartFrame.width > 0 else { return } + let horizontalRange = currentHorizontalMainChartRange + let dateToFind = Date(timeIntervalSince1970: TimeInterval(horizontalRange.distance * point.x + horizontalRange.lowerBound)) + guard let (closestDate, minIndex) = findClosestDateTo(dateToFind: dateToFind) else { return } + + let chartWasInteracting = isChartInteractionBegun + lastChartInteractionPoint = point + isChartInteractionBegun = true + isChartInteracting = true + + let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970) + let detailsViewPosition = (chartValue - horizontalRange.lowerBound) / horizontalRange.distance * chartFrame.width + chartFrame.minX + showDetailsView(at: chartValue, detailsViewPosition: detailsViewPosition, dataIndex: minIndex, date: closestDate, animted: chartWasInteracting) + } + + func showDetailsView(at chartPosition: CGFloat, detailsViewPosition: CGFloat, dataIndex: Int, date: Date, animted: Bool) { + setDetailsViewModel?(chartDetailsViewModel(closestDate: date, pointIndex: dataIndex), animted) + setDetailsChartVisibleClosure?(true, true) + setDetailsViewPositionClosure?(detailsViewPosition) + } + + func chartInteractionDidEnd() { + isChartInteracting = false + } + + func hideDetailsView(animated: Bool) { + isChartInteractionBegun = false + setDetailsChartVisibleClosure?(false, animated) + } + + var visibleDetailsChartValues: [ChartsCollection.Chart] { + let visibleCharts: [ChartsCollection.Chart] = chartVisibility.enumerated().compactMap { args in + args.element ? chartsCollection.chartValues[args.offset] : nil + } + return visibleCharts + } + + var updatePreviewRangeClosure: ((ClosedRange, Bool) -> Void)? + var zoomInOnDateClosure: ((Date) -> Void)? + var setChartTitleClosure: ((String, Bool) -> Void)? + var setDetailsViewPositionClosure: ((CGFloat) -> Void)? + var setDetailsChartVisibleClosure: ((Bool, Bool) -> Void)? + var setDetailsViewModel: ((ChartDetailsViewModel, Bool) -> Void)? + var chartRangePagingClosure: ((Bool, CGFloat) -> Void)? // isEnabled, PageSize + + func apply(colorMode: ColorMode, animated: Bool) { + self.colorMode = colorMode + } + +// MARK: - Helpers + var prevoiusHorizontalStrideInterval: Int = -1 + func updateHorizontalLimitLabels(horizontalScalesRenderer: HorizontalScalesRenderer, + horizontalRange: ClosedRange, + scaleType: ChartScaleType, + forceUpdate: Bool, + animated: Bool) { + let scaleTimeInterval: TimeInterval + if chartsCollection.axisValues.count >= 1 { + scaleTimeInterval = chartsCollection.axisValues[1].timeIntervalSince1970 - chartsCollection.axisValues[0].timeIntervalSince1970 + } else { + scaleTimeInterval = scaleType.timeInterval + } + + let numberOfItems = horizontalRange.distance / CGFloat(scaleTimeInterval) + let maximumNumberOfItems = chartFrame().width / scaleType.minimumAxisXDistance + let tempStride = max(1, Int((numberOfItems / maximumNumberOfItems).rounded(.up))) + var strideInterval = 1 + while strideInterval < tempStride { + strideInterval *= 2 + } + + if forceUpdate || (strideInterval != prevoiusHorizontalStrideInterval && strideInterval > 0) { + var labels: [LinesChartLabel] = [] + for index in stride(from: chartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() { + let date = chartsCollection.axisValues[index] + labels.append(LinesChartLabel(value: CGFloat(date.timeIntervalSince1970), + text: scaleType.dateFormatter.string(from: date))) + } + prevoiusHorizontalStrideInterval = strideInterval + horizontalScalesRenderer.setup(labels: labels, animated: animated) + } + } + + func verticalLimitsLabels(verticalRange: ClosedRange) -> (ClosedRange, [LinesChartLabel]) { + let ditance = verticalRange.distance + let chartHeight = chartFrame().height + + guard ditance > 0, chartHeight > 0 else { return (BaseConstants.defaultRange, []) } + + let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance) + + var numberOfOffsetsPerItem = ditance / approximateNumberOfChartValues + var multiplier: CGFloat = 1.0 + while numberOfOffsetsPerItem > 10 { + numberOfOffsetsPerItem /= 10 + multiplier *= 10 + } + var dividor: CGFloat = 1.0 + var maximumNumberOfDecimals = 2 + while numberOfOffsetsPerItem < 1 { + numberOfOffsetsPerItem *= 10 + dividor *= 10 + maximumNumberOfDecimals += 1 + } + + var base: CGFloat = BaseConstants.verticalBaseAnchors.first { numberOfOffsetsPerItem > $0 } ?? BaseConstants.defaultVerticalBaseAnchor + base = base * multiplier / dividor + + var verticalLabels: [LinesChartLabel] = [] + var verticalValue = (verticalRange.lowerBound / base).rounded(.down) * base + let lowerBound = verticalValue + + let numberFormatter = BaseConstants.chartNumberFormatter + numberFormatter.maximumFractionDigits = maximumNumberOfDecimals + while verticalValue < verticalRange.upperBound { + let text: String = numberFormatter.string(from: NSNumber(value: Double(verticalValue))) ?? "" + + verticalLabels.append(LinesChartLabel(value: verticalValue, text: text)) + verticalValue += base + } + let updatedRange = lowerBound...verticalValue + + return (updatedRange, verticalLabels) + } + + func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel { + let values: [ChartDetailsViewModel.Value] = chartsCollection.chartValues.enumerated().map { arg in + let (index, component) = arg + return ChartDetailsViewModel.Value(prefix: nil, + title: component.name, + value: BaseConstants.detailsNumberFormatter.string(from: NSNumber(value: component.values[pointIndex])) ?? "", + color: component.color, + visible: chartVisibility[index]) + } + let dateString: String + if isZoomed { + dateString = BaseConstants.timeDateFormatter.string(from: closestDate) + } else { + dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate) + } + let viewModel = ChartDetailsViewModel(title: dateString, + showArrow: !self.isZoomed, + showPrefixes: false, + values: values, + totalValue: nil, + tapAction: { [weak self] in + self?.zoomInOnDateClosure?(closestDate) }) + return viewModel + } + + func updateChartRangeTitle(animated: Bool) { + let fromDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.lowerBound) + 1) + let toDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.upperBound)) + if Calendar.utc.startOfDay(for: fromDate) == Calendar.utc.startOfDay(for: toDate) { + let stirng = BaseConstants.headerFullZoomedFormatter.string(from: fromDate) + self.setChartTitleClosure?(stirng, animated) + } else { + let stirng = "\(BaseConstants.headerMediumRangeFormatter.string(from: fromDate)) - \(BaseConstants.headerMediumRangeFormatter.string(from: toDate))" + self.setChartTitleClosure?(stirng, animated) + } + } +} diff --git a/submodules/Charts/Sources/Charts/Controllers/Lines/BaseLinesChartController.swift b/submodules/Charts/Sources/Charts/Controllers/Lines/BaseLinesChartController.swift new file mode 100644 index 0000000000..60d5069d10 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Controllers/Lines/BaseLinesChartController.swift @@ -0,0 +1,236 @@ +// +// BaseLinesChartController.swift +// GraphTest +// +// Created by Andrei Salavei on 4/14/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class BaseLinesChartController: BaseChartController { + var chartVisibility: [Bool] + var zoomChartVisibility: [Bool] + var lastChartInteractionPoint: CGPoint = .zero + var isChartInteractionBegun: Bool = false + + var initialChartRange: ClosedRange = BaseConstants.defaultRange + var zoomedChartRange: ClosedRange = BaseConstants.defaultRange + + override init(chartsCollection: ChartsCollection) { + self.chartVisibility = Array(repeating: true, count: chartsCollection.chartValues.count) + self.zoomChartVisibility = [] + super.init(chartsCollection: chartsCollection) + } + + func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) { + if animated { + TimeInterval.setDefaultSuration(.expandAnimationDuration) + DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) { + TimeInterval.setDefaultSuration(.osXDuration) + } + } + + self.initialChartsCollection = chartsCollection + self.isZoomed = isZoomed + + self.setBackButtonVisibilityClosure?(isZoomed, animated) + + updateChartRangeTitle(animated: animated) + } + + func updateChartRangeTitle(animated: Bool) { + let fromDate = Date(timeIntervalSince1970: TimeInterval(zoomedChartRange.lowerBound) + .hour) + let toDate = Date(timeIntervalSince1970: TimeInterval(zoomedChartRange.upperBound)) + if Calendar.utc.startOfDay(for: fromDate) == Calendar.utc.startOfDay(for: toDate) { + let stirng = BaseConstants.headerFullZoomedFormatter.string(from: fromDate) + self.setChartTitleClosure?(stirng, animated) + } else { + let stirng = "\(BaseConstants.headerMediumRangeFormatter.string(from: fromDate)) - \(BaseConstants.headerMediumRangeFormatter.string(from: toDate))" + self.setChartTitleClosure?(stirng, animated) + } + } + + override func chartInteractionDidBegin(point: CGPoint) { + lastChartInteractionPoint = point + isChartInteractionBegun = true + } + + override func chartInteractionDidEnd() { + + } + + override func cancelChartInteraction() { + isChartInteractionBegun = false + } + + override func updateChartRange(_ rangeFraction: ClosedRange) { + + } + + override var actualChartVisibility: [Bool] { + return isZoomed ? zoomChartVisibility : chartVisibility + } + + override var actualChartsCollection: ChartsCollection { + return initialChartsCollection + } + + var visibleChartValues: [ChartsCollection.Chart] { + let visibleCharts: [ChartsCollection.Chart] = actualChartVisibility.enumerated().compactMap { args in + args.element ? initialChartsCollection.chartValues[args.offset] : nil + } + return visibleCharts + } + + + func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel { + let values: [ChartDetailsViewModel.Value] = actualChartsCollection.chartValues.enumerated().map { arg in + let (index, component) = arg + return ChartDetailsViewModel.Value(prefix: nil, + title: component.name, + value: BaseConstants.detailsNumberFormatter.string(from: component.values[pointIndex]), + color: component.color, + visible: actualChartVisibility[index]) + } + let dateString: String + if isZoomed { + dateString = BaseConstants.timeDateFormatter.string(from: closestDate) + } else { + dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate) + } + let viewModel = ChartDetailsViewModel(title: dateString, + showArrow: !self.isZoomed, + showPrefixes: false, + values: values, + totalValue: nil, + tapAction: { [weak self] in self?.didTapZoomIn(date: closestDate) }) + return viewModel + } + + override func didTapZoomIn(date: Date) { + guard isZoomed == false else { return } + cancelChartInteraction() + self.getDetailsData?(date, { updatedCollection in + if let updatedCollection = updatedCollection { + self.initialChartRange = self.currentHorizontalRange + if let startDate = updatedCollection.axisValues.first, + let endDate = updatedCollection.axisValues.last { + self.zoomedChartRange = CGFloat(max(date.timeIntervalSince1970, startDate.timeIntervalSince1970))...CGFloat(min(date.timeIntervalSince1970 + .day - .hour, endDate.timeIntervalSince1970)) + } else { + self.zoomedChartRange = CGFloat(date.timeIntervalSince1970)...CGFloat(date.timeIntervalSince1970 + .day - 1) + } + self.setupChartCollection(chartsCollection: updatedCollection, animated: true, isZoomed: true) + } + }) + } + + func horizontalLimitsLabels(horizontalRange: ClosedRange, + scaleType: ChartScaleType, + prevoiusHorizontalStrideInterval: Int) -> (Int, [LinesChartLabel])? { + let numberOfItems = horizontalRange.distance / CGFloat(scaleType.timeInterval) + let maximumNumberOfItems = chartFrame().width / scaleType.minimumAxisXDistance + let tempStride = max(1, Int((numberOfItems / maximumNumberOfItems).rounded(.up))) + var strideInterval = 1 + while strideInterval < tempStride { + strideInterval *= 2 + } + + if strideInterval != prevoiusHorizontalStrideInterval && strideInterval > 0 { + var labels: [LinesChartLabel] = [] + for index in stride(from: initialChartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() { + let date = initialChartsCollection.axisValues[index] + labels.append(LinesChartLabel(value: CGFloat(date.timeIntervalSince1970), + text: scaleType.dateFormatter.string(from: date))) + } + return (strideInterval, labels) + } + return nil + } + + func findClosestDateTo(dateToFind: Date) -> (Date, Int)? { + guard initialChartsCollection.axisValues.count > 0 else { return nil } + var closestDate = initialChartsCollection.axisValues[0] + var minIndex = 0 + for (index, date) in initialChartsCollection.axisValues.enumerated() { + if abs(dateToFind.timeIntervalSince(date)) < abs(dateToFind.timeIntervalSince(closestDate)) { + closestDate = date + minIndex = index + } + } + return (closestDate, minIndex) + } + + func verticalLimitsLabels(verticalRange: ClosedRange) -> (ClosedRange, [LinesChartLabel]) { + let ditance = verticalRange.distance + let chartHeight = chartFrame().height + + guard ditance > 0, chartHeight > 0 else { return (BaseConstants.defaultRange, []) } + + let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance) + + var numberOfOffsetsPerItem = ditance / approximateNumberOfChartValues + var multiplier: CGFloat = 1.0 + while numberOfOffsetsPerItem > 10 { + numberOfOffsetsPerItem /= 10 + multiplier *= 10 + } + var dividor: CGFloat = 1.0 + var maximumNumberOfDecimals = 2 + while numberOfOffsetsPerItem < 1 { + numberOfOffsetsPerItem *= 10 + dividor *= 10 + maximumNumberOfDecimals += 1 + } + + var base: CGFloat = BaseConstants.verticalBaseAnchors.first { numberOfOffsetsPerItem > $0 } ?? BaseConstants.defaultVerticalBaseAnchor + base = base * multiplier / dividor + + var verticalLabels: [LinesChartLabel] = [] + var verticalValue = (verticalRange.lowerBound / base).rounded(.down) * base + let lowerBound = verticalValue + + let numberFormatter = BaseConstants.chartNumberFormatter + numberFormatter.maximumFractionDigits = maximumNumberOfDecimals + while verticalValue < verticalRange.upperBound { + let text: String = numberFormatter.string(from: NSNumber(value: Double(verticalValue))) ?? "" + + verticalLabels.append(LinesChartLabel(value: verticalValue, text: text)) + verticalValue += base + } + let updatedRange = lowerBound...verticalValue + + return (updatedRange, verticalLabels) + } +} + +enum ChartScaleType { + case day + case hour + case minutes5 +} + +extension ChartScaleType { + var timeInterval: TimeInterval { + switch self { + case .day: return .day + case .hour: return .hour + case .minutes5: return .minute * 5 + } + } + + var minimumAxisXDistance: CGFloat { + switch self { + case .day: return 50 + case .hour: return 40 + case .minutes5: return 40 + } + } + var dateFormatter: DateFormatter { + switch self { + case .day: return BaseConstants.monthDayDateFormatter + case .hour: return BaseConstants.timeDateFormatter + case .minutes5: return BaseConstants.timeDateFormatter + } + } +} diff --git a/submodules/Charts/Sources/Charts/Controllers/Lines/GeneralLinesChartController.swift b/submodules/Charts/Sources/Charts/Controllers/Lines/GeneralLinesChartController.swift new file mode 100644 index 0000000000..3d52888d72 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Controllers/Lines/GeneralLinesChartController.swift @@ -0,0 +1,247 @@ +// +// LinesChartController.swift +// GraphTest +// +// Created by Andrei Salavei on 4/7/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +private enum Constants { + static let defaultRange: ClosedRange = 0...1 +} + +class GeneralLinesChartController: BaseLinesChartController { + private let initialChartCollection: ChartsCollection + + private let mainLinesRenderer = LinesChartRenderer() + private let horizontalScalesRenderer = HorizontalScalesRenderer() + private let verticalScalesRenderer = VerticalScalesRenderer() + private let verticalLineRenderer = VerticalLinesRenderer() + private let lineBulletsRenerer = LineBulletsRenerer() + + private let previewLinesRenderer = LinesChartRenderer() + + private var totalVerticalRange: ClosedRange = Constants.defaultRange + private var totalHorizontalRange: ClosedRange = Constants.defaultRange + + private var prevoiusHorizontalStrideInterval: Int = 1 + + private (set) var chartLines: [LinesChartRenderer.LineData] = [] + + override init(chartsCollection: ChartsCollection) { + self.initialChartCollection = chartsCollection + self.mainLinesRenderer.lineWidth = 2 + self.mainLinesRenderer.optimizationLevel = BaseConstants.linesChartOptimizationLevel + self.previewLinesRenderer.optimizationLevel = BaseConstants.previewLinesChartOptimizationLevel + + self.lineBulletsRenerer.isEnabled = false + + super.init(chartsCollection: chartsCollection) + self.zoomChartVisibility = chartVisibility + } + + override func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) { + super.setupChartCollection(chartsCollection: chartsCollection, animated: animated, isZoomed: isZoomed) + + self.chartLines = chartsCollection.chartValues.map { chart in + let points = chart.values.enumerated().map({ (arg) -> CGPoint in + return CGPoint(x: chartsCollection.axisValues[arg.offset].timeIntervalSince1970, + y: arg.element) + }) + return LinesChartRenderer.LineData(color: chart.color, points: points) + } + + self.prevoiusHorizontalStrideInterval = -1 + self.totalVerticalRange = LinesChartRenderer.LineData.verticalRange(lines: chartLines) ?? Constants.defaultRange + self.totalHorizontalRange = LinesChartRenderer.LineData.horizontalRange(lines: chartLines) ?? Constants.defaultRange + self.lineBulletsRenerer.bullets = self.chartLines.map { LineBulletsRenerer.Bullet(coordinate: $0.points.first ?? .zero, + color: $0.color)} + + let chartRange: ClosedRange + if isZoomed { + chartRange = zoomedChartRange + } else { + chartRange = initialChartRange + } + + self.previewLinesRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated) + self.previewLinesRenderer.setup(verticalRange: totalVerticalRange, animated: animated) + + self.mainLinesRenderer.setLines(lines: chartLines, animated: animated) + self.previewLinesRenderer.setLines(lines: chartLines, animated: animated) + + updateHorizontalLimists(horizontalRange: chartRange, animated: animated) + updateMainChartHorizontalRange(range: chartRange, animated: animated) + updateVerticalLimitsAndRange(horizontalRange: chartRange, animated: animated) + + self.chartRangeUpdatedClosure?(currentChartHorizontalRangeFraction, animated) + } + + override func initializeChart() { + if let first = initialChartCollection.axisValues.first?.timeIntervalSince1970, + let last = initialChartCollection.axisValues.last?.timeIntervalSince1970 { + initialChartRange = CGFloat(max(first, last - BaseConstants.defaultRangePresetLength))...CGFloat(last) + } + setupChartCollection(chartsCollection: initialChartCollection, animated: false, isZoomed: false) + } + + override var mainChartRenderers: [ChartViewRenderer] { + return [//performanceRenderer, + mainLinesRenderer, + horizontalScalesRenderer, + verticalScalesRenderer, + verticalLineRenderer, + lineBulletsRenerer + ] + } + + override var navigationRenderers: [ChartViewRenderer] { + return [previewLinesRenderer] + } + + override func updateChartsVisibility(visibility: [Bool], animated: Bool) { + chartVisibility = visibility + zoomChartVisibility = visibility + for (index, isVisible) in visibility.enumerated() { + mainLinesRenderer.setLineVisible(isVisible, at: index, animated: animated) + previewLinesRenderer.setLineVisible(isVisible, at: index, animated: animated) + lineBulletsRenerer.setLineVisible(isVisible, at: index, animated: animated) + } + + updateVerticalLimitsAndRange(horizontalRange: currentHorizontalRange, animated: true) + + if isChartInteractionBegun { + chartInteractionDidBegin(point: lastChartInteractionPoint) + } + } + + override func chartInteractionDidBegin(point: CGPoint) { + let horizontalRange = mainLinesRenderer.horizontalRange.current + let chartFrame = self.chartFrame() + guard chartFrame.width > 0 else { return } + let chartInteractionWasBegin = isChartInteractionBegun + + let dateToFind = Date(timeIntervalSince1970: TimeInterval(horizontalRange.distance * point.x + horizontalRange.lowerBound)) + guard let (closestDate, minIndex) = findClosestDateTo(dateToFind: dateToFind) else { return } + + super.chartInteractionDidBegin(point: point) + + self.lineBulletsRenerer.bullets = chartLines.compactMap { chart in + return LineBulletsRenerer.Bullet(coordinate: chart.points[minIndex], color: chart.color) + } + self.lineBulletsRenerer.isEnabled = true + + let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970) + let detailsViewPosition = (chartValue - horizontalRange.lowerBound) / horizontalRange.distance * chartFrame.width + chartFrame.minX + self.setDetailsViewModel?(chartDetailsViewModel(closestDate: closestDate, pointIndex: minIndex), chartInteractionWasBegin) + self.setDetailsChartVisibleClosure?(true, true) + self.setDetailsViewPositionClosure?(detailsViewPosition) + self.verticalLineRenderer.values = [chartValue] + } + + + override var currentChartHorizontalRangeFraction: ClosedRange { + let lowerPercent = (currentHorizontalRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance + let upperPercent = (currentHorizontalRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance + return lowerPercent...upperPercent + } + + override var currentHorizontalRange: ClosedRange { + return mainLinesRenderer.horizontalRange.end + } + + override func cancelChartInteraction() { + super.cancelChartInteraction() + self.lineBulletsRenerer.isEnabled = false + + self.setDetailsChartVisibleClosure?(false, true) + self.verticalLineRenderer.values = [] + } + + override func didTapZoomOut() { + cancelChartInteraction() + self.setupChartCollection(chartsCollection: initialChartCollection, animated: true, isZoomed: false) + } + + var visibleCharts: [LinesChartRenderer.LineData] { + let visibleCharts: [LinesChartRenderer.LineData] = chartVisibility.enumerated().compactMap { args in + args.element ? chartLines[args.offset] : nil + } + return visibleCharts + } + + override func updateChartRange(_ rangeFraction: ClosedRange) { + cancelChartInteraction() + + let horizontalRange = ClosedRange(uncheckedBounds: + (lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance, + upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance)) + + zoomedChartRange = horizontalRange + updateChartRangeTitle(animated: true) + + updateMainChartHorizontalRange(range: horizontalRange, animated: false) + updateHorizontalLimists(horizontalRange: horizontalRange, animated: true) + updateVerticalLimitsAndRange(horizontalRange: horizontalRange, animated: true) + } + + func updateMainChartHorizontalRange(range: ClosedRange, animated: Bool) { + mainLinesRenderer.setup(horizontalRange: range, animated: animated) + horizontalScalesRenderer.setup(horizontalRange: range, animated: animated) + verticalScalesRenderer.setup(horizontalRange: range, animated: animated) + verticalLineRenderer.setup(horizontalRange: range, animated: animated) + lineBulletsRenerer.setup(horizontalRange: range, animated: animated) + } + + func updateMainChartVerticalRange(range: ClosedRange, animated: Bool) { + mainLinesRenderer.setup(verticalRange: range, animated: animated) + horizontalScalesRenderer.setup(verticalRange: range, animated: animated) + verticalScalesRenderer.setup(verticalRange: range, animated: animated) + verticalLineRenderer.setup(verticalRange: range, animated: animated) + lineBulletsRenerer.setup(verticalRange: range, animated: animated) + } + + func updateHorizontalLimists(horizontalRange: ClosedRange, animated: Bool) { + if let (stride, labels) = horizontalLimitsLabels(horizontalRange: horizontalRange, + scaleType: isZoomed ? .hour : .day, + prevoiusHorizontalStrideInterval: prevoiusHorizontalStrideInterval) { + self.horizontalScalesRenderer.setup(labels: labels, animated: animated) + self.prevoiusHorizontalStrideInterval = stride + } + } + + func updateVerticalLimitsAndRange(horizontalRange: ClosedRange, animated: Bool) { + if let verticalRange = LinesChartRenderer.LineData.verticalRange(lines: visibleCharts, + calculatingRange: horizontalRange, + addBounds: true) { + + + let (range, labels) = verticalLimitsLabels(verticalRange: verticalRange) + + if verticalScalesRenderer.verticalRange.end != range { + verticalScalesRenderer.setup(verticalLimitsLabels: labels, animated: animated) + updateMainChartVerticalRange(range: range, animated: animated) + } + verticalScalesRenderer.setVisible(true, animated: animated) + } else { + verticalScalesRenderer.setVisible(false, animated: animated) + } + + guard let previewVerticalRange = LinesChartRenderer.LineData.verticalRange(lines: visibleCharts) else { return } + + if previewLinesRenderer.verticalRange.end != previewVerticalRange { + previewLinesRenderer.setup(verticalRange: previewVerticalRange, animated: animated) + } + } + + override func apply(colorMode: ColorMode, animated: Bool) { + horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor + verticalScalesRenderer.labelsColor = colorMode.chartLabelsColor + verticalScalesRenderer.axisXColor = colorMode.chartStrongLinesColor + verticalScalesRenderer.horizontalLinesColor = colorMode.chartHelperLinesColor + lineBulletsRenerer.setInnerColor(colorMode.chartBackgroundColor, animated: animated) + verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor + } +} diff --git a/submodules/Charts/Sources/Charts/Controllers/Lines/TwoAxisLinesChartController.swift b/submodules/Charts/Sources/Charts/Controllers/Lines/TwoAxisLinesChartController.swift new file mode 100644 index 0000000000..251e76271e --- /dev/null +++ b/submodules/Charts/Sources/Charts/Controllers/Lines/TwoAxisLinesChartController.swift @@ -0,0 +1,306 @@ +// +// TwoAxisLinesChartController.swift +// GraphTest +// +// Created by Andrei Salavei on 4/7/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +private enum Constants { + static let verticalBaseAnchors: [CGFloat] = [8, 5, 4, 2.5, 2, 1] + static let defaultRange: ClosedRange = 0...1 +} + +class TwoAxisLinesChartController: BaseLinesChartController { + class GraphController { + let mainLinesRenderer = LinesChartRenderer() + let verticalScalesRenderer = VerticalScalesRenderer() + let lineBulletsRenerer = LineBulletsRenerer() + let previewLinesRenderer = LinesChartRenderer() + + var chartLines: [LinesChartRenderer.LineData] = [] + + var totalVerticalRange: ClosedRange = Constants.defaultRange + + init() { + self.mainLinesRenderer.lineWidth = 2 + self.previewLinesRenderer.lineWidth = 1 + self.lineBulletsRenerer.isEnabled = false + + self.mainLinesRenderer.optimizationLevel = BaseConstants.linesChartOptimizationLevel + self.previewLinesRenderer.optimizationLevel = BaseConstants.previewLinesChartOptimizationLevel + } + + func updateMainChartVerticalRange(range: ClosedRange, animated: Bool) { + mainLinesRenderer.setup(verticalRange: range, animated: animated) + verticalScalesRenderer.setup(verticalRange: range, animated: animated) + lineBulletsRenerer.setup(verticalRange: range, animated: animated) + } + } + + private var graphControllers: [GraphController] = [] + private let verticalLineRenderer = VerticalLinesRenderer() + private let horizontalScalesRenderer = HorizontalScalesRenderer() + + var totalHorizontalRange: ClosedRange = Constants.defaultRange + + private let initialChartCollection: ChartsCollection + + private var prevoiusHorizontalStrideInterval: Int = 1 + + override init(chartsCollection: ChartsCollection) { + self.initialChartCollection = chartsCollection + graphControllers = chartsCollection.chartValues.map { _ in GraphController() } + + super.init(chartsCollection: chartsCollection) + self.zoomChartVisibility = chartVisibility + } + + override func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) { + super.setupChartCollection(chartsCollection: chartsCollection, animated: animated, isZoomed: isZoomed) + + for (index, controller) in self.graphControllers.enumerated() { + let chart = chartsCollection.chartValues[index] + let points = chart.values.enumerated().map({ (arg) -> CGPoint in + return CGPoint(x: chartsCollection.axisValues[arg.offset].timeIntervalSince1970, + y: arg.element) + }) + let chartLines = [LinesChartRenderer.LineData(color: chart.color, points: points)] + controller.chartLines = [LinesChartRenderer.LineData(color: chart.color, points: points)] + controller.verticalScalesRenderer.labelsColor = chart.color + controller.totalVerticalRange = LinesChartRenderer.LineData.verticalRange(lines: chartLines) ?? Constants.defaultRange + self.totalHorizontalRange = LinesChartRenderer.LineData.horizontalRange(lines: chartLines) ?? Constants.defaultRange + controller.lineBulletsRenerer.bullets = chartLines.map { LineBulletsRenerer.Bullet(coordinate: $0.points.first ?? .zero, + color: $0.color) } + controller.previewLinesRenderer.setup(horizontalRange: self.totalHorizontalRange, animated: animated) + controller.previewLinesRenderer.setup(verticalRange: controller.totalVerticalRange, animated: animated) + controller.mainLinesRenderer.setLines(lines: chartLines, animated: animated) + controller.previewLinesRenderer.setLines(lines: chartLines, animated: animated) + + controller.verticalScalesRenderer.setHorizontalLinesVisible((index == 0), animated: animated) + controller.verticalScalesRenderer.isRightAligned = (index != 0) + } + + self.prevoiusHorizontalStrideInterval = -1 + + let chartRange: ClosedRange + if isZoomed { + chartRange = zoomedChartRange + } else { + chartRange = initialChartRange + } + + updateHorizontalLimists(horizontalRange: chartRange, animated: animated) + updateMainChartHorizontalRange(range: chartRange, animated: animated) + updateVerticalLimitsAndRange(horizontalRange: chartRange, animated: animated) + + self.chartRangeUpdatedClosure?(currentChartHorizontalRangeFraction, animated) + } + + override func initializeChart() { + if let first = initialChartCollection.axisValues.first?.timeIntervalSince1970, + let last = initialChartCollection.axisValues.last?.timeIntervalSince1970 { + initialChartRange = CGFloat(max(first, last - BaseConstants.defaultRangePresetLength))...CGFloat(last) + } + setupChartCollection(chartsCollection: initialChartCollection, animated: false, isZoomed: false) + } + + override var mainChartRenderers: [ChartViewRenderer] { + return graphControllers.map { $0.mainLinesRenderer } + + graphControllers.flatMap { [$0.verticalScalesRenderer, $0.lineBulletsRenerer] } + + [horizontalScalesRenderer, verticalLineRenderer, +// performanceRenderer + ] + } + + override var navigationRenderers: [ChartViewRenderer] { + return graphControllers.map { $0.previewLinesRenderer } + } + + override func updateChartsVisibility(visibility: [Bool], animated: Bool) { + chartVisibility = visibility + zoomChartVisibility = visibility + let firstIndex = visibility.firstIndex(where: { $0 }) + for (index, isVisible) in visibility.enumerated() { + let graph = graphControllers[index] + for graphIndex in graph.chartLines.indices { + graph.mainLinesRenderer.setLineVisible(isVisible, at: graphIndex, animated: animated) + graph.previewLinesRenderer.setLineVisible(isVisible, at: graphIndex, animated: animated) + graph.lineBulletsRenerer.setLineVisible(isVisible, at: graphIndex, animated: animated) + } + graph.verticalScalesRenderer.setVisible(isVisible, animated: animated) + if let firstIndex = firstIndex { + graph.verticalScalesRenderer.setHorizontalLinesVisible(index == firstIndex, animated: animated) + } + } + + updateVerticalLimitsAndRange(horizontalRange: currentHorizontalRange, animated: true) + + if isChartInteractionBegun { + chartInteractionDidBegin(point: lastChartInteractionPoint) + } + } + + override func chartInteractionDidBegin(point: CGPoint) { + let horizontalRange = currentHorizontalRange + let chartFrame = self.chartFrame() + guard chartFrame.width > 0 else { return } + + let dateToFind = Date(timeIntervalSince1970: TimeInterval(horizontalRange.distance * point.x + horizontalRange.lowerBound)) + guard let (closestDate, minIndex) = findClosestDateTo(dateToFind: dateToFind) else { return } + + let chartInteractionWasBegin = isChartInteractionBegun + super.chartInteractionDidBegin(point: point) + + for graphController in graphControllers { + graphController.lineBulletsRenerer.bullets = graphController.chartLines.map { chart in + LineBulletsRenerer.Bullet(coordinate: chart.points[minIndex], color: chart.color) + } + graphController.lineBulletsRenerer.isEnabled = true + } + + let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970) + let detailsViewPosition = (chartValue - horizontalRange.lowerBound) / horizontalRange.distance * chartFrame.width + chartFrame.minX + self.setDetailsViewModel?(chartDetailsViewModel(closestDate: closestDate, pointIndex: minIndex), chartInteractionWasBegin) + self.setDetailsChartVisibleClosure?(true, true) + self.setDetailsViewPositionClosure?(detailsViewPosition) + self.verticalLineRenderer.values = [chartValue] + } + + override var currentChartHorizontalRangeFraction: ClosedRange { + let lowerPercent = (currentHorizontalRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance + let upperPercent = (currentHorizontalRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance + return lowerPercent...upperPercent + } + + override var currentHorizontalRange: ClosedRange { + return graphControllers.first?.mainLinesRenderer.horizontalRange.end ?? Constants.defaultRange + } + + override func cancelChartInteraction() { + super.cancelChartInteraction() + for graphController in graphControllers { + graphController.lineBulletsRenerer.isEnabled = false + } + + self.setDetailsChartVisibleClosure?(false, true) + self.verticalLineRenderer.values = [] + } + + override func didTapZoomOut() { + cancelChartInteraction() + self.setupChartCollection(chartsCollection: initialChartCollection, animated: true, isZoomed: false) + } + + override func updateChartRange(_ rangeFraction: ClosedRange) { + cancelChartInteraction() + + let horizontalRange = ClosedRange(uncheckedBounds: + (lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance, + upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance)) + + zoomedChartRange = horizontalRange + updateChartRangeTitle(animated: true) + + updateMainChartHorizontalRange(range: horizontalRange, animated: false) + updateHorizontalLimists(horizontalRange: horizontalRange, animated: true) + updateVerticalLimitsAndRange(horizontalRange: horizontalRange, animated: true) + } + + func updateMainChartHorizontalRange(range: ClosedRange, animated: Bool) { + for controller in graphControllers { + controller.mainLinesRenderer.setup(horizontalRange: range, animated: animated) + controller.verticalScalesRenderer.setup(horizontalRange: range, animated: animated) + controller.lineBulletsRenerer.setup(horizontalRange: range, animated: animated) + } + horizontalScalesRenderer.setup(horizontalRange: range, animated: animated) + verticalLineRenderer.setup(horizontalRange: range, animated: animated) + } + + func updateHorizontalLimists(horizontalRange: ClosedRange, animated: Bool) { + if let (stride, labels) = horizontalLimitsLabels(horizontalRange: horizontalRange, + scaleType: isZoomed ? .hour : .day, + prevoiusHorizontalStrideInterval: prevoiusHorizontalStrideInterval) { + self.horizontalScalesRenderer.setup(labels: labels, animated: animated) + self.prevoiusHorizontalStrideInterval = stride + } + } + + func updateVerticalLimitsAndRange(horizontalRange: ClosedRange, animated: Bool) { + let chartHeight = chartFrame().height + let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance) + + var dividorsAndMultiplers: [(startValue: CGFloat, base: CGFloat, count: Int, maximumNumberOfDecimals: Int)] = graphControllers.enumerated().map { arg in + let (index, controller) = arg + let verticalRange = LinesChartRenderer.LineData.verticalRange(lines: controller.chartLines, + calculatingRange: horizontalRange, + addBounds: true) ?? controller.totalVerticalRange + + var numberOfOffsetsPerItem = verticalRange.distance / approximateNumberOfChartValues + + var multiplier: CGFloat = 1.0 + while numberOfOffsetsPerItem > 10 { + numberOfOffsetsPerItem /= 10 + multiplier *= 10 + } + var dividor: CGFloat = 1.0 + var maximumNumberOfDecimals = 2 + while numberOfOffsetsPerItem < 1 { + numberOfOffsetsPerItem *= 10 + dividor *= 10 + maximumNumberOfDecimals += 1 + } + + let generalBase = Constants.verticalBaseAnchors.first { numberOfOffsetsPerItem > $0 } ?? BaseConstants.defaultVerticalBaseAnchor + let base = generalBase * multiplier / dividor + + var verticalValue = (verticalRange.lowerBound / base).rounded(.down) * base + let startValue = verticalValue + var count = 0 + if chartVisibility[index] { + while verticalValue < verticalRange.upperBound { + count += 1 + verticalValue += base + } + } + return (startValue: startValue, base: base, count: count, maximumNumberOfDecimals: maximumNumberOfDecimals) + } + + let totalCount = dividorsAndMultiplers.map { $0.count }.max() ?? 0 + guard totalCount > 0 else { return } + + let numberFormatter = BaseConstants.chartNumberFormatter + for (index, controller) in graphControllers.enumerated() { + + let (startValue, base, _, maximumNumberOfDecimals) = dividorsAndMultiplers[index] + + let updatedRange = startValue...(startValue + base * CGFloat(totalCount)) + if controller.verticalScalesRenderer.verticalRange.end != updatedRange { + numberFormatter.maximumFractionDigits = maximumNumberOfDecimals + + var verticalLabels: [LinesChartLabel] = [] + for multipler in 0...(totalCount - 1) { + let verticalValue = startValue + base * CGFloat(multipler) + let text: String = numberFormatter.string(from: NSNumber(value: Double(verticalValue))) ?? "" + verticalLabels.append(LinesChartLabel(value: verticalValue, text: text)) + } + + controller.verticalScalesRenderer.setup(verticalLimitsLabels: verticalLabels, animated: animated) + controller.updateMainChartVerticalRange(range: updatedRange, animated: animated) + } + } + } + + override func apply(colorMode: ColorMode, animated: Bool) { + horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor + verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor + + for controller in graphControllers { + controller.verticalScalesRenderer.horizontalLinesColor = colorMode.chartHelperLinesColor + controller.lineBulletsRenerer.setInnerColor(colorMode.chartBackgroundColor, animated: animated) + controller.verticalScalesRenderer.axisXColor = colorMode.chartStrongLinesColor + } + } +} diff --git a/submodules/Charts/Sources/Charts/Controllers/Percent And Pie/PercentChartComponentController.swift b/submodules/Charts/Sources/Charts/Controllers/Percent And Pie/PercentChartComponentController.swift new file mode 100644 index 0000000000..d75ba09d91 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Controllers/Percent And Pie/PercentChartComponentController.swift @@ -0,0 +1,195 @@ +// +// PercentChartComponentController.swift +// GraphTest +// +// Created by Andrei Salavei on 4/14/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class PercentChartComponentController: GeneralChartComponentController { + let mainPecentChartRenderer: PecentChartRenderer + let horizontalScalesRenderer: HorizontalScalesRenderer + let verticalScalesRenderer: VerticalScalesRenderer + let verticalLineRenderer: VerticalLinesRenderer + let previewPercentChartRenderer: PecentChartRenderer + var percentageData: PecentChartRenderer.PercentageData = .blank + + init(isZoomed: Bool, + mainPecentChartRenderer: PecentChartRenderer, + horizontalScalesRenderer: HorizontalScalesRenderer, + verticalScalesRenderer: VerticalScalesRenderer, + verticalLineRenderer: VerticalLinesRenderer, + previewPercentChartRenderer: PecentChartRenderer) { + self.mainPecentChartRenderer = mainPecentChartRenderer + self.horizontalScalesRenderer = horizontalScalesRenderer + self.verticalScalesRenderer = verticalScalesRenderer + self.verticalLineRenderer = verticalLineRenderer + self.previewPercentChartRenderer = previewPercentChartRenderer + + super.init(isZoomed: isZoomed) + } + + override func initialize(chartsCollection: ChartsCollection, initialDate: Date, totalHorizontalRange _: ClosedRange, totalVerticalRange _: ClosedRange) { + let components = chartsCollection.chartValues.map { PecentChartRenderer.PercentageData.Component(color: $0.color, + values: $0.values.map { CGFloat($0) }) } + self.percentageData = PecentChartRenderer.PercentageData(locations: chartsCollection.axisValues.map { CGFloat($0.timeIntervalSince1970) }, + components: components) + let totalHorizontalRange = PecentChartRenderer.PercentageData.horizontalRange(data: self.percentageData) ?? BaseConstants.defaultRange + let totalVerticalRange = BaseConstants.defaultRange + + super.initialize(chartsCollection: chartsCollection, + initialDate: initialDate, + totalHorizontalRange: totalHorizontalRange, + totalVerticalRange: totalVerticalRange) + + mainPecentChartRenderer.percentageData = self.percentageData + previewPercentChartRenderer.percentageData = self.percentageData + + let axisValues: [CGFloat] = [0, 25, 50, 75, 100] + let labels: [LinesChartLabel] = axisValues.map { value in + return LinesChartLabel(value: value / 100, text: BaseConstants.detailsNumberFormatter.string(from: NSNumber(value: Double(value))) ?? "") + } + verticalScalesRenderer.setup(verticalLimitsLabels: labels, animated: false) + + setupMainChart(horizontalRange: initialHorizontalRange, animated: false) + setupMainChart(verticalRange: initialVerticalRange, animated: false) + previewPercentChartRenderer.setup(verticalRange: totalVerticalRange, animated: false) + previewPercentChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: false) + updateHorizontalLimitLabels(animated: false) + } + + override func willAppear(animated: Bool) { + previewPercentChartRenderer.setup(verticalRange: totalVerticalRange, animated: animated) + previewPercentChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated) + + setConponentsVisible(visible: true, animated: true) + + setupMainChart(verticalRange: initialVerticalRange, animated: animated) + setupMainChart(horizontalRange: initialHorizontalRange, animated: animated) + + updatePreviewRangeClosure?(currentChartHorizontalRangeFraction, animated) + + super.willAppear(animated: animated) + } + + override func chartRangeDidUpdated(_ updatedRange: ClosedRange) { + super.chartRangeDidUpdated(updatedRange) + + initialHorizontalRange = updatedRange + setupMainChart(horizontalRange: updatedRange, animated: false) + updateHorizontalLimitLabels(animated: true) + } + + func updateHorizontalLimitLabels(animated: Bool) { + updateHorizontalLimitLabels(horizontalScalesRenderer: horizontalScalesRenderer, + horizontalRange: initialHorizontalRange, + scaleType: isZoomed ? .hour : .day, + forceUpdate: false, + animated: animated) + } + + func prepareAppearanceAnimation(horizontalRnage: ClosedRange) { + setupMainChart(horizontalRange: horizontalRnage, animated: false) + setConponentsVisible(visible: false, animated: false) + } + + func setConponentsVisible(visible: Bool, animated: Bool) { + mainPecentChartRenderer.setVisible(visible, animated: animated) + horizontalScalesRenderer.setVisible(visible, animated: animated) + verticalScalesRenderer.setVisible(visible, animated: animated) + verticalLineRenderer.setVisible(visible, animated: animated) + previewPercentChartRenderer.setVisible(visible, animated: animated) + } + + func setupMainChart(horizontalRange: ClosedRange, animated: Bool) { + mainPecentChartRenderer.setup(horizontalRange: horizontalRange, animated: animated) + horizontalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated) + verticalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated) + verticalLineRenderer.setup(horizontalRange: horizontalRange, animated: animated) + } + + func setupMainChart(verticalRange: ClosedRange, animated: Bool) { + mainPecentChartRenderer.setup(verticalRange: verticalRange, animated: animated) + horizontalScalesRenderer.setup(verticalRange: verticalRange, animated: animated) + verticalScalesRenderer.setup(verticalRange: verticalRange, animated: animated) + verticalLineRenderer.setup(verticalRange: verticalRange, animated: animated) + } + + override func updateChartsVisibility(visibility: [Bool], animated: Bool) { + super.updateChartsVisibility(visibility: visibility, animated: animated) + for (index, isVisible) in visibility.enumerated() { + mainPecentChartRenderer.setComponentVisible(isVisible, at: index, animated: animated) + previewPercentChartRenderer.setComponentVisible(isVisible, at: index, animated: animated) + } + verticalScalesRenderer.setVisible(visibility.contains(true), animated: animated) + } + + override func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel { + let visibleValues = visibleDetailsChartValues + + let total = visibleValues.map { $0.values[pointIndex] }.reduce(0, +) + + let values: [ChartDetailsViewModel.Value] = chartsCollection.chartValues.enumerated().map { arg in + let (index, component) = arg + return ChartDetailsViewModel.Value(prefix: PercentConstants.percentValueFormatter.string(from: component.values[pointIndex] / total * 100), + title: component.name, + value: BaseConstants.detailsNumberFormatter.string(from: component.values[pointIndex]), + color: component.color, + visible: chartVisibility[index]) + } + let dateString: String + if isZoomed { + dateString = BaseConstants.timeDateFormatter.string(from: closestDate) + } else { + dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate) + } + let viewModel = ChartDetailsViewModel(title: dateString, + showArrow: !self.isZoomed, + showPrefixes: true, + values: values, + totalValue: nil, + tapAction: { [weak self] in + self?.hideDetailsView(animated: true) + self?.zoomInOnDateClosure?(closestDate) }) + return viewModel + } + + var currentlyVisiblePercentageData: PecentChartRenderer.PercentageData { + var currentPercentageData = percentageData + currentPercentageData.components = chartVisibility.enumerated().compactMap { $0.element ? currentPercentageData.components[$0.offset] : nil } + return currentPercentageData + } + + override var currentMainRangeRenderer: BaseChartRenderer { + return mainPecentChartRenderer + } + + override var currentPreviewRangeRenderer: BaseChartRenderer { + return previewPercentChartRenderer + } + + override func showDetailsView(at chartPosition: CGFloat, detailsViewPosition: CGFloat, dataIndex: Int, date: Date, animted: Bool) { + super.showDetailsView(at: chartPosition, detailsViewPosition: detailsViewPosition, dataIndex: dataIndex, date: date, animted: animted) + verticalLineRenderer.values = [chartPosition] + verticalLineRenderer.isEnabled = true + } + + override func hideDetailsView(animated: Bool) { + super.hideDetailsView(animated: animated) + + verticalLineRenderer.values = [] + verticalLineRenderer.isEnabled = false + } + + override func apply(colorMode: ColorMode, animated: Bool) { + super.apply(colorMode: colorMode, animated: animated) + + horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor + verticalScalesRenderer.labelsColor = colorMode.chartLabelsColor + verticalScalesRenderer.axisXColor = colorMode.barChartStrongLinesColor + verticalScalesRenderer.horizontalLinesColor = colorMode.barChartStrongLinesColor + verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor + } +} diff --git a/submodules/Charts/Sources/Charts/Controllers/Percent And Pie/PercentPieChartController.swift b/submodules/Charts/Sources/Charts/Controllers/Percent And Pie/PercentPieChartController.swift new file mode 100644 index 0000000000..484d8a2f11 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Controllers/Percent And Pie/PercentPieChartController.swift @@ -0,0 +1,281 @@ +// +// PercentPieChartController.swift +// GraphTest +// +// Created by Andrei Salavei on 4/7/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +enum PercentConstants { + static let percentValueFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.positiveSuffix = "%" + return formatter + }() +} + +private enum Constants { + static let zoomedRange = 7 +} + +class PercentPieChartController: BaseChartController { + let percentController: PercentChartComponentController + let pieController: PieChartComponentController + let transitionRenderer: PercentPieAnimationRenderer + + override init(chartsCollection: ChartsCollection) { + transitionRenderer = PercentPieAnimationRenderer() + percentController = PercentChartComponentController(isZoomed: false, + mainPecentChartRenderer: PecentChartRenderer(), + horizontalScalesRenderer: HorizontalScalesRenderer(), + verticalScalesRenderer: VerticalScalesRenderer(), + verticalLineRenderer: VerticalLinesRenderer(), + previewPercentChartRenderer: PecentChartRenderer()) + pieController = PieChartComponentController(isZoomed: true, + pieChartRenderer: PieChartRenderer(), + previewBarChartRenderer: BarChartRenderer()) + + super.init(chartsCollection: chartsCollection) + + [percentController, pieController].forEach { controller in + controller.chartFrame = { [unowned self] in self.chartFrame() } + controller.cartViewBounds = { [unowned self] in self.cartViewBounds() } + controller.zoomInOnDateClosure = { [unowned self] date in + self.didTapZoomIn(date: date) + } + controller.setChartTitleClosure = { [unowned self] (title, animated) in + self.setChartTitleClosure?(title, animated) + } + controller.setDetailsViewPositionClosure = { [unowned self] (position) in + self.setDetailsViewPositionClosure?(position) + } + controller.setDetailsChartVisibleClosure = { [unowned self] (visible, animated) in + self.setDetailsChartVisibleClosure?(visible, animated) + } + controller.setDetailsViewModel = { [unowned self] (viewModel, animated) in + self.setDetailsViewModel?(viewModel, animated) + } + controller.updatePreviewRangeClosure = { [unowned self] (fraction, animated) in + self.chartRangeUpdatedClosure?(fraction, animated) + } + controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in + self.setChartRangePagingEnabled(isEnabled: isEnabled, minimumSelectionSize: pageSize) + } + } + transitionRenderer.isEnabled = false + } + + override var mainChartRenderers: [ChartViewRenderer] { + return [percentController.mainPecentChartRenderer, + transitionRenderer, + percentController.horizontalScalesRenderer, + percentController.verticalScalesRenderer, + percentController.verticalLineRenderer, + pieController.pieChartRenderer, +// performanceRenderer + ] + } + + override var navigationRenderers: [ChartViewRenderer] { + return [percentController.previewPercentChartRenderer, + pieController.previewBarChartRenderer] + } + + override func initializeChart() { + percentController.initialize(chartsCollection: initialChartsCollection, + initialDate: Date(), + totalHorizontalRange: BaseConstants.defaultRange, + totalVerticalRange: BaseConstants.defaultRange) + switchToChart(chartsCollection: percentController.chartsCollection, isZoomed: false, animated: false) + } + + func switchToChart(chartsCollection: ChartsCollection, isZoomed: Bool, animated: Bool) { + if animated { + TimeInterval.setDefaultSuration(.expandAnimationDuration) + DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) { + TimeInterval.setDefaultSuration(.osXDuration) + } + } + + super.isZoomed = isZoomed + if isZoomed { + let toHorizontalRange = pieController.initialHorizontalRange + + pieController.updateChartsVisibility(visibility: percentController.chartVisibility, animated: false) + pieController.pieChartRenderer.setup(horizontalRange: percentController.currentHorizontalMainChartRange, animated: false) + pieController.previewBarChartRenderer.setup(horizontalRange: percentController.currentPreviewHorizontalRange, animated: false) + pieController.pieChartRenderer.setVisible(false, animated: false) + pieController.previewBarChartRenderer.setVisible(true, animated: false) + + pieController.willAppear(animated: animated) + percentController.willDisappear(animated: animated) + + pieController.pieChartRenderer.drawPie = false + percentController.mainPecentChartRenderer.isEnabled = false + + setupTransitionRenderer() + + percentController.setupMainChart(horizontalRange: toHorizontalRange, animated: animated) + percentController.previewPercentChartRenderer.setup(horizontalRange: toHorizontalRange, animated: animated) + percentController.setConponentsVisible(visible: false, animated: animated) + + transitionRenderer.animate(fromDataToPie: true, animated: animated) { [weak self] in + self?.pieController.pieChartRenderer.drawPie = true + self?.percentController.mainPecentChartRenderer.isEnabled = true + } + } else { + if !pieController.chartsCollection.isBlank { + let fromHorizontalRange = pieController.currentHorizontalMainChartRange + let toHorizontalRange = percentController.initialHorizontalRange + + pieController.pieChartRenderer.setup(horizontalRange: toHorizontalRange, animated: animated) + pieController.previewBarChartRenderer.setup(horizontalRange: toHorizontalRange, animated: animated) + pieController.pieChartRenderer.setVisible(false, animated: animated) + pieController.previewBarChartRenderer.setVisible(false, animated: animated) + + percentController.updateChartsVisibility(visibility: pieController.chartVisibility, animated: false) + percentController.setupMainChart(horizontalRange: fromHorizontalRange, animated: false) + percentController.previewPercentChartRenderer.setup(horizontalRange: fromHorizontalRange, animated: false) + percentController.setConponentsVisible(visible: false, animated: false) + } + + percentController.willAppear(animated: animated) + pieController.willDisappear(animated: animated) + + if animated { + pieController.pieChartRenderer.drawPie = false + percentController.mainPecentChartRenderer.isEnabled = false + + setupTransitionRenderer() + + transitionRenderer.animate(fromDataToPie: false, animated: true) { + self.pieController.pieChartRenderer.drawPie = true + self.percentController.mainPecentChartRenderer.isEnabled = true + } + } + } + + self.setBackButtonVisibilityClosure?(isZoomed, animated) + } + + func setupTransitionRenderer() { + transitionRenderer.setup(verticalRange: percentController.currentVerticalMainChartRange, animated: false) + transitionRenderer.setup(horizontalRange: percentController.currentHorizontalMainChartRange, animated: false) + transitionRenderer.visiblePieComponents = pieController.visiblePieDataWithCurrentPreviewRange + transitionRenderer.visiblePercentageData = percentController.currentlyVisiblePercentageData + } + + override func updateChartsVisibility(visibility: [Bool], animated: Bool) { + if isZoomed { + pieController.updateChartsVisibility(visibility: visibility, animated: animated) + } else { + percentController.updateChartsVisibility(visibility: visibility, animated: animated) + } + } + + var visibleChartValues: [ChartsCollection.Chart] { + let visibility = isZoomed ? pieController.chartVisibility : percentController.chartVisibility + let collection = isZoomed ? pieController.chartsCollection : percentController.chartsCollection + let visibleCharts: [ChartsCollection.Chart] = visibility.enumerated().compactMap { args in + args.element ? collection.chartValues[args.offset] : nil + } + return visibleCharts + } + + override var actualChartVisibility: [Bool] { + return isZoomed ? pieController.chartVisibility : percentController.chartVisibility + } + + override var actualChartsCollection: ChartsCollection { + return isZoomed ? pieController.chartsCollection : percentController.chartsCollection + } + + override func chartInteractionDidBegin(point: CGPoint) { + if isZoomed { + pieController.chartInteractionDidBegin(point: point) + } else { + percentController.chartInteractionDidBegin(point: point) + } + } + + override func chartInteractionDidEnd() { + if isZoomed { + pieController.chartInteractionDidEnd() + } else { + percentController.chartInteractionDidEnd() + } + } + + override var drawChartVisibity: Bool { + return true + } + + override var currentChartHorizontalRangeFraction: ClosedRange { + if isZoomed { + return pieController.currentChartHorizontalRangeFraction + } else { + return percentController.currentChartHorizontalRangeFraction + } + } + + override func cancelChartInteraction() { + if isZoomed { + return pieController.hideDetailsView(animated: true) + } else { + return percentController.hideDetailsView(animated: true) + } + } + + override func didTapZoomIn(date: Date) { + guard isZoomed == false else { return } + cancelChartInteraction() + let currentCollection = percentController.chartsCollection + let range: Int = Constants.zoomedRange + guard let (foundDate, index) = percentController.findClosestDateTo(dateToFind: date) else { return } + var lowIndex = max(0, index - range / 2) + var highIndex = min(currentCollection.axisValues.count - 1, index + range / 2) + if lowIndex == 0 { + highIndex = lowIndex + (range - 1) + } else if highIndex == currentCollection.axisValues.count - 1 { + lowIndex = highIndex - (range - 1) + } + + let newValues = currentCollection.chartValues.map { chart in + return ChartsCollection.Chart(color: chart.color, + name: chart.name, + values: Array(chart.values[(lowIndex...highIndex)])) + } + let newCollection = ChartsCollection(axisValues: Array(currentCollection.axisValues[(lowIndex...highIndex)]), + chartValues: newValues) + let selectedRange = CGFloat(foundDate.timeIntervalSince1970 - .day)...CGFloat(foundDate.timeIntervalSince1970) + pieController.initialize(chartsCollection: newCollection, initialDate: date, totalHorizontalRange: 0...1, totalVerticalRange: 0...1) + pieController.initialHorizontalRange = selectedRange + + switchToChart(chartsCollection: newCollection, isZoomed: true, animated: true) + } + + override func didTapZoomOut() { + self.pieController.deselectSegment(completion: { [weak self] in + guard let self = self else { return } + self.switchToChart(chartsCollection: self.percentController.chartsCollection, isZoomed: false, animated: true) + }) + } + + override func updateChartRange(_ rangeFraction: ClosedRange) { + if isZoomed { + return pieController.chartRangeFractionDidUpdated(rangeFraction) + } else { + return percentController.chartRangeFractionDidUpdated(rangeFraction) + } + } + + override func apply(colorMode: ColorMode, animated: Bool) { + super.apply(colorMode: colorMode, animated: animated) + + pieController.apply(colorMode: colorMode, animated: animated) + percentController.apply(colorMode: colorMode, animated: animated) + transitionRenderer.backgroundColor = colorMode.chartBackgroundColor + } +} diff --git a/submodules/Charts/Sources/Charts/Controllers/Percent And Pie/PieChartComponentController.swift b/submodules/Charts/Sources/Charts/Controllers/Percent And Pie/PieChartComponentController.swift new file mode 100644 index 0000000000..68c3541912 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Controllers/Percent And Pie/PieChartComponentController.swift @@ -0,0 +1,198 @@ +// +// PieChartComponentController.swift +// GraphTest +// +// Created by Andrei Salavei on 4/14/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class PieChartComponentController: GeneralChartComponentController { + let pieChartRenderer: PieChartRenderer + let previewBarChartRenderer: BarChartRenderer + var barWidth: CGFloat = 1 + + var chartBars: BarChartRenderer.BarsData = .blank + + init(isZoomed: Bool, + pieChartRenderer: PieChartRenderer, + previewBarChartRenderer: BarChartRenderer) { + self.pieChartRenderer = pieChartRenderer + self.previewBarChartRenderer = previewBarChartRenderer + super.init(isZoomed: isZoomed) + } + + override func initialize(chartsCollection: ChartsCollection, initialDate: Date, totalHorizontalRange _: ClosedRange, totalVerticalRange _: ClosedRange) { + let (width, chartBars, totalHorizontalRange, _) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection) + self.barWidth = width + self.chartBars = chartBars + super.initialize(chartsCollection: chartsCollection, + initialDate: initialDate, + totalHorizontalRange: totalHorizontalRange, + totalVerticalRange: BaseConstants.defaultRange) + + self.previewBarChartRenderer.bars = chartBars + self.previewBarChartRenderer.fillToTop = true + + pieChartRenderer.valuesFormatter = PercentConstants.percentValueFormatter + pieChartRenderer.setup(horizontalRange: initialHorizontalRange, animated: false) + previewBarChartRenderer.setup(verticalRange: initialVerticalRange, animated: false) + previewBarChartRenderer.setup(horizontalRange: initialHorizontalRange, animated: false) + + pieChartRenderer.updatePercentageData(pieDataWithCurrentPreviewRange, animated: false) + pieChartRenderer.selectSegmentAt(at: nil, animated: false) + } + + private var pieDataWithCurrentPreviewRange: [PieChartRenderer.PieComponent] { + let range = currentHorizontalMainChartRange + var pieComponents = chartsCollection.chartValues.map { PieChartRenderer.PieComponent(color: $0.color, + value: 0) } + guard var valueIndex = chartsCollection.axisValues.firstIndex(where: { CGFloat($0.timeIntervalSince1970) > (range.lowerBound + 1)}) else { + return pieComponents + } + var count = 0 + while valueIndex < chartsCollection.axisValues.count, CGFloat(chartsCollection.axisValues[valueIndex].timeIntervalSince1970) <= range.upperBound { + count += 1 + for pieIndex in pieComponents.indices { + pieComponents[pieIndex].value += CGFloat(chartsCollection.chartValues[pieIndex].values[valueIndex]) + } + valueIndex += 1 + } + return pieComponents + } + + var visiblePieDataWithCurrentPreviewRange: [PieChartRenderer.PieComponent] { + let currentData = pieDataWithCurrentPreviewRange + return chartVisibility.enumerated().compactMap { $0.element ? currentData[$0.offset] : nil } + } + + override func willAppear(animated: Bool) { + pieChartRenderer.setup(horizontalRange: initialHorizontalRange, animated: animated) + pieChartRenderer.setVisible(true, animated: animated) + + previewBarChartRenderer.setup(verticalRange: totalVerticalRange, animated: animated) + previewBarChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated) + previewBarChartRenderer.setVisible(true, animated: animated) + + updatePreviewRangeClosure?(currentChartHorizontalRangeFraction, animated) + pieChartRenderer.updatePercentageData(pieDataWithCurrentPreviewRange, animated: false) + + super.willAppear(animated: animated) + } + + override func setupChartRangePaging() { + let valuesCount = chartsCollection.axisValues.count + guard valuesCount > 0 else { return } + chartRangePagingClosure?(true, 1.0 / CGFloat(valuesCount)) + } + + override func chartRangeDidUpdated(_ updatedRange: ClosedRange) { + if isChartInteractionBegun { + chartInteractionDidBegin(point: lastChartInteractionPoint) + } + initialHorizontalRange = updatedRange + + setupMainChart(horizontalRange: updatedRange, animated: true) + updateSelectedDataLabelIfNeeded() + } + + func setupMainChart(horizontalRange: ClosedRange, animated: Bool) { + pieChartRenderer.setup(horizontalRange: horizontalRange, animated: animated) + pieChartRenderer.updatePercentageData(pieDataWithCurrentPreviewRange, animated: animated) + } + + override func updateChartsVisibility(visibility: [Bool], animated: Bool) { + super.updateChartsVisibility(visibility: visibility, animated: animated) + for (index, isVisible) in visibility.enumerated() { + pieChartRenderer.setComponentVisible(isVisible, at: index, animated: animated) + previewBarChartRenderer.setComponentVisible(isVisible, at: index, animated: animated) + } + if let segment = pieChartRenderer.selectedSegment { + if !visibility[segment] { + pieChartRenderer.selectSegmentAt(at: nil, animated: true) + } + } + updateSelectedDataLabelIfNeeded() + } + + func deselectSegment(completion: @escaping () -> Void) { + if pieChartRenderer.hasSelectedSegments { + hideDetailsView(animated: true) + pieChartRenderer.selectSegmentAt(at: nil, animated: true) + DispatchQueue.main.asyncAfter(deadline: .now() + .defaultDuration / 2) { + completion() + } + } else { + completion() + } + } + + func updateSelectedDataLabelIfNeeded() { + if let segment = pieChartRenderer.selectedSegment { + self.setDetailsChartVisibleClosure?(true, true) + self.setDetailsViewModel?(chartDetailsViewModel(segmentInde: segment), false) + self.setDetailsViewPositionClosure?(chartFrame().width / 4) + } else { + self.setDetailsChartVisibleClosure?(false, true) + } + } + + func chartDetailsViewModel(segmentInde: Int) -> ChartDetailsViewModel { + let pieItem = pieDataWithCurrentPreviewRange[segmentInde] + let title = chartsCollection.chartValues[segmentInde].name + let valueString = BaseConstants.detailsNumberFormatter.string(from: pieItem.value) + let viewModel = ChartDetailsViewModel(title: "", + showArrow: false, + showPrefixes: false, + values: [ChartDetailsViewModel.Value(prefix: nil, + title: title, + value: valueString, + color: pieItem.color, + visible: true)], + totalValue: nil, + tapAction: nil) + return viewModel + } + + override var currentMainRangeRenderer: BaseChartRenderer { + return pieChartRenderer + } + + override var currentPreviewRangeRenderer: BaseChartRenderer { + return previewBarChartRenderer + } + + var lastInteractionPoint: CGPoint = .zero + override func chartInteractionDidBegin(point: CGPoint) { + lastInteractionPoint = point + } + + override func chartInteractionDidEnd() { + if let segment = pieChartRenderer.selectedItemIndex(at: lastInteractionPoint) { + if pieChartRenderer.selectedSegment == segment { + pieChartRenderer.selectSegmentAt(at: nil, animated: true) + } else { + pieChartRenderer.selectSegmentAt(at: segment, animated: true) + } + updateSelectedDataLabelIfNeeded() + } + } + + override func hideDetailsView(animated: Bool) { + pieChartRenderer.selectSegmentAt(at: nil, animated: animated) + updateSelectedDataLabelIfNeeded() + } + + override func updateChartRangeTitle(animated: Bool) { + let fromDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.lowerBound) + .day + 1) + let toDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.upperBound)) + if Calendar.utc.startOfDay(for: fromDate) == Calendar.utc.startOfDay(for: toDate) { + let stirng = BaseConstants.headerFullZoomedFormatter.string(from: fromDate) + self.setChartTitleClosure?(stirng, animated) + } else { + let stirng = "\(BaseConstants.headerMediumRangeFormatter.string(from: fromDate)) - \(BaseConstants.headerMediumRangeFormatter.string(from: toDate))" + self.setChartTitleClosure?(stirng, animated) + } + } +} diff --git a/submodules/Charts/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift b/submodules/Charts/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift new file mode 100644 index 0000000000..2e1894465b --- /dev/null +++ b/submodules/Charts/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift @@ -0,0 +1,226 @@ +// +// BarsComponentController.swift +// GraphTest +// +// Created by Andrei Salavei on 4/14/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class BarsComponentController: GeneralChartComponentController { + let mainBarsRenderer: BarChartRenderer + let horizontalScalesRenderer: HorizontalScalesRenderer + let verticalScalesRenderer: VerticalScalesRenderer + + let previewBarsChartRenderer: BarChartRenderer + private(set) var barsWidth: CGFloat = 1 + + private (set) var chartBars: BarChartRenderer.BarsData = .blank + + init(isZoomed: Bool, + mainBarsRenderer: BarChartRenderer, + horizontalScalesRenderer: HorizontalScalesRenderer, + verticalScalesRenderer: VerticalScalesRenderer, + previewBarsChartRenderer: BarChartRenderer) { + self.mainBarsRenderer = mainBarsRenderer + self.horizontalScalesRenderer = horizontalScalesRenderer + self.verticalScalesRenderer = verticalScalesRenderer + self.previewBarsChartRenderer = previewBarsChartRenderer + + self.mainBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel + self.previewBarsChartRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel + + super.init(isZoomed: isZoomed) + } + + override func initialize(chartsCollection: ChartsCollection, initialDate: Date, totalHorizontalRange _: ClosedRange, totalVerticalRange _: ClosedRange) { + let (width, chartBars, totalHorizontalRange, totalVerticalRange) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection) + self.chartBars = chartBars + self.barsWidth = width + + super.initialize(chartsCollection: chartsCollection, + initialDate: initialDate, + totalHorizontalRange: totalHorizontalRange, + totalVerticalRange: totalVerticalRange) + } + + override func setupInitialChartRange(initialDate: Date) { + guard let first = chartsCollection.axisValues.first?.timeIntervalSince1970, + let last = chartsCollection.axisValues.last?.timeIntervalSince1970 else { return } + + let rangeStart = CGFloat(first) + let rangeEnd = CGFloat(last) + + if isZoomed { + let initalDate = CGFloat(initialDate.timeIntervalSince1970) + + initialHorizontalRange = max(initalDate - barsWidth, rangeStart)...min(initalDate + GeneralChartComponentConstants.defaultZoomedRangeLength - barsWidth, rangeEnd) + initialVerticalRange = totalVerticalRange + } else { + super.setupInitialChartRange(initialDate: initialDate) + } + } + + + override func willAppear(animated: Bool) { + mainBarsRenderer.bars = self.chartBars + previewBarsChartRenderer.bars = self.chartBars + + previewBarsChartRenderer.setup(verticalRange: totalVerticalRange, animated: animated) + previewBarsChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated) + + setupMainChart(verticalRange: initialVerticalRange, animated: animated) + setupMainChart(horizontalRange: initialHorizontalRange, animated: animated) + + updateChartVerticalRanges(horizontalRange: initialHorizontalRange, animated: animated) + + super.willAppear(animated: animated) + + updatePreviewRangeClosure?(currentChartHorizontalRangeFraction, animated) + setConponentsVisible(visible: true, animated: animated) + updateHorizontalLimitLabels(animated: animated, forceUpdate: true) + } + + override func chartRangeDidUpdated(_ updatedRange: ClosedRange) { + super.chartRangeDidUpdated(updatedRange) + if !isZoomed { + initialHorizontalRange = updatedRange + } + setupMainChart(horizontalRange: updatedRange, animated: false) + updateHorizontalLimitLabels(animated: true, forceUpdate: false) + updateChartVerticalRanges(horizontalRange: updatedRange, animated: true) + } + + func updateHorizontalLimitLabels(animated: Bool, forceUpdate: Bool) { + updateHorizontalLimitLabels(horizontalScalesRenderer: horizontalScalesRenderer, + horizontalRange: currentHorizontalMainChartRange, + scaleType: isZoomed ? .hour : .day, + forceUpdate: forceUpdate, + animated: animated) + } + + func prepareAppearanceAnimation(horizontalRnage: ClosedRange) { + setupMainChart(horizontalRange: horizontalRnage, animated: false) + setConponentsVisible(visible: false, animated: false) + } + + func setConponentsVisible(visible: Bool, animated: Bool) { + mainBarsRenderer.setVisible(visible, animated: animated) + horizontalScalesRenderer.setVisible(visible, animated: animated) + verticalScalesRenderer.setVisible(visible, animated: animated) + previewBarsChartRenderer.setVisible(visible, animated: animated) + } + + func setupMainChart(horizontalRange: ClosedRange, animated: Bool) { + mainBarsRenderer.setup(horizontalRange: horizontalRange, animated: animated) + horizontalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated) + verticalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated) + } + + var visibleBars: BarChartRenderer.BarsData { + let visibleComponents: [BarChartRenderer.BarsData.Component] = chartVisibility.enumerated().compactMap { args in + args.element ? chartBars.components[args.offset] : nil + } + return BarChartRenderer.BarsData(barWidth: chartBars.barWidth, + locations: chartBars.locations, + components: visibleComponents) + } + + func updateChartVerticalRanges(horizontalRange: ClosedRange, animated: Bool) { + if let range = BarChartRenderer.BarsData.verticalRange(bars: visibleBars, + calculatingRange: horizontalRange, + addBounds: true) { + let (range, labels) = verticalLimitsLabels(verticalRange: range) + if verticalScalesRenderer.verticalRange.end != range { + verticalScalesRenderer.setup(verticalLimitsLabels: labels, animated: animated) + } + verticalScalesRenderer.setVisible(true, animated: animated) + + setupMainChart(verticalRange: range, animated: animated) + } else { + verticalScalesRenderer.setVisible(false, animated: animated) + } + + if let range = BarChartRenderer.BarsData.verticalRange(bars: visibleBars) { + previewBarsChartRenderer.setup(verticalRange: range, animated: animated) + } + } + + func setupMainChart(verticalRange: ClosedRange, animated: Bool) { + mainBarsRenderer.setup(verticalRange: verticalRange, animated: animated) + horizontalScalesRenderer.setup(verticalRange: verticalRange, animated: animated) + verticalScalesRenderer.setup(verticalRange: verticalRange, animated: animated) + } + + override func updateChartsVisibility(visibility: [Bool], animated: Bool) { + super.updateChartsVisibility(visibility: visibility, animated: animated) + for (index, isVisible) in visibility.enumerated() { + mainBarsRenderer.setComponentVisible(isVisible, at: index, animated: animated) + previewBarsChartRenderer.setComponentVisible(isVisible, at: index, animated: animated) + } + updateChartVerticalRanges(horizontalRange: currentHorizontalMainChartRange, animated: true) + } + + var visibleChartValues: [ChartsCollection.Chart] { + let visibleCharts: [ChartsCollection.Chart] = chartVisibility.enumerated().compactMap { args in + args.element ? chartsCollection.chartValues[args.offset] : nil + } + return visibleCharts + } + + override func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel { + var viewModel = super.chartDetailsViewModel(closestDate: closestDate, pointIndex: pointIndex) + let visibleChartValues = self.visibleChartValues + let totalSumm: CGFloat = visibleChartValues.map { CGFloat($0.values[pointIndex]) }.reduce(0, +) + + viewModel.totalValue = ChartDetailsViewModel.Value(prefix: nil, + title: "Total", + value: BaseConstants.detailsNumberFormatter.string(from: totalSumm), + color: .white, + visible: visibleChartValues.count > 1) + return viewModel + } + + override var currentMainRangeRenderer: BaseChartRenderer { + return mainBarsRenderer + } + + override var currentPreviewRangeRenderer: BaseChartRenderer { + return previewBarsChartRenderer + } + + override func showDetailsView(at chartPosition: CGFloat, detailsViewPosition: CGFloat, dataIndex: Int, date: Date, animted: Bool) { + let rangeWithOffset = detailsViewPosition - barsWidth / currentHorizontalMainChartRange.distance * chartFrame().width / 2 + super.showDetailsView(at: chartPosition, detailsViewPosition: rangeWithOffset, dataIndex: dataIndex, date: date, animted: animted) + mainBarsRenderer.setSelectedIndex(dataIndex, animated: true) + } + + override func hideDetailsView(animated: Bool) { + super.hideDetailsView(animated: animated) + + mainBarsRenderer.setSelectedIndex(nil, animated: animated) + } + override func apply(colorMode: ColorMode, animated: Bool) { + super.apply(colorMode: colorMode, animated: animated) + + horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor + verticalScalesRenderer.labelsColor = colorMode.chartLabelsColor + verticalScalesRenderer.axisXColor = colorMode.barChartStrongLinesColor + verticalScalesRenderer.horizontalLinesColor = colorMode.barChartStrongLinesColor + mainBarsRenderer.update(backgroundColor: colorMode.chartBackgroundColor, animated: false) + previewBarsChartRenderer.update(backgroundColor: colorMode.chartBackgroundColor, animated: false) + } + + override func updateChartRangeTitle(animated: Bool) { + let fromDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.lowerBound + barsWidth)) + let toDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.upperBound)) + if Calendar.utc.startOfDay(for: fromDate) == Calendar.utc.startOfDay(for: toDate) { + let stirng = BaseConstants.headerFullZoomedFormatter.string(from: fromDate) + self.setChartTitleClosure?(stirng, animated) + } else { + let stirng = "\(BaseConstants.headerMediumRangeFormatter.string(from: fromDate)) - \(BaseConstants.headerMediumRangeFormatter.string(from: toDate))" + self.setChartTitleClosure?(stirng, animated) + } + } +} diff --git a/submodules/Charts/Sources/Charts/Controllers/Stacked Bars/DailyBarsChartController.swift b/submodules/Charts/Sources/Charts/Controllers/Stacked Bars/DailyBarsChartController.swift new file mode 100644 index 0000000000..ae83803bb1 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Controllers/Stacked Bars/DailyBarsChartController.swift @@ -0,0 +1,249 @@ +// +// DailyBarsChartController.swift +// GraphTest +// +// Created by Andrei Salavei on 4/7/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class DailyBarsChartController: BaseChartController { + let barsController: BarsComponentController + let linesController: LinesComponentController + + override init(chartsCollection: ChartsCollection) { + let horizontalScalesRenderer = HorizontalScalesRenderer() + let verticalScalesRenderer = VerticalScalesRenderer() + barsController = BarsComponentController(isZoomed: false, + mainBarsRenderer: BarChartRenderer(), + horizontalScalesRenderer: horizontalScalesRenderer, + verticalScalesRenderer: verticalScalesRenderer, + previewBarsChartRenderer: BarChartRenderer()) + linesController = LinesComponentController(isZoomed: true, + userLinesTransitionAnimation: false, + mainLinesRenderer: LinesChartRenderer(), + horizontalScalesRenderer: horizontalScalesRenderer, + verticalScalesRenderer: verticalScalesRenderer, + verticalLineRenderer: VerticalLinesRenderer(), + lineBulletsRenerer: LineBulletsRenerer(), + previewLinesChartRenderer: LinesChartRenderer()) + + super.init(chartsCollection: chartsCollection) + + [barsController, linesController].forEach { controller in + controller.chartFrame = { [unowned self] in self.chartFrame() } + controller.cartViewBounds = { [unowned self] in self.cartViewBounds() } + controller.zoomInOnDateClosure = { [unowned self] date in + self.didTapZoomIn(date: date) + } + controller.setChartTitleClosure = { [unowned self] (title, animated) in + self.setChartTitleClosure?(title, animated) + } + controller.setDetailsViewPositionClosure = { [unowned self] (position) in + self.setDetailsViewPositionClosure?(position) + } + controller.setDetailsChartVisibleClosure = { [unowned self] (visible, animated) in + self.setDetailsChartVisibleClosure?(visible, animated) + } + controller.setDetailsViewModel = { [unowned self] (viewModel, animated) in + self.setDetailsViewModel?(viewModel, animated) + } + controller.updatePreviewRangeClosure = { [unowned self] (fraction, animated) in + self.chartRangeUpdatedClosure?(fraction, animated) + } + controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in + self.setChartRangePagingEnabled(isEnabled: isEnabled, minimumSelectionSize: pageSize) + } + } + } + + override var mainChartRenderers: [ChartViewRenderer] { + return [barsController.mainBarsRenderer, + linesController.mainLinesRenderer, + barsController.horizontalScalesRenderer, + barsController.verticalScalesRenderer, + linesController.verticalLineRenderer, + linesController.lineBulletsRenerer, +// performanceRenderer + ] + } + + override var navigationRenderers: [ChartViewRenderer] { + return [barsController.previewBarsChartRenderer, + linesController.previewLinesChartRenderer] + } + + override func initializeChart() { + barsController.initialize(chartsCollection: initialChartsCollection, + initialDate: Date(), + totalHorizontalRange: BaseConstants.defaultRange, + totalVerticalRange: BaseConstants.defaultRange) + switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: false) + } + + func switchToChart(chartsCollection: ChartsCollection, isZoomed: Bool, animated: Bool) { + if animated { + TimeInterval.setDefaultSuration(.expandAnimationDuration) + DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) { + TimeInterval.setDefaultSuration(.osXDuration) + } + } + + super.isZoomed = isZoomed + if isZoomed { + let toHorizontalRange = linesController.initialHorizontalRange + let destinationHorizontalRange = (toHorizontalRange.lowerBound - barsController.barsWidth)...(toHorizontalRange.upperBound - barsController.barsWidth) + let initialChartVerticalRange = lineProportionAnimationRange() + + linesController.mainLinesRenderer.setup(horizontalRange: barsController.currentHorizontalMainChartRange, animated: false) + linesController.previewLinesChartRenderer.setup(horizontalRange: barsController.currentPreviewHorizontalRange, animated: false) + linesController.mainLinesRenderer.setup(verticalRange: initialChartVerticalRange, animated: false) + linesController.previewLinesChartRenderer.setup(verticalRange: initialChartVerticalRange, animated: false) + linesController.mainLinesRenderer.setVisible(false, animated: false) + linesController.previewLinesChartRenderer.setVisible(false, animated: false) + + barsController.setupMainChart(horizontalRange: destinationHorizontalRange, animated: animated) + barsController.previewBarsChartRenderer.setup(horizontalRange: linesController.totalHorizontalRange, animated: animated) + barsController.mainBarsRenderer.setVisible(false, animated: animated) + barsController.previewBarsChartRenderer.setVisible(false, animated: animated) + + linesController.willAppear(animated: animated) + barsController.willDisappear(animated: animated) + + linesController.updateChartsVisibility(visibility: linesController.chartLines.map { _ in true }, animated: false) + } else { + if !linesController.chartsCollection.isBlank { + barsController.hideDetailsView(animated: false) + let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: barsController.visibleBars, + calculatingRange: barsController.initialHorizontalRange) ?? BaseConstants.defaultRange + barsController.mainBarsRenderer.setup(verticalRange: visibleVerticalRange, animated: false) + + let toHorizontalRange = barsController.initialHorizontalRange + let destinationChartVerticalRange = lineProportionAnimationRange() + + linesController.setupMainChart(horizontalRange: toHorizontalRange, animated: animated) + linesController.mainLinesRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated) + linesController.previewLinesChartRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated) + linesController.previewLinesChartRenderer.setup(horizontalRange: barsController.totalHorizontalRange, animated: animated) + linesController.mainLinesRenderer.setVisible(false, animated: animated) + linesController.previewLinesChartRenderer.setVisible(false, animated: animated) + } + + barsController.willAppear(animated: animated) + linesController.willDisappear(animated: animated) + } + + self.setBackButtonVisibilityClosure?(isZoomed, animated) + self.refreshChartToolsClosure?(animated) + } + + override func updateChartsVisibility(visibility: [Bool], animated: Bool) { + if isZoomed { + linesController.updateChartsVisibility(visibility: visibility, animated: animated) + } else { + barsController.updateChartsVisibility(visibility: visibility, animated: animated) + } + } + + var visibleChartValues: [ChartsCollection.Chart] { + let visibility = isZoomed ? linesController.chartVisibility : barsController.chartVisibility + let collection = isZoomed ? linesController.chartsCollection : barsController.chartsCollection + let visibleCharts: [ChartsCollection.Chart] = visibility.enumerated().compactMap { args in + args.element ? collection.chartValues[args.offset] : nil + } + return visibleCharts + } + + override var actualChartVisibility: [Bool] { + return isZoomed ? linesController.chartVisibility : barsController.chartVisibility + } + + override var actualChartsCollection: ChartsCollection { + return isZoomed ? linesController.chartsCollection : barsController.chartsCollection + } + + override func chartInteractionDidBegin(point: CGPoint) { + if isZoomed { + linesController.chartInteractionDidBegin(point: point) + } else { + barsController.chartInteractionDidBegin(point: point) + } + } + + override func chartInteractionDidEnd() { + if isZoomed { + linesController.chartInteractionDidEnd() + } else { + barsController.chartInteractionDidEnd() + } + } + + override var currentChartHorizontalRangeFraction: ClosedRange { + if isZoomed { + return linesController.currentChartHorizontalRangeFraction + } else { + return barsController.currentChartHorizontalRangeFraction + } + } + + override func cancelChartInteraction() { + if isZoomed { + return linesController.hideDetailsView(animated: true) + } else { + return barsController.hideDetailsView(animated: true) + } + } + + override func didTapZoomIn(date: Date) { + guard isZoomed == false else { return } + if isZoomed { + return linesController.hideDetailsView(animated: true) + } + self.getDetailsData?(date, { updatedCollection in + if let updatedCollection = updatedCollection { + self.linesController.initialize(chartsCollection: updatedCollection, + initialDate: date, + totalHorizontalRange: 0...1, + totalVerticalRange: 0...1) + self.switchToChart(chartsCollection: updatedCollection, isZoomed: true, animated: true) + } + }) + } + + func lineProportionAnimationRange() -> ClosedRange { + let visibleLines = self.barsController.chartVisibility.enumerated().compactMap { $0.element ? self.linesController.chartLines[$0.offset] : nil } + let linesRange = LinesChartRenderer.LineData.verticalRange(lines: visibleLines) ?? BaseConstants.defaultRange + let barsRange = BarChartRenderer.BarsData.verticalRange(bars: self.barsController.visibleBars, + calculatingRange: self.linesController.totalHorizontalRange) ?? BaseConstants.defaultRange + let range = 0...(linesRange.upperBound / barsRange.distance * self.barsController.currentVerticalMainChartRange.distance) + return range + } + + override func didTapZoomOut() { + cancelChartInteraction() + switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: true) + } + + override func updateChartRange(_ rangeFraction: ClosedRange) { + if isZoomed { + return linesController.chartRangeFractionDidUpdated(rangeFraction) + } else { + return barsController.chartRangeFractionDidUpdated(rangeFraction) + } + } + + override func apply(colorMode: ColorMode, animated: Bool) { + super.apply(colorMode: colorMode, animated: animated) + + linesController.apply(colorMode: colorMode, animated: animated) + barsController.apply(colorMode: colorMode, animated: animated) + } + + override var drawChartVisibity: Bool { + return isZoomed + } +} + +//TODO: Убрать Performance полоÑки Ñверзу чартов (Ðе забыть) +//TODO: Добавить ховеры на кнопки diff --git a/submodules/Charts/Sources/Charts/Controllers/Stacked Bars/LinesComponentController.swift b/submodules/Charts/Sources/Charts/Controllers/Stacked Bars/LinesComponentController.swift new file mode 100644 index 0000000000..fc39a5f4c6 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Controllers/Stacked Bars/LinesComponentController.swift @@ -0,0 +1,210 @@ +// +// LinesComponentController.swift +// GraphTest +// +// Created by Andrei Salavei on 4/14/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class LinesComponentController: GeneralChartComponentController { + let mainLinesRenderer: LinesChartRenderer + let horizontalScalesRenderer: HorizontalScalesRenderer + let verticalScalesRenderer: VerticalScalesRenderer + let verticalLineRenderer: VerticalLinesRenderer + let lineBulletsRenerer: LineBulletsRenerer + + let previewLinesChartRenderer: LinesChartRenderer + + private let zoomedLinesRenderer = LinesChartRenderer() + private let zoomedPreviewLinesRenderer = LinesChartRenderer() + + private let userLinesTransitionAnimation: Bool + + private(set) var chartLines: [LinesChartRenderer.LineData] = [] + + init(isZoomed: Bool, + userLinesTransitionAnimation: Bool, + mainLinesRenderer: LinesChartRenderer, + horizontalScalesRenderer: HorizontalScalesRenderer, + verticalScalesRenderer: VerticalScalesRenderer, + verticalLineRenderer: VerticalLinesRenderer, + lineBulletsRenerer: LineBulletsRenerer, + previewLinesChartRenderer: LinesChartRenderer) { + self.mainLinesRenderer = mainLinesRenderer + self.horizontalScalesRenderer = horizontalScalesRenderer + self.verticalScalesRenderer = verticalScalesRenderer + self.verticalLineRenderer = verticalLineRenderer + self.lineBulletsRenerer = lineBulletsRenerer + self.previewLinesChartRenderer = previewLinesChartRenderer + self.userLinesTransitionAnimation = userLinesTransitionAnimation + + super.init(isZoomed: isZoomed) + + self.mainLinesRenderer.lineWidth = BaseConstants.mainChartLineWidth + self.mainLinesRenderer.optimizationLevel = BaseConstants.linesChartOptimizationLevel + self.previewLinesChartRenderer.lineWidth = BaseConstants.previewChartLineWidth + self.previewLinesChartRenderer.optimizationLevel = BaseConstants.previewLinesChartOptimizationLevel + + self.lineBulletsRenerer.isEnabled = false + } + + override func initialize(chartsCollection: ChartsCollection, + initialDate: Date, + totalHorizontalRange _: ClosedRange, + totalVerticalRange _: ClosedRange) { + let (chartLines, totalHorizontalRange, totalVerticalRange) = LinesChartRenderer.LineData.initialComponents(chartsCollection: chartsCollection) + self.chartLines = chartLines + + self.lineBulletsRenerer.bullets = self.chartLines.map { LineBulletsRenerer.Bullet(coordinate: $0.points.first ?? .zero, + color: $0.color)} + + super.initialize(chartsCollection: chartsCollection, + initialDate: initialDate, + totalHorizontalRange: totalHorizontalRange, + totalVerticalRange: totalVerticalRange) + + self.mainLinesRenderer.setup(verticalRange: totalVerticalRange, animated: true) + } + + override func willAppear(animated: Bool) { + mainLinesRenderer.setLines(lines: self.chartLines, animated: animated && userLinesTransitionAnimation) + previewLinesChartRenderer.setLines(lines: self.chartLines, animated: animated && userLinesTransitionAnimation) + + previewLinesChartRenderer.setup(verticalRange: totalVerticalRange, animated: animated) + previewLinesChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated) + + setupMainChart(verticalRange: initialVerticalRange, animated: animated) + setupMainChart(horizontalRange: initialHorizontalRange, animated: animated) + + updateChartVerticalRanges(horizontalRange: initialHorizontalRange, animated: animated) + + super.willAppear(animated: animated) + + updatePreviewRangeClosure?(currentChartHorizontalRangeFraction, animated) + setConponentsVisible(visible: true, animated: animated) + updateHorizontalLimitLabels(animated: animated, forceUpdate: true) + } + + override func chartRangeDidUpdated(_ updatedRange: ClosedRange) { + super.chartRangeDidUpdated(updatedRange) + if !isZoomed { + initialHorizontalRange = updatedRange + } + setupMainChart(horizontalRange: updatedRange, animated: false) + updateHorizontalLimitLabels(animated: true, forceUpdate: false) + updateChartVerticalRanges(horizontalRange: updatedRange, animated: true) + } + + func updateHorizontalLimitLabels(animated: Bool, forceUpdate: Bool) { + updateHorizontalLimitLabels(horizontalScalesRenderer: horizontalScalesRenderer, + horizontalRange: currentHorizontalMainChartRange, + scaleType: isZoomed ? .hour : .day, + forceUpdate: forceUpdate, + animated: animated) + } + + func prepareAppearanceAnimation(horizontalRnage: ClosedRange) { + setupMainChart(horizontalRange: horizontalRnage, animated: false) + setConponentsVisible(visible: false, animated: false) + } + + func setConponentsVisible(visible: Bool, animated: Bool) { + mainLinesRenderer.setVisible(visible, animated: animated) + horizontalScalesRenderer.setVisible(visible, animated: animated) + verticalScalesRenderer.setVisible(visible, animated: animated) + verticalLineRenderer.setVisible(visible, animated: animated) + previewLinesChartRenderer.setVisible(visible, animated: animated) + lineBulletsRenerer.setVisible(visible, animated: animated) + } + + func setupMainChart(horizontalRange: ClosedRange, animated: Bool) { + mainLinesRenderer.setup(horizontalRange: horizontalRange, animated: animated) + horizontalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated) + verticalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated) + verticalLineRenderer.setup(horizontalRange: horizontalRange, animated: animated) + lineBulletsRenerer.setup(horizontalRange: horizontalRange, animated: animated) + } + + var visibleLines: [LinesChartRenderer.LineData] { + return chartVisibility.enumerated().compactMap { $0.element ? chartLines[$0.offset] : nil } + } + + func updateChartVerticalRanges(horizontalRange: ClosedRange, animated: Bool) { + if let range = LinesChartRenderer.LineData.verticalRange(lines: visibleLines, + calculatingRange: horizontalRange, + addBounds: true) { + let (range, labels) = verticalLimitsLabels(verticalRange: range) + if verticalScalesRenderer.verticalRange.end != range { + verticalScalesRenderer.setup(verticalLimitsLabels: labels, animated: animated) + } + + setupMainChart(verticalRange: range, animated: animated) + verticalScalesRenderer.setVisible(true, animated: animated) + } else { + verticalScalesRenderer.setVisible(false, animated: animated) + } + + if let range = LinesChartRenderer.LineData.verticalRange(lines: visibleLines) { + previewLinesChartRenderer.setup(verticalRange: range, animated: animated) + } + } + + func setupMainChart(verticalRange: ClosedRange, animated: Bool) { + mainLinesRenderer.setup(verticalRange: verticalRange, animated: animated) + horizontalScalesRenderer.setup(verticalRange: verticalRange, animated: animated) + verticalScalesRenderer.setup(verticalRange: verticalRange, animated: animated) + verticalLineRenderer.setup(verticalRange: verticalRange, animated: animated) + lineBulletsRenerer.setup(verticalRange: verticalRange, animated: animated) + } + + override func updateChartsVisibility(visibility: [Bool], animated: Bool) { + super.updateChartsVisibility(visibility: visibility, animated: animated) + for (index, isVisible) in visibility.enumerated() { + mainLinesRenderer.setLineVisible(isVisible, at: index, animated: animated) + previewLinesChartRenderer.setLineVisible(isVisible, at: index, animated: animated) + lineBulletsRenerer.setLineVisible(isVisible, at: index, animated: animated) + } + updateChartVerticalRanges(horizontalRange: currentHorizontalMainChartRange, animated: true) + } + + override var currentMainRangeRenderer: BaseChartRenderer { + return mainLinesRenderer + } + + override var currentPreviewRangeRenderer: BaseChartRenderer { + return previewLinesChartRenderer + } + + override func showDetailsView(at chartPosition: CGFloat, detailsViewPosition: CGFloat, dataIndex: Int, date: Date, animted: Bool) { + super.showDetailsView(at: chartPosition, detailsViewPosition: detailsViewPosition, dataIndex: dataIndex, date: date, animted: animted) + verticalLineRenderer.values = [chartPosition] + verticalLineRenderer.isEnabled = true + + lineBulletsRenerer.isEnabled = true + lineBulletsRenerer.setVisible(true, animated: animted) + lineBulletsRenerer.bullets = chartLines.compactMap { chart in + return LineBulletsRenerer.Bullet(coordinate: chart.points[dataIndex], color: chart.color) + } + } + + override func hideDetailsView(animated: Bool) { + super.hideDetailsView(animated: animated) + + verticalLineRenderer.values = [] + verticalLineRenderer.isEnabled = false + lineBulletsRenerer.isEnabled = false + } + + override func apply(colorMode: ColorMode, animated: Bool) { + super.apply(colorMode: colorMode, animated: animated) + + horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor + verticalScalesRenderer.labelsColor = colorMode.chartLabelsColor + verticalScalesRenderer.axisXColor = colorMode.chartStrongLinesColor + verticalScalesRenderer.horizontalLinesColor = colorMode.chartHelperLinesColor + lineBulletsRenerer.setInnerColor(colorMode.chartBackgroundColor, animated: animated) + verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor + } +} diff --git a/submodules/Charts/Sources/Charts/Controllers/Stacked Bars/StackedBarsChartController.swift b/submodules/Charts/Sources/Charts/Controllers/Stacked Bars/StackedBarsChartController.swift new file mode 100644 index 0000000000..ab836d00d2 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Controllers/Stacked Bars/StackedBarsChartController.swift @@ -0,0 +1,243 @@ +// +// StackedBarsChartController.swift +// GraphTest +// +// Created by Andrei Salavei on 4/7/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class StackedBarsChartController: BaseChartController { + let barsController: BarsComponentController + let zoomedBarsController: BarsComponentController + + override init(chartsCollection: ChartsCollection) { + let horizontalScalesRenderer = HorizontalScalesRenderer() + let verticalScalesRenderer = VerticalScalesRenderer() + barsController = BarsComponentController(isZoomed: false, + mainBarsRenderer: BarChartRenderer(), + horizontalScalesRenderer: horizontalScalesRenderer, + verticalScalesRenderer: verticalScalesRenderer, + previewBarsChartRenderer: BarChartRenderer()) + zoomedBarsController = BarsComponentController(isZoomed: true, + mainBarsRenderer: BarChartRenderer(), + horizontalScalesRenderer: horizontalScalesRenderer, + verticalScalesRenderer: verticalScalesRenderer, + previewBarsChartRenderer: BarChartRenderer()) + + super.init(chartsCollection: chartsCollection) + + [barsController, zoomedBarsController].forEach { controller in + controller.chartFrame = { [unowned self] in self.chartFrame() } + controller.cartViewBounds = { [unowned self] in self.cartViewBounds() } + controller.zoomInOnDateClosure = { [unowned self] date in + self.didTapZoomIn(date: date) + } + controller.setChartTitleClosure = { [unowned self] (title, animated) in + self.setChartTitleClosure?(title, animated) + } + controller.setDetailsViewPositionClosure = { [unowned self] (position) in + self.setDetailsViewPositionClosure?(position) + } + controller.setDetailsChartVisibleClosure = { [unowned self] (visible, animated) in + self.setDetailsChartVisibleClosure?(visible, animated) + } + controller.setDetailsViewModel = { [unowned self] (viewModel, animated) in + self.setDetailsViewModel?(viewModel, animated) + } + controller.updatePreviewRangeClosure = { [unowned self] (fraction, animated) in + self.chartRangeUpdatedClosure?(fraction, animated) + } + controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in + self.setChartRangePagingEnabled(isEnabled: isEnabled, minimumSelectionSize: pageSize) + } + } + } + + override var mainChartRenderers: [ChartViewRenderer] { + return [barsController.mainBarsRenderer, + zoomedBarsController.mainBarsRenderer, + barsController.horizontalScalesRenderer, + barsController.verticalScalesRenderer, +// performanceRenderer + ] + } + + override var navigationRenderers: [ChartViewRenderer] { + return [barsController.previewBarsChartRenderer, + zoomedBarsController.previewBarsChartRenderer] + } + + override func initializeChart() { + barsController.initialize(chartsCollection: initialChartsCollection, + initialDate: Date(), + totalHorizontalRange: BaseConstants.defaultRange, + totalVerticalRange: BaseConstants.defaultRange) + switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: false) + } + + func switchToChart(chartsCollection: ChartsCollection, isZoomed: Bool, animated: Bool) { + if animated { + TimeInterval.setDefaultSuration(.expandAnimationDuration) + DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) { + TimeInterval.setDefaultSuration(.osXDuration) + } + } + + super.isZoomed = isZoomed + if isZoomed { + let toHorizontalRange = zoomedBarsController.initialHorizontalRange + let destinationHorizontalRange = (toHorizontalRange.lowerBound - barsController.barsWidth)...(toHorizontalRange.upperBound - barsController.barsWidth) + let verticalVisibleRange = barsController.currentVerticalMainChartRange + let initialVerticalRange = verticalVisibleRange.lowerBound...(verticalVisibleRange.upperBound + verticalVisibleRange.distance * 10) + + zoomedBarsController.mainBarsRenderer.setup(horizontalRange: barsController.currentHorizontalMainChartRange, animated: false) + zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.currentPreviewHorizontalRange, animated: false) + zoomedBarsController.mainBarsRenderer.setup(verticalRange: initialVerticalRange, animated: false) + zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: initialVerticalRange, animated: false) + zoomedBarsController.mainBarsRenderer.setVisible(true, animated: false) + zoomedBarsController.previewBarsChartRenderer.setVisible(true, animated: false) + + barsController.setupMainChart(horizontalRange: destinationHorizontalRange, animated: animated) + barsController.previewBarsChartRenderer.setup(horizontalRange: zoomedBarsController.totalHorizontalRange, animated: animated) + barsController.mainBarsRenderer.setVisible(false, animated: animated) + barsController.previewBarsChartRenderer.setVisible(false, animated: animated) + + zoomedBarsController.willAppear(animated: animated) + barsController.willDisappear(animated: animated) + + zoomedBarsController.updateChartsVisibility(visibility: barsController.chartVisibility, animated: false) + zoomedBarsController.mainBarsRenderer.setup(verticalRange: zoomedBarsController.currentVerticalMainChartRange, animated: animated, timeFunction: .easeOut) + zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: zoomedBarsController.currentPreviewVerticalRange, animated: animated, timeFunction: .easeOut) + } else { + if !zoomedBarsController.chartsCollection.isBlank { + barsController.hideDetailsView(animated: false) + barsController.chartVisibility = zoomedBarsController.chartVisibility + let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: barsController.visibleBars, + calculatingRange: barsController.initialHorizontalRange) ?? BaseConstants.defaultRange + barsController.mainBarsRenderer.setup(verticalRange: visibleVerticalRange, animated: false) + + let toHorizontalRange = barsController.initialHorizontalRange + + let verticalVisibleRange = barsController.initialVerticalRange + let targetVerticalRange = verticalVisibleRange.lowerBound...(verticalVisibleRange.upperBound + verticalVisibleRange.distance * 10) + + zoomedBarsController.setupMainChart(horizontalRange: toHorizontalRange, animated: animated) + zoomedBarsController.mainBarsRenderer.setup(verticalRange: targetVerticalRange, animated: animated, timeFunction: .easeIn) + zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: targetVerticalRange, animated: animated, timeFunction: .easeIn) + zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.totalHorizontalRange, animated: animated) + DispatchQueue.main.asyncAfter(deadline: .now() + .defaultDuration) { [weak self] in + self?.zoomedBarsController.mainBarsRenderer.setVisible(false, animated: false) + self?.zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: false) + } + } + + barsController.willAppear(animated: animated) + zoomedBarsController.willDisappear(animated: animated) + + if !zoomedBarsController.chartsCollection.isBlank { + barsController.updateChartsVisibility(visibility: zoomedBarsController.chartVisibility, animated: false) + } + } + + self.setBackButtonVisibilityClosure?(isZoomed, animated) + } + + override func updateChartsVisibility(visibility: [Bool], animated: Bool) { + if isZoomed { + zoomedBarsController.updateChartsVisibility(visibility: visibility, animated: animated) + } else { + barsController.updateChartsVisibility(visibility: visibility, animated: animated) + } + } + + var visibleChartValues: [ChartsCollection.Chart] { + let visibility = isZoomed ? zoomedBarsController.chartVisibility : barsController.chartVisibility + let collection = isZoomed ? zoomedBarsController.chartsCollection : barsController.chartsCollection + let visibleCharts: [ChartsCollection.Chart] = visibility.enumerated().compactMap { args in + args.element ? collection.chartValues[args.offset] : nil + } + return visibleCharts + } + + override var actualChartVisibility: [Bool] { + return isZoomed ? zoomedBarsController.chartVisibility : barsController.chartVisibility + } + + override var actualChartsCollection: ChartsCollection { + return isZoomed ? zoomedBarsController.chartsCollection : barsController.chartsCollection + } + + override func chartInteractionDidBegin(point: CGPoint) { + if isZoomed { + zoomedBarsController.chartInteractionDidBegin(point: point) + } else { + barsController.chartInteractionDidBegin(point: point) + } + } + + override func chartInteractionDidEnd() { + if isZoomed { + zoomedBarsController.chartInteractionDidEnd() + } else { + barsController.chartInteractionDidEnd() + } + } + + override var drawChartVisibity: Bool { + return true + } + + override var currentChartHorizontalRangeFraction: ClosedRange { + if isZoomed { + return zoomedBarsController.currentChartHorizontalRangeFraction + } else { + return barsController.currentChartHorizontalRangeFraction + } + } + + override func cancelChartInteraction() { + if isZoomed { + return zoomedBarsController.hideDetailsView(animated: true) + } else { + return barsController.hideDetailsView(animated: true) + } + } + + override func didTapZoomIn(date: Date) { + guard isZoomed == false else { return } + if isZoomed { + return zoomedBarsController.hideDetailsView(animated: true) + } + self.getDetailsData?(date, { updatedCollection in + if let updatedCollection = updatedCollection { + self.zoomedBarsController.initialize(chartsCollection: updatedCollection, + initialDate: date, + totalHorizontalRange: 0...1, + totalVerticalRange: 0...1) + self.switchToChart(chartsCollection: updatedCollection, isZoomed: true, animated: true) + } + }) + } + + override func didTapZoomOut() { + cancelChartInteraction() + switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: true) + } + + override func updateChartRange(_ rangeFraction: ClosedRange) { + if isZoomed { + return zoomedBarsController.chartRangeFractionDidUpdated(rangeFraction) + } else { + return barsController.chartRangeFractionDidUpdated(rangeFraction) + } + } + + override func apply(colorMode: ColorMode, animated: Bool) { + super.apply(colorMode: colorMode, animated: animated) + + zoomedBarsController.apply(colorMode: colorMode, animated: animated) + barsController.apply(colorMode: colorMode, animated: animated) + } +} diff --git a/submodules/Charts/Sources/Charts/Renderes/BarChartRenderer.swift b/submodules/Charts/Sources/Charts/Renderes/BarChartRenderer.swift new file mode 100644 index 0000000000..73a6b52f42 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Renderes/BarChartRenderer.swift @@ -0,0 +1,293 @@ +// +// BarChartRenderer.swift +// GraphTest +// +// Created by Andrei Salavei on 4/7/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class BarChartRenderer: BaseChartRenderer { + struct BarsData { + static let blank = BarsData(barWidth: 1, locations: [], components: []) + var barWidth: CGFloat + var locations: [CGFloat] + var components: [Component] + + struct Component { + var color: UIColor + var values: [CGFloat] + } + } + + var fillToTop: Bool = false + private(set) lazy var selectedIndexAnimator: AnimationController = { + return AnimationController(current: 0, refreshClosure: self.refreshClosure) + }() + func setSelectedIndex(_ index: Int?, animated: Bool) { + let destinationValue: CGFloat = (index == nil) ? 0 : 1 + if animated { + if index != nil { + selectedBarIndex = index + } + self.selectedIndexAnimator.completionClosure = { + self.selectedBarIndex = index + } + guard self.selectedIndexAnimator.end != destinationValue else { return } + self.selectedIndexAnimator.animate(to: destinationValue, duration: .defaultDuration) + } else { + self.selectedIndexAnimator.set(current: destinationValue) + self.selectedBarIndex = index + } + } + + private var selectedBarIndex: Int? { + didSet { + setNeedsDisplay() + } + } + var generalUnselectedAlpha: CGFloat = 0.5 + + private var componentsAnimators: [AnimationController] = [] + var bars: BarsData = BarsData(barWidth: 1, locations: [], components: []) { + willSet { + if bars.components.count != newValue.components.count { + componentsAnimators = newValue.components.map { _ in AnimationController(current: 1, refreshClosure: self.refreshClosure) } + } + } + didSet { + setNeedsDisplay() + } + } + + func setComponentVisible(_ isVisible: Bool, at index: Int, animated: Bool) { + componentsAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0) + } + + private lazy var backgroundColorAnimator = AnimationController(current: UIColorContainer(color: .white), refreshClosure: refreshClosure) + func update(backgroundColor: UIColor, animated: Bool) { + if animated { + backgroundColorAnimator.animate(to: UIColorContainer(color: backgroundColor), duration: .defaultDuration) + } else { + backgroundColorAnimator.set(current: UIColorContainer(color: backgroundColor)) + } + } + + override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) { + guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return } + let chartsAlpha = chartAlphaAnimator.current + if chartsAlpha == 0 { return } + + let range = renderRange(bounds: bounds, chartFrame: chartFrame) + + var selectedPaths: [[CGRect]] = bars.components.map { _ in [] } + var unselectedPaths: [[CGRect]] = bars.components.map { _ in [] } + + if var barIndex = bars.locations.firstIndex(where: { $0 >= range.lowerBound }) { + if fillToTop { + barIndex = max(0, barIndex - 1) + + while barIndex < bars.locations.count { + let currentLocation = bars.locations[barIndex] + let right = transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame).roundedUpToPixelGrid() + let left = transform(toChartCoordinateHorizontal: currentLocation - bars.barWidth, chartFrame: chartFrame).roundedUpToPixelGrid() + + var summ: CGFloat = 0 + for (index, component) in bars.components.enumerated() { + summ += componentsAnimators[index].current * component.values[barIndex] + } + guard summ > 0 else { + barIndex += 1 + continue + } + + var stackedValue: CGFloat = 0 + for (index, component) in bars.components.enumerated() { + let visibilityPercent = componentsAnimators[index].current + if visibilityPercent == 0 { continue } + + let bottomFraction = stackedValue + let topFraction = stackedValue + ((component.values[barIndex] * visibilityPercent) / summ) + + let rect = CGRect(x: left, + y: chartFrame.maxY - chartFrame.height * topFraction, + width: right - left, + height: chartFrame.height * (topFraction - bottomFraction)) + if selectedBarIndex == barIndex { + selectedPaths[index].append(rect) + } else { + unselectedPaths[index].append(rect) + } + stackedValue = topFraction + } + if currentLocation > range.upperBound { + break + } + barIndex += 1 + } + + for (index, component) in bars.components.enumerated() { + context.saveGState() + context.setFillColor(component.color.withAlphaComponent(chartsAlpha * component.color.alphaValue).cgColor) + context.fill(selectedPaths[index]) + let resultAlpha: CGFloat = 1.0 - (1.0 - generalUnselectedAlpha) * selectedIndexAnimator.current + context.setFillColor(component.color.withAlphaComponent(chartsAlpha * component.color.alphaValue * resultAlpha).cgColor) + context.fill(unselectedPaths[index]) + context.restoreGState() + } + } else { + var selectedPaths: [[CGRect]] = bars.components.map { _ in [] } + barIndex = max(0, barIndex - 1) + + var currentLocation = bars.locations[barIndex] + var leftX = transform(toChartCoordinateHorizontal: currentLocation - bars.barWidth, chartFrame: chartFrame) + var rightX: CGFloat = 0 + + let startPoint = CGPoint(x: leftX, + y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame)) + + var backgourndPaths: [[CGPoint]] = bars.components.map { _ in Array() } + let itemsCount = ((bars.locations.count - barIndex) * 2) + 4 + for path in backgourndPaths.indices { + backgourndPaths[path].reserveCapacity(itemsCount) + backgourndPaths[path].append(startPoint) + } + var maxValues: [CGFloat] = bars.components.map { _ in 0 } + while barIndex < bars.locations.count { + currentLocation = bars.locations[barIndex] + rightX = transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame) + + var stackedValue: CGFloat = 0 + var bottomY: CGFloat = transform(toChartCoordinateVertical: stackedValue, chartFrame: chartFrame) + for (index, component) in bars.components.enumerated() { + let visibilityPercent = componentsAnimators[index].current + if visibilityPercent == 0 { continue } + + let height = component.values[barIndex] * visibilityPercent + stackedValue += height + let topY = transform(toChartCoordinateVertical: stackedValue, chartFrame: chartFrame) + let componentHeight = (bottomY - topY) + maxValues[index] = max(maxValues[index], componentHeight) + if selectedBarIndex == barIndex { + let rect = CGRect(x: leftX, + y: topY, + width: rightX - leftX, + height: componentHeight) + selectedPaths[index].append(rect) + } + backgourndPaths[index].append(CGPoint(x: leftX, y: topY)) + backgourndPaths[index].append(CGPoint(x: rightX, y: topY)) + bottomY = topY + } + if currentLocation > range.upperBound { + break + } + leftX = rightX + barIndex += 1 + } + + let endPoint = CGPoint(x: transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame).roundedUpToPixelGrid(), + y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame)) + let colorOffset = Double((1.0 - (1.0 - generalUnselectedAlpha) * selectedIndexAnimator.current) * chartsAlpha) + + for (index, component) in bars.components.enumerated().reversed() { + if maxValues[index] < optimizationLevel { + continue + } + context.saveGState() + backgourndPaths[index].append(endPoint) + + context.setFillColor(UIColor.valueBetween(start: backgroundColorAnimator.current.color, + end: component.color, + offset: colorOffset).cgColor) + context.beginPath() + context.addLines(between: backgourndPaths[index]) + context.closePath() + context.fillPath() + context.restoreGState() + } + + for (index, component) in bars.components.enumerated().reversed() { + context.setFillColor(component.color.withAlphaComponent(chartsAlpha * component.color.alphaValue).cgColor) + context.fill(selectedPaths[index]) + } + } + } + } +} + +extension BarChartRenderer.BarsData { + static func initialComponents(chartsCollection: ChartsCollection) -> + (width: CGFloat, + chartBars: BarChartRenderer.BarsData, + totalHorizontalRange: ClosedRange, + totalVerticalRange: ClosedRange) { + let width: CGFloat + if chartsCollection.axisValues.count > 1 { + width = CGFloat(abs(chartsCollection.axisValues[1].timeIntervalSince1970 - chartsCollection.axisValues[0].timeIntervalSince1970)) + } else { + width = 1 + } + let components = chartsCollection.chartValues.map { BarChartRenderer.BarsData.Component(color: $0.color, + values: $0.values.map { CGFloat($0) }) } + let chartBars = BarChartRenderer.BarsData(barWidth: width, + locations: chartsCollection.axisValues.map { CGFloat($0.timeIntervalSince1970) }, + components: components) + + + + let totalVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: chartBars) ?? 0...1 + let totalHorizontalRange = BarChartRenderer.BarsData.visibleHorizontalRange(bars: chartBars, width: width) ?? 0...1 + return (width: width, chartBars: chartBars, totalHorizontalRange: totalHorizontalRange, totalVerticalRange: totalVerticalRange) + } + + static func visibleHorizontalRange(bars: BarChartRenderer.BarsData, width: CGFloat) -> ClosedRange? { + guard let firstPoint = bars.locations.first, + let lastPoint = bars.locations.last, + firstPoint <= lastPoint else { + return nil + } + + return (firstPoint - width)...lastPoint + } + + static func verticalRange(bars: BarChartRenderer.BarsData, calculatingRange: ClosedRange? = nil, addBounds: Bool = false) -> ClosedRange? { + guard bars.components.count > 0 else { + return nil + } + if let calculatingRange = calculatingRange { + guard var index = bars.locations.firstIndex(where: { $0 >= calculatingRange.lowerBound && $0 <= calculatingRange.upperBound }) else { + return nil + } + + var vMax: CGFloat = bars.components[0].values[index] + while index < bars.locations.count { + var summ: CGFloat = 0 + for component in bars.components { + summ += component.values[index] + } + vMax = max(vMax, summ) + + if bars.locations[index] > calculatingRange.upperBound { + break + } + index += 1 + } + return 0...vMax + } else { + var index = 0 + + var vMax: CGFloat = bars.components[0].values[index] + while index < bars.locations.count { + var summ: CGFloat = 0 + for component in bars.components { + summ += component.values[index] + } + vMax = max(vMax, summ) + index += 1 + } + return 0...vMax + } + } +} diff --git a/submodules/Charts/Sources/Charts/Renderes/BaseChartRenderer.swift b/submodules/Charts/Sources/Charts/Renderes/BaseChartRenderer.swift new file mode 100644 index 0000000000..63627566e7 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Renderes/BaseChartRenderer.swift @@ -0,0 +1,116 @@ +// +// BaseChartRenderer.swift +// GraphTest +// +// Created by Andrei Salavei on 4/7/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +private let exponentialAnimationTrashold: CGFloat = 100 + +class BaseChartRenderer: ChartViewRenderer { + var containerViews: [UIView] = [] + + var optimizationLevel: CGFloat = 1 { + didSet { + setNeedsDisplay() + } + } + var isEnabled: Bool = true { + didSet { + setNeedsDisplay() + } + } + + private(set) lazy var chartAlphaAnimator: AnimationController = { + return AnimationController(current: 1, refreshClosure: self.refreshClosure) + }() + func setVisible(_ visible: Bool, animated: Bool) { + let destinationValue: CGFloat = visible ? 1 : 0 + guard self.chartAlphaAnimator.end != destinationValue else { return } + if animated { + self.chartAlphaAnimator.animate(to: destinationValue, duration: .defaultDuration) + } else { + self.chartAlphaAnimator.set(current: destinationValue) + } + } + + lazy var horizontalRange = AnimationController>(current: 0...1, refreshClosure: refreshClosure) + lazy var verticalRange = AnimationController>(current: 0...1, refreshClosure: refreshClosure) + + func setup(verticalRange: ClosedRange, animated: Bool, timeFunction: TimeFunction? = nil) { + guard self.verticalRange.end != verticalRange else { + self.verticalRange.timeFunction = timeFunction ?? .linear + return + } + if animated { + let function: TimeFunction + if let timeFunction = timeFunction { + function = timeFunction + } else if self.verticalRange.current.distance > 0 && verticalRange.distance > 0 { + if self.verticalRange.current.distance / verticalRange.distance > exponentialAnimationTrashold { + function = .easeIn + } else if verticalRange.distance / self.verticalRange.current.distance > exponentialAnimationTrashold { + function = .easeOut + } else { + function = .linear + } + } else { + function = .linear + } + + self.verticalRange.animate(to: verticalRange, duration: .defaultDuration, timeFunction: function) + } else { + self.verticalRange.set(current: verticalRange) + } + } + + func setup(horizontalRange: ClosedRange, animated: Bool) { + guard self.horizontalRange.end != horizontalRange else { return } + if animated { + let animationCurve: TimeFunction = self.horizontalRange.current.distance > horizontalRange.distance ? .easeOut : .easeIn + self.horizontalRange.animate(to: horizontalRange, duration: .defaultDuration, timeFunction: animationCurve) + } else { + self.horizontalRange.set(current: horizontalRange) + } + } + + func transform(toChartCoordinateHorizontal x: CGFloat, chartFrame: CGRect) -> CGFloat { + return chartFrame.origin.x + (x - horizontalRange.current.lowerBound) / horizontalRange.current.distance * chartFrame.width + } + + func transform(toChartCoordinateVertical y: CGFloat, chartFrame: CGRect) -> CGFloat { + return chartFrame.height + chartFrame.origin.y - (y - verticalRange.current.lowerBound) / verticalRange.current.distance * chartFrame.height + } + + func transform(toChartCoordinate point: CGPoint, chartFrame: CGRect) -> CGPoint { + return CGPoint(x: transform(toChartCoordinateHorizontal: point.x, chartFrame: chartFrame), + y: transform(toChartCoordinateVertical: point.y, chartFrame: chartFrame)) + } + + func renderRange(bounds: CGRect, chartFrame: CGRect) -> ClosedRange { + let lowerBound = horizontalRange.current.lowerBound - chartFrame.origin.x / chartFrame.width * horizontalRange.current.distance + let upperBound = horizontalRange.current.upperBound + (bounds.width - chartFrame.width - chartFrame.origin.x) / chartFrame.width * horizontalRange.current.distance + guard lowerBound <= upperBound else { + print("Error: Unexpecated bounds range!") + return 0...1 + } + return lowerBound...upperBound + } + + func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) { + fatalError("abstract") + } + + func setNeedsDisplay() { + containerViews.forEach { $0.setNeedsDisplay() } + } + + var refreshClosure: () -> Void { + return { [weak self] in + self?.setNeedsDisplay() + } + } +} diff --git a/submodules/Charts/Sources/Charts/Renderes/ChartDetailsRenderer.swift b/submodules/Charts/Sources/Charts/Renderes/ChartDetailsRenderer.swift new file mode 100644 index 0000000000..ad61ff5dd9 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Renderes/ChartDetailsRenderer.swift @@ -0,0 +1,147 @@ +// +// ChartDetailsRenderer.swift +// GraphTest +// +// Created by Andrei Salavei on 4/13/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class ChartDetailsRenderer: BaseChartRenderer, ColorModeContainer { + private lazy var colorAnimator = AnimationController(current: 1, refreshClosure: refreshClosure) + private var fromColorMode: ColorMode = .day + private var currentColorMode: ColorMode = .day + func apply(colorMode: ColorMode, animated: Bool) { + if currentColorMode != colorMode { + fromColorMode = currentColorMode + currentColorMode = colorMode + if animated { + colorAnimator.set(current: 0) + colorAnimator.animate(to: 1, duration: .defaultDuration) + } else { + colorAnimator.set(current: 1) + } + } + } + + private var valuesAnimators: [AnimationController] = [] + func setValueVisible(_ isVisible: Bool, at index: Int, animated: Bool) { + valuesAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0) + } + var detailsViewModel: ChartDetailsViewModel = .blank { + didSet { + if detailsViewModel.values.count != valuesAnimators.count { + valuesAnimators = detailsViewModel.values.map { _ in AnimationController(current: 1, refreshClosure: refreshClosure) } + } + setNeedsDisplay() + } + } + + var detailsViewPosition: CGFloat = 0 { + didSet { + setNeedsDisplay() + } + } + var detailViewPositionOffset: CGFloat = 10 + var detailViewTopOffset: CGFloat = 10 + private var iconWidth: CGFloat = 10 + private var margins: CGFloat = 10 + private let cornerRadius: CGFloat = 5 + private var rowHeight: CGFloat = 20 + private let titleFont = UIFont.systemFont(ofSize: 14, weight: .bold) + private let prefixFont = UIFont.systemFont(ofSize: 14, weight: .bold) + private let labelsFont = UIFont.systemFont(ofSize: 14, weight: .medium) + private let valuesFont = UIFont.systemFont(ofSize: 14, weight: .bold) + private let labelsColor: UIColor = .black + + private(set) var previousRenderBannerFrame: CGRect = .zero + override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) { + previousRenderBannerFrame = .zero + guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return } + let generalAlpha = chartAlphaAnimator.current + if generalAlpha == 0 { return } + + let widths: [(prefix: CGFloat, label: CGFloat, value: CGFloat)] = detailsViewModel.values.map { value in + var prefixWidth: CGFloat = 0 + if let prefixText = value.prefix { + prefixWidth = (prefixText as NSString).boundingRect(with: bounds.size, + options: .usesLineFragmentOrigin, + attributes: [.font: prefixFont], + context: nil).width.rounded(.up) + margins + } + + let labelWidth = (value.title as NSString).boundingRect(with: bounds.size, + options: .usesLineFragmentOrigin, + attributes: [.font: labelsFont], + context: nil).width.rounded(.up) + margins + + let valueWidth = (value.value as NSString).boundingRect(with: bounds.size, + options: .usesLineFragmentOrigin, + attributes: [.font: valuesFont], + context: nil).width.rounded(.up) + return (prefixWidth, labelWidth, valueWidth) + } + + let titleWidth = (detailsViewModel.title as NSString).boundingRect(with: bounds.size, + options: .usesLineFragmentOrigin, + attributes: [.font: titleFont], + context: nil).width + let prefixesWidth = widths.map { $0.prefix }.max() ?? 0 + let labelsWidth = widths.map { $0.label }.max() ?? 0 + let valuesWidth = widths.map { $0.value }.max() ?? 0 + + let totalWidth: CGFloat = max(prefixesWidth + labelsWidth + valuesWidth, titleWidth + iconWidth) + margins * 2 + let totalHeight: CGFloat = CGFloat(detailsViewModel.values.count + 1) * rowHeight + margins * 2 + let backgroundColor = UIColor.valueBetween(start: fromColorMode.chartDetailsViewColor, + end: currentColorMode.chartDetailsViewColor, + offset: Double(colorAnimator.current)) + let titleAndTextColor = UIColor.valueBetween(start: fromColorMode.chartDetailsTextColor, + end: currentColorMode.chartDetailsTextColor, + offset: Double(colorAnimator.current)) + let detailsViewFrame: CGRect + if totalWidth + detailViewTopOffset > detailsViewPosition { + detailsViewFrame = CGRect(x: detailsViewPosition + detailViewTopOffset, + y: detailViewTopOffset + chartFrame.minY, + width: totalWidth, + height: totalHeight) + } else { + detailsViewFrame = CGRect(x: detailsViewPosition - totalWidth - detailViewTopOffset, + y: detailViewTopOffset + chartFrame.minY, + width: totalWidth, + height: totalHeight) + } + previousRenderBannerFrame = detailsViewFrame + context.saveGState() + context.setFillColor(backgroundColor.cgColor) + context.beginPath() + context.addPath(CGPath(roundedRect: detailsViewFrame, cornerWidth: 5, cornerHeight: 5, transform: nil)) + context.fillPath() + context.endPage() + context.restoreGState() + + var drawY = detailsViewFrame.minY + margins + (rowHeight - titleFont.pointSize) / 2 + (detailsViewModel.title as NSString).draw(at: CGPoint(x: detailsViewFrame.minX + margins, y: drawY), withAttributes: [.font: titleFont, + .foregroundColor: titleAndTextColor]) + drawY += rowHeight + + for (index, row) in widths.enumerated() { + let value = detailsViewModel.values[index] + if let prefixText = value.prefix { + (prefixText as NSString).draw(at: CGPoint(x: detailsViewFrame.minX + prefixesWidth - row.prefix, + y: drawY), + withAttributes: [.font: prefixText, .foregroundColor: titleAndTextColor]) + } + + (value.title as NSString).draw(at: CGPoint(x: detailsViewFrame.minX + prefixesWidth + margins, + y: drawY), + withAttributes: [.font: labelsFont, .foregroundColor: titleAndTextColor]) + + (value.value as NSString).draw(at: CGPoint(x: detailsViewFrame.minX + prefixesWidth + labelsWidth + valuesWidth - row.value + margins, + y: drawY), + withAttributes: [.font: labelsFont, .foregroundColor: value.color]) + + drawY += rowHeight + } + } +} diff --git a/submodules/Charts/Sources/Charts/Renderes/HorizontalScalesRenderer.swift b/submodules/Charts/Sources/Charts/Renderes/HorizontalScalesRenderer.swift new file mode 100644 index 0000000000..3ab90ef546 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Renderes/HorizontalScalesRenderer.swift @@ -0,0 +1,99 @@ +// +// HorizontalScalesRenderer.swift +// GraphTest +// +// Created by Andrei Salavei on 4/8/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class HorizontalScalesRenderer: BaseChartRenderer { + private var horizontalLabels: [LinesChartLabel] = [] + private var animatedHorizontalLabels: [AnimatedLinesChartLabels] = [] + + var labelsVerticalOffset: CGFloat = 8 + var labelsFont: UIFont = .systemFont(ofSize: 11) + var labelsColor: UIColor = .gray + + func setup(labels: [LinesChartLabel], animated: Bool) { + if animated { + var labelsToKeepVisible: [LinesChartLabel] = [] + let labelsToHide: [LinesChartLabel] + var labelsToShow: [LinesChartLabel] = [] + + for label in labels { + if horizontalLabels.contains(label) { + labelsToKeepVisible.append(label) + } else { + labelsToShow.append(label) + } + } + labelsToHide = horizontalLabels.filter { !labels.contains($0) } + animatedHorizontalLabels.removeAll() + horizontalLabels = labelsToKeepVisible + + let showAnimation = AnimatedLinesChartLabels(labels: labelsToShow, alphaAnimator: AnimationController(current: 1.0, refreshClosure: refreshClosure)) + showAnimation.isAppearing = true + showAnimation.alphaAnimator.set(current: 0) + showAnimation.alphaAnimator.animate(to: 1, duration: .defaultDuration) + showAnimation.alphaAnimator.completionClosure = { [weak self, weak showAnimation] in + guard let self = self, let showAnimation = showAnimation else { return } + self.animatedHorizontalLabels.removeAll(where: { $0 === showAnimation }) + self.horizontalLabels = labels + } + + let hideAnimation = AnimatedLinesChartLabels(labels: labelsToHide, alphaAnimator: AnimationController(current: 1.0, refreshClosure: refreshClosure)) + hideAnimation.isAppearing = false + hideAnimation.alphaAnimator.set(current: 1) + hideAnimation.alphaAnimator.animate(to: 0, duration: .defaultDuration) + hideAnimation.alphaAnimator.completionClosure = { [weak self, weak hideAnimation] in + guard let self = self, let hideAnimation = hideAnimation else { return } + self.animatedHorizontalLabels.removeAll(where: { $0 === hideAnimation }) + } + + animatedHorizontalLabels.append(showAnimation) + animatedHorizontalLabels.append(hideAnimation) + } else { + horizontalLabels = labels + animatedHorizontalLabels = [] + } + } + + override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) { + guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return } + let itemsAlpha = chartAlphaAnimator.current + guard itemsAlpha > 0 else { return } + + let range = renderRange(bounds: bounds, chartFrame: chartFrame) + + func drawHorizontalLabels(_ labels: [LinesChartLabel], color: UIColor) { + let attributes: [NSAttributedString.Key : Any] = [.foregroundColor: color, + .font: labelsFont] + let y = chartFrame.origin.y + chartFrame.height + labelsVerticalOffset + + if let start = labels.firstIndex(where: { $0.value > range.lowerBound }) { + for index in start.. range.upperBound { + break + } + } + } + } + let labelColorAlpha = labelsColor.alphaValue * itemsAlpha + drawHorizontalLabels(horizontalLabels, color: labelsColor.withAlphaComponent(labelColorAlpha * itemsAlpha)) + for animation in animatedHorizontalLabels { + let color = labelsColor.withAlphaComponent(animation.alphaAnimator.current * labelColorAlpha) + drawHorizontalLabels(animation.labels, color: color) + } + } +} diff --git a/submodules/Charts/Sources/Charts/Renderes/LineBulletsRenerer.swift b/submodules/Charts/Sources/Charts/Renderes/LineBulletsRenerer.swift new file mode 100644 index 0000000000..e0417719d7 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Renderes/LineBulletsRenerer.swift @@ -0,0 +1,67 @@ +// +// LineBulletsRenerer.swift +// GraphTest +// +// Created by Andrei Salavei on 4/8/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class LineBulletsRenerer: BaseChartRenderer { + struct Bullet { + var coordinate: CGPoint + var color: UIColor + } + + var bullets: [Bullet] = [] { + willSet { + if alphaAnimators.count != newValue.count { + alphaAnimators = newValue.map { _ in AnimationController(current: 1.0, refreshClosure: refreshClosure) } + } + } + didSet { + setNeedsDisplay() + } + } + private var alphaAnimators: [AnimationController] = [] + + private lazy var innerColorAnimator = AnimationController(current: UIColorContainer(color: .white), refreshClosure: refreshClosure) + public func setInnerColor(_ color: UIColor, animated: Bool) { + if animated { + innerColorAnimator.animate(to: UIColorContainer(color: color), duration: .defaultDuration) + } else { + innerColorAnimator.set(current: UIColorContainer(color: color)) + } + } + + var linesWidth: CGFloat = 2 + var bulletRadius: CGFloat = 6 + + func setLineVisible(_ isVisible: Bool, at index: Int, animated: Bool) { + alphaAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0) + } + + override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) { + guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return } + let generalAlpha = chartAlphaAnimator.current + if generalAlpha == 0 { return } + + for (index, bullet) in bullets.enumerated() { + let alpha = alphaAnimators[index].current + if alpha == 0 { continue } + + let centerX = transform(toChartCoordinateHorizontal: bullet.coordinate.x, chartFrame: chartFrame) + let centerY = transform(toChartCoordinateVertical: bullet.coordinate.y, chartFrame: chartFrame) + context.setFillColor(innerColorAnimator.current.color.withAlphaComponent(alpha).cgColor) + context.setStrokeColor(bullet.color.withAlphaComponent(alpha).cgColor) + context.setLineWidth(linesWidth) + let rect = CGRect(x: centerX - bulletRadius / 2, + y: centerY - bulletRadius / 2, + width: bulletRadius, + height: bulletRadius) + context.fillEllipse(in: rect) + context.strokeEllipse(in: rect) + } + } +} diff --git a/submodules/Charts/Sources/Charts/Renderes/LinesChartRenderer.swift b/submodules/Charts/Sources/Charts/Renderes/LinesChartRenderer.swift new file mode 100644 index 0000000000..fe3cdd47ab --- /dev/null +++ b/submodules/Charts/Sources/Charts/Renderes/LinesChartRenderer.swift @@ -0,0 +1,538 @@ +// +// LinesChartRenderer.swift +// GraphTest +// +// Created by Andrei Salavei on 4/7/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class LinesChartRenderer: BaseChartRenderer { + struct LineData { + var color: UIColor + var points: [CGPoint] + } + + private var linesAlphaAnimators: [AnimationController] = [] + + var lineWidth: CGFloat = 1 { + didSet { + setNeedsDisplay() + } + } + private lazy var linesShapeAnimator = AnimationController(current: 1, refreshClosure: self.refreshClosure) + private var fromLines: [LineData] = [] + private var toLines: [LineData] = [] + + func setLines(lines: [LineData], animated: Bool) { + if toLines.count != lines.count { + linesAlphaAnimators = lines.map { _ in AnimationController(current: 1, refreshClosure: self.refreshClosure) } + } + if animated { + self.fromLines = self.toLines + self.toLines = lines + linesShapeAnimator.set(current: 1.0 - linesShapeAnimator.current) + linesShapeAnimator.completionClosure = { + self.fromLines = [] + } + linesShapeAnimator.animate(to: 1, duration: .defaultDuration) + } else { + self.fromLines = [] + self.toLines = lines + linesShapeAnimator.set(current: 1) + } + } + + func setLineVisible(_ isVisible: Bool, at index: Int, animated: Bool) { + linesAlphaAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0) + } + + override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) { + guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return } + let chartsAlpha = chartAlphaAnimator.current + if chartsAlpha == 0 { return } + let range = renderRange(bounds: bounds, chartFrame: chartFrame) + + for (index, toLine) in toLines.enumerated() { + let alpha = linesAlphaAnimators[index].current * chartsAlpha + if alpha == 0 { continue } + context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor) + context.setLineWidth(lineWidth) + + if linesShapeAnimator.isAnimating { + let animationOffset = linesShapeAnimator.current + + let path = CGMutablePath() + let fromPoints = fromLines.safeElement(at: index)?.points ?? [] + let toPoints = toLines.safeElement(at: index)?.points ?? [] + + var fromIndex: Int? = fromPoints.firstIndex(where: { $0.x >= range.lowerBound }) + var toIndex: Int? = toPoints.firstIndex(where: { $0.x >= range.lowerBound }) + + let fromRange = verticalRange.start + let currentRange = verticalRange.current + let toRange = verticalRange.end + + func convertFromPoint(_ fromPoint: CGPoint) -> CGPoint { + return CGPoint(x: fromPoint.x, + y: (fromPoint.y - fromRange.lowerBound) / fromRange.distance * currentRange.distance + currentRange.lowerBound) + } + + func convertToPoint(_ toPoint: CGPoint) -> CGPoint { + return CGPoint(x: toPoint.x, + y: (toPoint.y - toRange.lowerBound) / toRange.distance * currentRange.distance + currentRange.lowerBound) + } + + var previousFromPoint: CGPoint + var previousToPoint: CGPoint + let startFromPoint: CGPoint? + let startToPoint: CGPoint? + + if let validFrom = fromIndex { + previousFromPoint = convertFromPoint(fromPoints[max(0, validFrom - 1)]) + startFromPoint = previousFromPoint + } else { + previousFromPoint = .zero + startFromPoint = nil + } + if let validTo = toIndex { + previousToPoint = convertToPoint(toPoints[max(0, validTo - 1)]) + startToPoint = previousToPoint + } else { + previousToPoint = .zero + startToPoint = nil + } + + var combinedPoints: [CGPoint] = [] + + func add(pointToDraw: CGPoint) { + if let startFromPoint = startFromPoint, + pointToDraw.x < startFromPoint.x { + let animatedPoint = CGPoint(x: pointToDraw.x, + y: CGFloat.valueBetween(start: startFromPoint.y, end: pointToDraw.y, offset: animationOffset)) + combinedPoints.append(transform(toChartCoordinate: animatedPoint, chartFrame: chartFrame)) + } else if let startToPoint = startToPoint, + pointToDraw.x < startToPoint.x { + let animatedPoint = CGPoint(x: pointToDraw.x, + y: CGFloat.valueBetween(start: startToPoint.y, end: pointToDraw.y, offset: 1 - animationOffset)) + combinedPoints.append(transform(toChartCoordinate: animatedPoint, chartFrame: chartFrame)) + } else { + combinedPoints.append(transform(toChartCoordinate: pointToDraw, chartFrame: chartFrame)) + } + } + + if previousToPoint != .zero && previousFromPoint != .zero { + add(pointToDraw: (previousToPoint.x < previousFromPoint.x ? previousToPoint : previousFromPoint)) + } else if previousToPoint != .zero { + add(pointToDraw: previousToPoint) + } else if previousFromPoint != .zero { + add(pointToDraw: previousFromPoint) + } + + while let validFromIndex = fromIndex, + let validToIndex = toIndex, + validFromIndex < fromPoints.count, + validToIndex < toPoints.count { + let currentFromPoint = convertFromPoint(fromPoints[validFromIndex]) + let currentToPoint = convertToPoint(toPoints[validToIndex]) + let pointToAdd: CGPoint + if currentFromPoint.x == currentToPoint.x { + pointToAdd = CGPoint.valueBetween(start: currentFromPoint, end: currentToPoint, offset: animationOffset) + previousFromPoint = currentFromPoint + previousToPoint = currentToPoint + fromIndex = validFromIndex + 1 + toIndex = validToIndex + 1 + } else if currentFromPoint.x < currentToPoint.x { + if previousToPoint.x < currentFromPoint.x { + let offset = Double((currentFromPoint.x - previousToPoint.x) / (currentToPoint.x - previousToPoint.x)) + let intermidiateToPoint = CGPoint.valueBetween(start: previousToPoint, end: currentToPoint, offset: offset) + pointToAdd = CGPoint.valueBetween(start: currentFromPoint, end: intermidiateToPoint, offset: animationOffset) + } else { + pointToAdd = currentFromPoint + } + previousFromPoint = currentFromPoint + fromIndex = validFromIndex + 1 + } else { + if previousFromPoint.x < currentToPoint.x { + let offset = Double((currentToPoint.x - previousFromPoint.x) / (currentFromPoint.x - previousFromPoint.x)) + let intermidiateFromPoint = CGPoint.valueBetween(start: previousFromPoint, end: currentFromPoint, offset: offset) + pointToAdd = CGPoint.valueBetween(start: intermidiateFromPoint, end: currentToPoint, offset: animationOffset) + } else { + pointToAdd = currentToPoint + } + previousToPoint = currentToPoint + toIndex = validToIndex + 1 + } + add(pointToDraw: pointToAdd) + if (pointToAdd.x > range.upperBound) { + break + } + } + + while let validToIndex = toIndex, validToIndex < toPoints.count { + var pointToAdd = convertToPoint(toPoints[validToIndex]) + pointToAdd.y = CGFloat.valueBetween(start: previousFromPoint.y, + end: pointToAdd.y, + offset: animationOffset) + + add(pointToDraw: pointToAdd) + if (pointToAdd.x > range.upperBound) { + break + } + + toIndex = validToIndex + 1 + } + + while let validFromIndex = fromIndex, validFromIndex < fromPoints.count { + var pointToAdd = convertFromPoint(fromPoints[validFromIndex]) + pointToAdd.y = CGFloat.valueBetween(start: previousToPoint.y, + end: pointToAdd.y, + offset: 1 - animationOffset) + + add(pointToDraw: pointToAdd) + if (pointToAdd.x > range.upperBound) { + break + } + + fromIndex = validFromIndex + 1 + } + + var index = 0 + var lines: [CGPoint] = [] + var currentChartPoint = combinedPoints[index] + lines.append(currentChartPoint) + + var chartPoints = [currentChartPoint] + var minIndex = 0 + var maxIndex = 0 + index += 1 + + while index < combinedPoints.count { + currentChartPoint = combinedPoints[index] + + if currentChartPoint.x - chartPoints[0].x < lineWidth * optimizationLevel { + chartPoints.append(currentChartPoint) + + if currentChartPoint.y > chartPoints[maxIndex].y { + maxIndex = chartPoints.count - 1 + } + if currentChartPoint.y < chartPoints[minIndex].y { + minIndex = chartPoints.count - 1 + } + + index += 1 + } else { + if chartPoints.count == 1 { + lines.append(currentChartPoint) + lines.append(currentChartPoint) + chartPoints[0] = currentChartPoint + index += 1 + minIndex = 0 + maxIndex = 0 + } else { + if minIndex < maxIndex { + if minIndex != 0 { + lines.append(chartPoints[minIndex]) + lines.append(chartPoints[minIndex]) + } + lines.append(chartPoints[maxIndex]) + lines.append(chartPoints[maxIndex]) + if maxIndex != chartPoints.count - 1 { + chartPoints = [chartPoints[maxIndex], chartPoints.last!] + } else { + chartPoints = [chartPoints[maxIndex]] + } + } else { + if maxIndex != 0 { + lines.append(chartPoints[maxIndex]) + lines.append(chartPoints[maxIndex]) + } + lines.append(chartPoints[minIndex]) + lines.append(chartPoints[minIndex]) + if minIndex != chartPoints.count - 1 { + chartPoints = [chartPoints[minIndex], chartPoints.last!] + } else { + chartPoints = [chartPoints[minIndex]] + } + } + if chartPoints.count == 2 { + if chartPoints[0].y < chartPoints[1].y { + minIndex = 0 + maxIndex = 1 + } else { + minIndex = 1 + maxIndex = 0 + } + } else { + minIndex = 0 + maxIndex = 0 + } + } + } + } + + if chartPoints.count == 1 { + lines.append(currentChartPoint) + lines.append(currentChartPoint) + } else { + if minIndex < maxIndex { + if minIndex != 0 { + lines.append(chartPoints[minIndex]) + lines.append(chartPoints[minIndex]) + } + lines.append(chartPoints[maxIndex]) + lines.append(chartPoints[maxIndex]) + if maxIndex != chartPoints.count - 1 { + lines.append(chartPoints.last!) + lines.append(chartPoints.last!) + } + } else { + if maxIndex != 0 { + lines.append(chartPoints[maxIndex]) + lines.append(chartPoints[maxIndex]) + } + lines.append(chartPoints[minIndex]) + lines.append(chartPoints[minIndex]) + if minIndex != chartPoints.count - 1 { + lines.append(chartPoints.last!) + lines.append(chartPoints.last!) + } + } + } + + if (lines.count % 2) == 1 { + lines.removeLast() + } + + context.setLineCap(.round) + context.strokeLineSegments(between: lines) + + } else { + let alpha = linesAlphaAnimators[index].current * chartsAlpha + if alpha == 0 { continue } + context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor) + context.setLineWidth(lineWidth) + + if var index = toLine.points.firstIndex(where: { $0.x >= range.lowerBound }) { + var lines: [CGPoint] = [] + index = max(0, index - 1) + var currentPoint = toLine.points[index] + var currentChartPoint = transform(toChartCoordinate: currentPoint, chartFrame: chartFrame) + lines.append(currentChartPoint) + //context.move(to: currentChartPoint) + + var chartPoints = [currentChartPoint] + var minIndex = 0 + var maxIndex = 0 + index += 1 + + while index < toLine.points.count { + currentPoint = toLine.points[index] + currentChartPoint = transform(toChartCoordinate: currentPoint, chartFrame: chartFrame) + + if currentChartPoint.x - chartPoints[0].x < lineWidth * optimizationLevel { + chartPoints.append(currentChartPoint) + + if currentChartPoint.y > chartPoints[maxIndex].y { + maxIndex = chartPoints.count - 1 + } + if currentChartPoint.y < chartPoints[minIndex].y { + minIndex = chartPoints.count - 1 + } + + index += 1 + } else { + if chartPoints.count == 1 { + lines.append(currentChartPoint) + lines.append(currentChartPoint) + chartPoints[0] = currentChartPoint + index += 1 + minIndex = 0 + maxIndex = 0 + } else { + if minIndex < maxIndex { + if minIndex != 0 { + lines.append(chartPoints[minIndex]) + lines.append(chartPoints[minIndex]) + } + lines.append(chartPoints[maxIndex]) + lines.append(chartPoints[maxIndex]) + if maxIndex != chartPoints.count - 1 { + chartPoints = [chartPoints[maxIndex], chartPoints.last!] + } else { + chartPoints = [chartPoints[maxIndex]] + } + } else { + if maxIndex != 0 { + lines.append(chartPoints[maxIndex]) + lines.append(chartPoints[maxIndex]) + } + lines.append(chartPoints[minIndex]) + lines.append(chartPoints[minIndex]) + if minIndex != chartPoints.count - 1 { + chartPoints = [chartPoints[minIndex], chartPoints.last!] + } else { + chartPoints = [chartPoints[minIndex]] + } + } + if chartPoints.count == 2 { + if chartPoints[0].y < chartPoints[1].y { + minIndex = 0 + maxIndex = 1 + } else { + minIndex = 1 + maxIndex = 0 + } + } else { + minIndex = 0 + maxIndex = 0 + } + } + } + if currentPoint.x > range.upperBound { + break + } + } + + if chartPoints.count == 1 { + lines.append(currentChartPoint) + lines.append(currentChartPoint) + } else { + if minIndex < maxIndex { + if minIndex != 0 { + lines.append(chartPoints[minIndex]) + lines.append(chartPoints[minIndex]) + } + lines.append(chartPoints[maxIndex]) + lines.append(chartPoints[maxIndex]) + if maxIndex != chartPoints.count - 1 { + lines.append(chartPoints.last!) + lines.append(chartPoints.last!) + } + } else { + if maxIndex != 0 { + lines.append(chartPoints[maxIndex]) + lines.append(chartPoints[maxIndex]) + } + lines.append(chartPoints[minIndex]) + lines.append(chartPoints[minIndex]) + if minIndex != chartPoints.count - 1 { + lines.append(chartPoints.last!) + lines.append(chartPoints.last!) + } + } + } + + if (lines.count % 2) == 1 { + lines.removeLast() + } + + context.setLineCap(.round) + context.strokeLineSegments(between: lines) + } + +// if var start = toLine.points.firstIndex(where: { $0.x > range.lowerBound }) { +// let alpha = linesAlphaAnimators[index].current * chartsAlpha +// if alpha == 0 { continue } +// context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor) +// context.setLineWidth(lineWidth) +// +// context.setLineCap(.round) +// start = max(0, start - 1) +// let startPoint = toLine.points[start] +// var lines: [CGPoint] = [] +// var pointToDraw = CGPoint(x: transform(toChartCoordinateHorizontal: startPoint.x, chartFrame: chartFrame), +// y: transform(toChartCoordinateVertical: startPoint.y, chartFrame: chartFrame)) +// for index in (start + 1).. range.upperBound { +// break +// } +// } +// +// context.strokeLineSegments(between: lines) +// } + } + } + } +} + +extension LinesChartRenderer.LineData { + static func initialComponents(chartsCollection: ChartsCollection) -> (linesData: [LinesChartRenderer.LineData], + totalHorizontalRange: ClosedRange, + totalVerticalRange: ClosedRange) { + let lines: [LinesChartRenderer.LineData] = chartsCollection.chartValues.map { chart in + let points = chart.values.enumerated().map({ (arg) -> CGPoint in + return CGPoint(x: chartsCollection.axisValues[arg.offset].timeIntervalSince1970, + y: arg.element) + }) + return LinesChartRenderer.LineData(color: chart.color, points: points) + } + let horizontalRange = LinesChartRenderer.LineData.horizontalRange(lines: lines) ?? BaseConstants.defaultRange + let verticalRange = LinesChartRenderer.LineData.verticalRange(lines: lines) ?? BaseConstants.defaultRange + return (linesData: lines, totalHorizontalRange: horizontalRange, totalVerticalRange: verticalRange) + } + + static func horizontalRange(lines: [LinesChartRenderer.LineData]) -> ClosedRange? { + guard let firstPoint = lines.first?.points.first else { return nil } + var hMin: CGFloat = firstPoint.x + var hMax: CGFloat = firstPoint.x + + for line in lines { + if let first = line.points.first, + let last = line.points.last { + hMin = min(hMin, first.x) + hMax = max(hMax, last.x) + } + } + + return hMin...hMax + } + + static func verticalRange(lines: [LinesChartRenderer.LineData], calculatingRange: ClosedRange? = nil, addBounds: Bool = false) -> ClosedRange? { + if let calculatingRange = calculatingRange { + guard let initalStart = lines.first?.points.first(where: { $0.x >= calculatingRange.lowerBound && + $0.x <= calculatingRange.upperBound }) else { return nil } + var vMin: CGFloat = initalStart.y + var vMax: CGFloat = initalStart.y + for line in lines { + if var index = line.points.firstIndex(where: { $0.x > calculatingRange.lowerBound }) { + if addBounds { + index = max(0, index - 1) + } + while index < line.points.count { + let point = line.points[index] + if point.x < calculatingRange.upperBound { + vMin = min(vMin, point.y) + vMax = max(vMax, point.y) + } else if addBounds { + vMin = min(vMin, point.y) + vMax = max(vMax, point.y) + break + } else { + break + } + index += 1 + } + } + } + return vMin...vMax + } else { + guard let firstPoint = lines.first?.points.first else { return nil } + var vMin: CGFloat = firstPoint.y + var vMax: CGFloat = firstPoint.y + for line in lines { + for point in line.points { + vMin = min(vMin, point.y) + vMax = max(vMax, point.y) + } + } + return vMin...vMax + } + } +} diff --git a/submodules/Charts/Sources/Charts/Renderes/PecentChartRenderer.swift b/submodules/Charts/Sources/Charts/Renderes/PecentChartRenderer.swift new file mode 100644 index 0000000000..07ff3daaaa --- /dev/null +++ b/submodules/Charts/Sources/Charts/Renderes/PecentChartRenderer.swift @@ -0,0 +1,132 @@ +// +// PecentChartRenderer.swift +// GraphTest +// +// Created by Andrei Salavei on 4/7/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class PecentChartRenderer: BaseChartRenderer { + struct PercentageData { + static let blank = PecentChartRenderer.PercentageData(locations: [], components: []) + var locations: [CGFloat] + var components: [Component] + + struct Component { + var color: UIColor + var values: [CGFloat] + } + } + + override func setup(verticalRange: ClosedRange, animated: Bool, timeFunction: TimeFunction? = nil) { + super.setup(verticalRange: 0...1, animated: animated, timeFunction: timeFunction) + } + + private var componentsAnimators: [AnimationController] = [] + var percentageData: PercentageData = PercentageData(locations: [], components: []) { + willSet { + if percentageData.components.count != newValue.components.count { + componentsAnimators = newValue.components.map { _ in AnimationController(current: 1, refreshClosure: self.refreshClosure) } + } + } + didSet { + setNeedsDisplay() + } + } + + func setComponentVisible(_ isVisible: Bool, at index: Int, animated: Bool) { + componentsAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0) + } + + override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) { + guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return } + let alpha = chartAlphaAnimator.current + guard alpha > 0 else { return } + + let range = renderRange(bounds: bounds, chartFrame: chartFrame) + + var paths: [CGMutablePath] = percentageData.components.map { _ in CGMutablePath() } + var vertices: [CGFloat] = Array(repeating: 0, count: percentageData.components.count) + + if var locationIndex = percentageData.locations.firstIndex(where: { $0 > range.lowerBound }) { + locationIndex = max(0, locationIndex - 1) + + var currentLocation = transform(toChartCoordinateHorizontal: percentageData.locations[locationIndex], chartFrame: chartFrame) + + let startPoint = CGPoint(x: currentLocation, + y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame)) + + for path in paths { + path.move(to: startPoint) + } + paths.last?.addLine(to: CGPoint(x: currentLocation, + y: transform(toChartCoordinateVertical: verticalRange.current.upperBound, chartFrame: chartFrame))) + + while locationIndex < percentageData.locations.count { + currentLocation = transform(toChartCoordinateHorizontal: percentageData.locations[locationIndex], chartFrame: chartFrame) + var summ: CGFloat = 0 + + for (index, component) in percentageData.components.enumerated() { + let visibilityPercent = componentsAnimators[index].current + + let value = component.values[locationIndex] * visibilityPercent + if index == 0 { + vertices[index] = value + } else { + vertices[index] = value + vertices[index - 1] + } + summ += value + } + + if summ > 0 { + for (index, value) in vertices.dropLast().enumerated() { + paths[index].addLine(to: CGPoint(x: currentLocation, + y: transform(toChartCoordinateVertical: value / summ, chartFrame: chartFrame))) + } + } + + if currentLocation > range.upperBound { + break + } + + locationIndex += 1 + } + + paths.last?.addLine(to: CGPoint(x: currentLocation, + y: transform(toChartCoordinateVertical: verticalRange.current.upperBound, chartFrame: chartFrame))) + + let endPoint = CGPoint(x: currentLocation, + y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame)) + + for (index, path) in paths.enumerated().reversed() { + let visibilityPercent = componentsAnimators[index].current + if visibilityPercent == 0 { continue } + + path.addLine(to: endPoint) + path.closeSubpath() + + context.saveGState() + context.beginPath() + context.addPath(path) + + context.setFillColor(percentageData.components[index].color.cgColor) + context.fillPath() + context.restoreGState() + } + } + } +} + +extension PecentChartRenderer.PercentageData { + static func horizontalRange(data: PecentChartRenderer.PercentageData) -> ClosedRange? { + guard let firstPoint = data.locations.first, + let lastPoint = data.locations.last, + firstPoint <= lastPoint else { + return nil + } + + return firstPoint...lastPoint + } +} diff --git a/submodules/Charts/Sources/Charts/Renderes/PercentPieAnimationRenderer.swift b/submodules/Charts/Sources/Charts/Renderes/PercentPieAnimationRenderer.swift new file mode 100644 index 0000000000..e36fdaf913 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Renderes/PercentPieAnimationRenderer.swift @@ -0,0 +1,202 @@ +// +// PercentPieAnimationRenderer.swift +// GraphTest +// +// Created by Andrei Salavei on 4/13/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class PercentPieAnimationRenderer: BaseChartRenderer { + override func setup(verticalRange: ClosedRange, animated: Bool, timeFunction: TimeFunction? = nil) { + super.setup(verticalRange: 0...1, animated: animated, timeFunction: timeFunction) + } + + private lazy var transitionAnimator = AnimationController(current: 0, refreshClosure: refreshClosure) + private var animationComponentsPoints: [[CGPoint]] = [] + var visiblePercentageData: PecentChartRenderer.PercentageData = .blank { + didSet { + animationComponentsPoints = [] + } + } + var visiblePieComponents: [PieChartRenderer.PieComponent] = [] + + func animate(fromDataToPie: Bool, animated: Bool, completion: @escaping () -> Void) { + assert(visiblePercentageData.components.count == visiblePieComponents.count) + + isEnabled = true + transitionAnimator.completionClosure = { [weak self] in + self?.isEnabled = false + completion() + } + transitionAnimator.animate(to: fromDataToPie ? 1 : 0, duration: animated ? .defaultDuration : 0) + } + + private func generateAnimationComponentPoints(bounds: CGRect, chartFrame: CGRect) { + let range = renderRange(bounds: bounds, chartFrame: chartFrame) + + let componentsCount = visiblePercentageData.components.count + guard componentsCount > 0 else { return } + animationComponentsPoints = visiblePercentageData.components.map { _ in [] } + var vertices: [CGFloat] = Array(repeating: 0, count: visiblePercentageData.components.count) + + if var locationIndex = visiblePercentageData.locations.firstIndex(where: { $0 > range.lowerBound }) { + locationIndex = max(0, locationIndex - 1) + var currentLocation = transform(toChartCoordinateHorizontal: visiblePercentageData.locations[locationIndex], chartFrame: chartFrame) + let startPoint = CGPoint(x: currentLocation, y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame)) + for index in 0.. range.upperBound { + break + } + locationIndex += 1 + } + + animationComponentsPoints[componentsCount - 1].append(CGPoint(x: currentLocation, y: transform(toChartCoordinateVertical: verticalRange.current.upperBound, chartFrame: chartFrame))) + let endPoint = CGPoint(x: currentLocation, y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame)) + for index in 0.. 0 && verticalRange.current.distance > 0 else { return } + self.optimizationLevel = 1 + + if animationComponentsPoints.isEmpty { + generateAnimationComponentPoints(bounds: bounds, chartFrame: chartFrame) + } + + let numberOfComponents = animationComponentsPoints.count + guard numberOfComponents > 0 else { return } + let destinationRadius = max(chartFrame.width, chartFrame.height) + + let animationFraction = transitionAnimator.current + let animationFractionD = Double(transitionAnimator.current) + let easeInAnimationFractionD = animationFractionD * animationFractionD * animationFractionD * animationFractionD + let center = CGPoint(x: chartFrame.midX, y: chartFrame.midY) + let totalPieSumm: CGFloat = visiblePieComponents.map { $0.value } .reduce(0, +) + + let pathsToDraw: [CGMutablePath] = (0.. 4 else { + return + } + + let percent = visiblePieComponents[componentIndex].value / totalPieSumm + let segmentSize = 2 * .pi * percent + let endAngle = startAngle + segmentSize + let centerAngle = (startAngle + endAngle) / 2 + + let lineCenterPoint = CGPoint.valueBetween(start: componentPoints[componentPoints.count / 2], + end: center, + offset: animationFractionD) + + let startDestinationPoint = lineCenterPoint + CGPoint(x: destinationRadius, y: 0) + let centerDestinationPoint = lineCenterPoint + CGPoint(x: 0, y: destinationRadius) + let endDestinationPoint = lineCenterPoint + CGPoint(x: -destinationRadius, y: 0) + let initialStartDestinationAngle: CGFloat = 0 + let initialCenterDestinationAngle: CGFloat = .pi / 2 + let initialEndDestinationAngle: CGFloat = .pi + + var previousAddedPoint = (componentPoints[0] * 2 - center) + .rotate(origin: lineCenterPoint, angle: CGFloat.valueBetween(start: 0, end: centerAngle - initialCenterDestinationAngle, offset: animationFractionD)) + + pathsToDraw[componentIndex].move(to: previousAddedPoint) + + func addPointToPath(_ point: CGPoint) { + if (point - previousAddedPoint).lengthSquared() > optimizationLevel { + pathsToDraw[componentIndex].addLine(to: point) + previousAddedPoint = point + } + } + + for endPointIndex in 1..<(componentPoints.count / 2) { + addPointToPath(CGPoint.valueBetween(start: componentPoints[endPointIndex], end: endDestinationPoint, offset: easeInAnimationFractionD) + .rotate(origin: lineCenterPoint, angle: CGFloat.valueBetween(start: 0, end: endAngle - initialEndDestinationAngle, offset: animationFractionD))) + } + + addPointToPath(lineCenterPoint) + + for startPointIndex in (componentPoints.count / 2 + 1)..<(componentPoints.count - 1) { + addPointToPath(CGPoint.valueBetween(start: componentPoints[startPointIndex], end: startDestinationPoint, offset: easeInAnimationFractionD) + .rotate(origin: lineCenterPoint, angle: CGFloat.valueBetween(start: 0, end: startAngle - initialStartDestinationAngle, offset: animationFractionD))) + } + + if let lastPoint = componentPoints.last { + addPointToPath((lastPoint * 2 - center) + .rotate(origin: lineCenterPoint, angle: CGFloat.valueBetween(start: 0, end: centerAngle - initialCenterDestinationAngle, offset: animationFractionD))) + } + + startAngle = endAngle + } + + if let lastPath = animationComponentsPoints.last { + pathsToDraw.last?.addLines(between: lastPath) + } + + for (index, path) in pathsToDraw.enumerated().reversed() { + path.closeSubpath() + + context.saveGState() + context.beginPath() + context.addPath(path) + + context.setFillColor(visiblePieComponents[index].color.cgColor) + context.fillPath() + context.restoreGState() + } + + let diagramRadius = (min(chartFrame.width, chartFrame.height) / 2) * 0.925 + let targetFrame = CGRect(origin: CGPoint(x: center.x - diagramRadius, + y: center.y - diagramRadius), + size: CGSize(width: diagramRadius * 2, + height: diagramRadius * 2)) + + let minX = animationComponentsPoints.last?.first?.x ?? 0 + let maxX = animationComponentsPoints.last?.last?.x ?? 0 + let startFrame = CGRect(x: minX, + y: chartFrame.minY, + width: maxX - minX, + height: chartFrame.height) + let cornerRadius = diagramRadius * animationFraction + let fadeOutFrame = CGRect.valueBetween(start: startFrame, end: targetFrame, offset: animationFractionD) + let fadeOutPath = CGMutablePath() + fadeOutPath.addRect(bounds) + fadeOutPath.addPath(CGPath(roundedRect: fadeOutFrame, cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)) + + context.saveGState() + context.beginPath() + context.addPath(fadeOutPath) + context.setFillColor(backgroundColor.cgColor) + context.fillPath(using: .evenOdd) + context.restoreGState() + } +} diff --git a/submodules/Charts/Sources/Charts/Renderes/PerformanceRenderer.swift b/submodules/Charts/Sources/Charts/Renderes/PerformanceRenderer.swift new file mode 100644 index 0000000000..c17663ce5f --- /dev/null +++ b/submodules/Charts/Sources/Charts/Renderes/PerformanceRenderer.swift @@ -0,0 +1,31 @@ +// +// PerformanceRenderer.swift +// GraphTest +// +// Created by Andrei Salavei on 4/10/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class PerformanceRenderer: ChartViewRenderer { + var containerViews: [UIView] = [] + + private var previousTickTime: TimeInterval = CACurrentMediaTime() + + func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) { + let currentTime = CACurrentMediaTime() + let delta = currentTime - previousTickTime + previousTickTime = currentTime + + let normalDelta = 0.017 + let redDelta = 0.05 + + if delta > normalDelta || delta < 0.75 { + let green = CGFloat( 1.0 - crop(0, (delta - normalDelta) / (redDelta - normalDelta), 1)) + let color = UIColor(red: 1.0, green: green, blue: 0, alpha: 1) + context.setFillColor(color.cgColor) + context.fill(CGRect(x: 0, y: 0, width: bounds.width, height: 3)) + } + } +} diff --git a/submodules/Charts/Sources/Charts/Renderes/PieChartRenderer.swift b/submodules/Charts/Sources/Charts/Renderes/PieChartRenderer.swift new file mode 100644 index 0000000000..ed4e6bdd49 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Renderes/PieChartRenderer.swift @@ -0,0 +1,191 @@ +// +// PieChartRenderer.swift +// GraphTest +// +// Created by Andrei Salavei on 4/11/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class PieChartRenderer: BaseChartRenderer { + struct PieComponent: Hashable { + var color: UIColor + var value: CGFloat + } + + override func setup(verticalRange: ClosedRange, animated: Bool, timeFunction: TimeFunction? = nil) { + super.setup(verticalRange: 0...1, animated: animated, timeFunction: timeFunction) + } + + var valuesFormatter: NumberFormatter = NumberFormatter() + var drawValues: Bool = true + + private var componentsAnimators: [AnimationController] = [] + private lazy var transitionAnimator: AnimationController = { AnimationController(current: 1, refreshClosure: self.refreshClosure) }() + private var oldPercentageData: [PieComponent] = [] + private var percentageData: [PieComponent] = [] + private var setlectedSegmentsAnimators: [AnimationController] = [] + + var drawPie: Bool = true + var initialAngle: CGFloat = .pi / 3 + var hasSelectedSegments: Bool { + return selectedSegment != nil + } + private(set) var selectedSegment: Int? + func selectSegmentAt(at indexToSelect: Int?, animated: Bool) { + selectedSegment = indexToSelect + for (index, animator) in setlectedSegmentsAnimators.enumerated() { + let fraction: CGFloat = (index == indexToSelect) ? 1.0 : 0.0 + if animated { + animator.animate(to: fraction, duration: .defaultDuration / 2) + } else { + animator.set(current: fraction) + } + } + } + + func updatePercentageData(_ percentageData: [PieComponent], animated: Bool) { + if self.percentageData.count != percentageData.count { + componentsAnimators = percentageData.map { _ in AnimationController(current: 1, refreshClosure: self.refreshClosure) } + setlectedSegmentsAnimators = percentageData.map { _ in AnimationController(current: 0, refreshClosure: self.refreshClosure) } + } + if animated { + self.oldPercentageData = self.currentTransitionAnimationData + self.percentageData = percentageData + transitionAnimator.completionClosure = { [weak self] in + self?.oldPercentageData = [] + } + transitionAnimator.set(current: 0) + transitionAnimator.animate(to: 1, duration: .defaultDuration) + } else { + self.oldPercentageData = [] + self.percentageData = percentageData + transitionAnimator.set(current: 0) + } + } + + func setComponentVisible(_ isVisible: Bool, at index: Int, animated: Bool) { + componentsAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0) + } + + var lastRenderedBounds: CGRect = .zero + var lastRenderedChartFrame: CGRect = .zero + func selectedItemIndex(at point: CGPoint) -> Int? { + let touchPosition = lastRenderedChartFrame.origin + point * lastRenderedChartFrame.size + let center = CGPoint(x: lastRenderedChartFrame.midX, y: lastRenderedChartFrame.midY) + let radius = min(lastRenderedChartFrame.width, lastRenderedChartFrame.height) / 2 + if center.distanceTo(touchPosition) > radius { return nil } + let angle = (center - touchPosition).angle + .pi + let currentData = currentlyVisibleData + let total: CGFloat = currentData.map({ $0.value }).reduce(0, +) + var startAngle: CGFloat = initialAngle + for (index, piece) in currentData.enumerated() { + let percent = piece.value / total + let segmentSize = 2 * .pi * percent + let endAngle = startAngle + segmentSize + if angle >= startAngle && angle <= endAngle || + angle + .pi * 2 >= startAngle && angle + .pi * 2 <= endAngle { + return index + } + startAngle = endAngle + } + return nil + } + + private var currentTransitionAnimationData: [PieComponent] { + if transitionAnimator.isAnimating { + let animationFraction = transitionAnimator.current + return percentageData.enumerated().map { arg in + return PieComponent(color: arg.element.color, + value: oldPercentageData[arg.offset].value * (1 - animationFraction) + arg.element.value * animationFraction) + } + } else { + return percentageData + } + } + + var currentlyVisibleData: [PieComponent] { + return currentTransitionAnimationData.enumerated().map { arg in + return PieComponent(color: arg.element.color, + value: arg.element.value * componentsAnimators[arg.offset].current) + } + } + + override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) { + guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return } + lastRenderedBounds = bounds + lastRenderedChartFrame = chartFrame + let chartAlpha = chartAlphaAnimator.current + if chartAlpha == 0 { return } + + let center = CGPoint(x: chartFrame.midX, y: chartFrame.midY) + let radius = min(chartFrame.width, chartFrame.height) / 2 + + let currentData = currentlyVisibleData + + let total: CGFloat = currentData.map({ $0.value }).reduce(0, +) + guard total > 0 else { + return + } + + let animationSelectionOffset: CGFloat = radius / 15 + let maximumFontSize: CGFloat = radius / 7 + let minimumFontSize: CGFloat = 4 + let centerOffsetStartAngle = CGFloat.pi / 4 + let minimumValueToDraw: CGFloat = 0.01 + let diagramRadius = radius - animationSelectionOffset + + let numberOfVisibleItems = currentlyVisibleData.filter { $0.value > 0 }.count + var startAngle: CGFloat = initialAngle + for (index, piece) in currentData.enumerated() { + let percent = piece.value / total + guard percent > 0 else { continue } + let segmentSize = 2 * .pi * percent * chartAlpha + let endAngle = startAngle + segmentSize + let centerAngle = (startAngle + endAngle) / 2 + let labelVector = CGPoint(x: cos(centerAngle), + y: sin(centerAngle)) + + let selectionAnimationFraction = (numberOfVisibleItems > 1 ? setlectedSegmentsAnimators[index].current : 0) + + let updatedCenter = CGPoint(x: center.x + labelVector.x * selectionAnimationFraction * animationSelectionOffset, + y: center.y + labelVector.y * selectionAnimationFraction * animationSelectionOffset) + if drawPie { + context.saveGState() + context.setFillColor(piece.color.withAlphaComponent(piece.color.alphaValue * chartAlpha).cgColor) + context.move(to: updatedCenter) + context.addArc(center: updatedCenter, + radius: radius - animationSelectionOffset, + startAngle: startAngle, + endAngle: endAngle, + clockwise: false) + context.fillPath() + context.restoreGState() + } + + if drawValues && percent >= minimumValueToDraw { + context.saveGState() + + let text = valuesFormatter.string(from: percent * 100) + let fraction = crop(0, segmentSize / centerOffsetStartAngle, 1) + let fontSize = (minimumFontSize + (maximumFontSize - minimumFontSize) * fraction).rounded(.up) + let labelPotisionOffset = diagramRadius / 2 + diagramRadius / 2 * (1 - fraction) + let font = UIFont.systemFont(ofSize: fontSize, weight: .bold) + let labelsEaseInColor = crop(0, chartAlpha * chartAlpha * 2 - 1, 1) + let attributes: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.white.withAlphaComponent(labelsEaseInColor), + .font: font] + let rect = (text as NSString).boundingRect(with: bounds.size, + options: .usesLineFragmentOrigin, + attributes: attributes, + context: nil) + let labelPoint = CGPoint(x: labelVector.x * labelPotisionOffset + updatedCenter.x - rect.width / 2, + y: labelVector.y * labelPotisionOffset + updatedCenter.y - rect.height / 2) + (text as NSString).draw(at: labelPoint, withAttributes: attributes) + context.restoreGState() + } + + startAngle = endAngle + } + } +} diff --git a/submodules/Charts/Sources/Charts/Renderes/VerticalLinesRenderer.swift b/submodules/Charts/Sources/Charts/Renderes/VerticalLinesRenderer.swift new file mode 100644 index 0000000000..39d1f26a46 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Renderes/VerticalLinesRenderer.swift @@ -0,0 +1,42 @@ +// +// VerticalLinesRenderer.swift +// GraphTest +// +// Created by Andrei Salavei on 4/8/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class VerticalLinesRenderer: BaseChartRenderer { + var values: [CGFloat] = [] { + didSet { + alphaAnimators = values.map { _ in AnimationController(current: 1.0, refreshClosure: refreshClosure) } + setNeedsDisplay() + } + } + private var alphaAnimators: [AnimationController] = [] + + var linesColor: UIColor = .black + var linesWidth: CGFloat = UIView.oneDevicePixel + + func setLineVisible(_ isVisible: Bool, at index: Int, animated: Bool) { + alphaAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0) + } + + override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) { + guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return } + + context.setLineWidth(linesWidth) + + for (index, value) in values.enumerated() { + let alpha = alphaAnimators[index].current + if alpha == 0 { continue } + + context.setStrokeColor(linesColor.withAlphaComponent(linesColor.alphaValue * alpha).cgColor) + let pointX = transform(toChartCoordinateHorizontal: value, chartFrame: chartFrame) + context.strokeLineSegments(between: [CGPoint(x: pointX, y: chartFrame.minY), + CGPoint(x: pointX, y: chartFrame.maxY)]) + } + } +} diff --git a/submodules/Charts/Sources/Charts/Renderes/VerticalScalesRenderer.swift b/submodules/Charts/Sources/Charts/Renderes/VerticalScalesRenderer.swift new file mode 100644 index 0000000000..79d9dcec22 --- /dev/null +++ b/submodules/Charts/Sources/Charts/Renderes/VerticalScalesRenderer.swift @@ -0,0 +1,162 @@ +// +// VerticalScalesRenderer.swift +// GraphTest +// +// Created by Andrei Salavei on 4/8/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class VerticalScalesRenderer: BaseChartRenderer { + private var verticalLabelsAndLines: [LinesChartLabel] = [] + private var animatedVerticalLabelsAndLines: [AnimatedLinesChartLabels] = [] + private lazy var horizontalLinesAlphaAnimator: AnimationController = { + return AnimationController(current: 1, refreshClosure: self.refreshClosure) + }() + + var drawAxisX: Bool = true + var axisXColor: UIColor = .black + var axisXWidth: CGFloat = UIView.oneDevicePixel + + var isRightAligned: Bool = false + + var horizontalLinesColor: UIColor = .black { + didSet { + setNeedsDisplay() + } + } + var horizontalLinesWidth: CGFloat = UIView.oneDevicePixel + var lavelsAsisOffset: CGFloat = 6 + var labelsColor: UIColor = .black { + didSet { + setNeedsDisplay() + } + } + var labelsFont: UIFont = .systemFont(ofSize: 11) + + func setHorizontalLinesVisible(_ visible: Bool, animated: Bool) { + let destinationValue: CGFloat = visible ? 1 : 0 + guard self.horizontalLinesAlphaAnimator.end != destinationValue else { return } + if animated { + self.horizontalLinesAlphaAnimator.animate(to: destinationValue, duration: .defaultDuration) + } else { + self.horizontalLinesAlphaAnimator.set(current: destinationValue) + } + } + + func setup(verticalLimitsLabels: [LinesChartLabel], animated: Bool) { + if animated { + var labelsToKeepVisible: [LinesChartLabel] = [] + let labelsToHide: [LinesChartLabel] + var labelsToShow: [LinesChartLabel] = [] + + for label in verticalLimitsLabels { + if verticalLabelsAndLines.contains(label) { + labelsToKeepVisible.append(label) + } else { + labelsToShow.append(label) + } + } + labelsToHide = verticalLabelsAndLines.filter { !verticalLimitsLabels.contains($0) } + animatedVerticalLabelsAndLines.removeAll(where: { $0.isAppearing }) + verticalLabelsAndLines = labelsToKeepVisible + + let showAnimation = AnimatedLinesChartLabels(labels: labelsToShow, alphaAnimator: AnimationController(current: 1.0, refreshClosure: refreshClosure)) + showAnimation.isAppearing = true + showAnimation.alphaAnimator.set(current: 0) + showAnimation.alphaAnimator.animate(to: 1, duration: .defaultDuration) + showAnimation.alphaAnimator.completionClosure = { [weak self, weak showAnimation] in + guard let self = self, let showAnimation = showAnimation else { return } + self.animatedVerticalLabelsAndLines.removeAll(where: { $0 === showAnimation }) + self.verticalLabelsAndLines = verticalLimitsLabels + } + + let hideAnimation = AnimatedLinesChartLabels(labels: labelsToHide, alphaAnimator: AnimationController(current: 1.0, refreshClosure: refreshClosure)) + hideAnimation.isAppearing = false + hideAnimation.alphaAnimator.set(current: 1) + hideAnimation.alphaAnimator.animate(to: 0, duration: .defaultDuration) + hideAnimation.alphaAnimator.completionClosure = { [weak self, weak hideAnimation] in + guard let self = self, let hideAnimation = hideAnimation else { return } + self.animatedVerticalLabelsAndLines.removeAll(where: { $0 === hideAnimation }) + } + + animatedVerticalLabelsAndLines.append(showAnimation) + animatedVerticalLabelsAndLines.append(hideAnimation) + } else { + verticalLabelsAndLines = verticalLimitsLabels + animatedVerticalLabelsAndLines = [] + } + } + + override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) { + guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return } + let generalAlpha = chartAlphaAnimator.current + if generalAlpha == 0 { return } + let labelColorAlpha = labelsColor.alphaValue + + func drawLines(_ labels: [LinesChartLabel], alpha: CGFloat) { + var lineSegments: [CGPoint] = [] + let x0 = chartFrame.minX + let x1 = chartFrame.maxX + + context.setStrokeColor(horizontalLinesColor.withAlphaComponent(horizontalLinesColor.alphaValue * alpha).cgColor) + + for lineInfo in labels { + let y = transform(toChartCoordinateVertical: lineInfo.value, chartFrame: chartFrame).roundedUpToPixelGrid() + lineSegments.append(CGPoint(x: x0, y: y)) + lineSegments.append(CGPoint(x: x1, y: y)) + } + context.strokeLineSegments(between: lineSegments) + } + + func drawVerticalLabels(_ labels: [LinesChartLabel], attributes: [NSAttributedString.Key: Any]) { + if isRightAligned { + for label in labels { + let y = transform(toChartCoordinateVertical: label.value, chartFrame: chartFrame) - labelsFont.pointSize - lavelsAsisOffset + + let rect = (label.text as NSString).boundingRect(with: bounds.size, + options: .usesLineFragmentOrigin, + attributes: attributes, + context: nil) + + (label.text as NSString).draw(at: CGPoint(x:chartFrame.maxX - rect.width, y: y), withAttributes: attributes) + } + } else { + for label in labels { + let y = transform(toChartCoordinateVertical: label.value, chartFrame: chartFrame) - labelsFont.pointSize - lavelsAsisOffset + + (label.text as NSString).draw(at: CGPoint(x:chartFrame.minX, y: y), withAttributes: attributes) + } + } + } + + let horizontalLinesAlpha = horizontalLinesAlphaAnimator.current + if horizontalLinesAlpha > 0 { + context.setLineWidth(horizontalLinesWidth) + + drawLines(verticalLabelsAndLines, alpha: generalAlpha) + for animatedLabesAndLines in animatedVerticalLabelsAndLines { + drawLines(animatedLabesAndLines.labels, alpha: animatedLabesAndLines.alphaAnimator.current * generalAlpha * horizontalLinesAlpha) + } + + if drawAxisX { + context.setLineWidth(axisXWidth) + context.setStrokeColor(axisXColor.withAlphaComponent(axisXColor.alphaValue * horizontalLinesAlpha * generalAlpha).cgColor) + + let lineSegments: [CGPoint] = [CGPoint(x: chartFrame.minX, y: chartFrame.maxY.roundedUpToPixelGrid()), + CGPoint(x: chartFrame.maxX, y: chartFrame.maxY.roundedUpToPixelGrid())] + + context.strokeLineSegments(between: lineSegments) + } + } + + drawVerticalLabels(verticalLabelsAndLines, attributes: [.foregroundColor: labelsColor.withAlphaComponent(labelColorAlpha * generalAlpha), + .font: labelsFont]) + for animatedLabesAndLines in animatedVerticalLabelsAndLines { + drawVerticalLabels(animatedLabesAndLines.labels, + attributes: [.foregroundColor: labelsColor.withAlphaComponent(animatedLabesAndLines.alphaAnimator.current * labelColorAlpha * generalAlpha), + .font: labelsFont]) + } + } +} diff --git a/submodules/Charts/Sources/Helpers/AnimationController.swift b/submodules/Charts/Sources/Helpers/AnimationController.swift new file mode 100644 index 0000000000..6df8d17f76 --- /dev/null +++ b/submodules/Charts/Sources/Helpers/AnimationController.swift @@ -0,0 +1,178 @@ +// +// RangeAnimatedContainer.swift +// GraphTest +// +// Created by Andrei Salavei on 3/12/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +protocol Animatable { + static func valueBetween(start: Self, end: Self, offset: Double) -> Self +} + +enum TimeFunction { + case linear + case easeOut + case easeIn + + func profress(time: TimeInterval, duration: TimeInterval) -> TimeInterval { + switch self { + case .linear: + return time / duration + case .easeIn: + return (pow(2, 10 * (time / duration - 1)) - 0.0009765625) * 1.0009775171065499 + + case .easeOut: + return (-pow(2, -10 * time / duration)) + 1 * 1.0009775171065499 + } + } +} + +class AnimationController { + + private(set) var isAnimating: Bool = false + private(set) var animationDuration: TimeInterval = 0.0 + private(set) var currentTime: TimeInterval = 0.0 + + private(set) var start: AnimatableObject + private(set) var end: AnimatableObject + private(set) var current: AnimatableObject + + var timeFunction: TimeFunction = .linear + + var refreshClosure: (() -> Void)? +// var updateClosure: ((AnimatableObject) -> Void)? + var completionClosure: (() -> Void)? + + init(current: AnimatableObject, refreshClosure: (() -> Void)?) { + self.current = current + self.start = current + self.end = current + self.refreshClosure = refreshClosure + } + + func animate(to: AnimatableObject, duration: TimeInterval, timeFunction: TimeFunction = .linear) { + self.timeFunction = timeFunction + currentTime = 0 + animationDuration = duration + if animationDuration > 0 { + start = current + end = to + isAnimating = true + DisplayLinkService.shared.add(listner: self) + } else { + start = to + end = to + current = to + isAnimating = false + DisplayLinkService.shared.remove(listner: self) + } + refreshClosure?() + } + + func set(current: AnimatableObject) { + self.start = current + self.end = current + self.current = current + + animationDuration = 0.0 + currentTime = 0.0 +// updateClosure?(current) + refreshClosure?() + if isAnimating { + isAnimating = false + DisplayLinkService.shared.remove(listner: self) + } + } +} + +extension AnimationController: DisplayLinkListner { + func update(delta: TimeInterval) { + guard isAnimating else { + DisplayLinkService.shared.remove(listner: self) + return + } + + currentTime += delta + if currentTime > animationDuration || animationDuration <= 0 { + start = end + current = end + isAnimating = false + animationDuration = 0.0 + currentTime = 0.0 +// updateClosure?(end) + completionClosure?() + refreshClosure?() + DisplayLinkService.shared.remove(listner: self) + } else { + let offset = timeFunction.profress(time: currentTime, duration: animationDuration) + current = AnimatableObject.valueBetween(start: start, end: end, offset: offset) +// updateClosure?(current) + refreshClosure?() + } + } +} + +extension ClosedRange: Animatable where Bound: BinaryFloatingPoint { + static func valueBetween(start: ClosedRange, end: ClosedRange, offset: Double) -> ClosedRange { + let castedOffset = Bound(offset) + return ClosedRange(uncheckedBounds: (lower: start.lowerBound + (end.lowerBound - start.lowerBound) * castedOffset, + upper: start.upperBound + (end.upperBound - start.upperBound) * castedOffset)) + } +} + +extension CGFloat: Animatable { + static func valueBetween(start: CGFloat, end: CGFloat, offset: Double) -> CGFloat { + return start + (end - start) * CGFloat(offset) + } +} + +extension Double: Animatable { + static func valueBetween(start: Double, end: Double, offset: Double) -> Double { + return start + (end - start) * Double(offset) + } +} + +extension Int: Animatable { + static func valueBetween(start: Int, end: Int, offset: Double) -> Int { + return start + Int(Double(end - start) * offset) + } +} + +extension CGPoint: Animatable { + static func valueBetween(start: CGPoint, end: CGPoint, offset: Double) -> CGPoint { + return CGPoint(x: start.x + (end.x - start.x) * CGFloat(offset), + y: start.y + (end.y - start.y) * CGFloat(offset)) + } +} + +extension CGRect: Animatable { + static func valueBetween(start: CGRect, end: CGRect, offset: Double) -> CGRect { + return CGRect(x: start.origin.x + (end.origin.x - start.origin.x) * CGFloat(offset), + y: start.origin.y + (end.origin.y - start.origin.y) * CGFloat(offset), + width: start.width + (end.width - start.width) * CGFloat(offset), + height: start.height + (end.height - start.height) * CGFloat(offset)) + } +} + +struct UIColorContainer: Animatable { + var color: UIColor + + static func valueBetween(start: UIColorContainer, end: UIColorContainer, offset: Double) -> UIColorContainer { + return UIColorContainer(color: UIColor.valueBetween(start: start.color, end: end.color, offset: offset)) + } +} + +extension UIColor { + static func valueBetween(start: UIColor, end: UIColor, offset: Double) -> UIColor { + let offsetF = CGFloat(offset) + let startCIColor = CIColor(color: start) + let endCIColor = CIColor(color: end) + return UIColor(red: startCIColor.red + (endCIColor.red - startCIColor.red) * offsetF, + green: startCIColor.green + (endCIColor.green - startCIColor.green) * offsetF, + blue: startCIColor.blue + (endCIColor.blue - startCIColor.blue) * offsetF, + alpha: startCIColor.alpha + (endCIColor.alpha - startCIColor.alpha) * offsetF) + } +} diff --git a/submodules/Charts/Sources/Helpers/Array+Utils.swift b/submodules/Charts/Sources/Helpers/Array+Utils.swift new file mode 100644 index 0000000000..539a559756 --- /dev/null +++ b/submodules/Charts/Sources/Helpers/Array+Utils.swift @@ -0,0 +1,18 @@ +// +// Array+Utils.swift +// GraphTest +// +// Created by Andrei Salavei on 4/7/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import Foundation + +extension Array { + func safeElement(at index: Int) -> Element? { + if index >= 0 && index < count { + return self[index] + } + return nil + } +} diff --git a/submodules/Charts/Sources/Helpers/CGFloat.swift b/submodules/Charts/Sources/Helpers/CGFloat.swift new file mode 100644 index 0000000000..7bde39b84f --- /dev/null +++ b/submodules/Charts/Sources/Helpers/CGFloat.swift @@ -0,0 +1,17 @@ +// +// CGFloat.swift +// GraphTest +// +// Created by Andrei Salavei on 4/11/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +private let screenScale: CGFloat = UIScreen.main.scale + +extension CGFloat { + func roundedUpToPixelGrid() -> CGFloat { + return (self * screenScale).rounded(.up) / screenScale + } +} diff --git a/submodules/Charts/Sources/Helpers/CGPoint+Extensions.swift b/submodules/Charts/Sources/Helpers/CGPoint+Extensions.swift new file mode 100644 index 0000000000..b5bbd06da4 --- /dev/null +++ b/submodules/Charts/Sources/Helpers/CGPoint+Extensions.swift @@ -0,0 +1,219 @@ +// +// CGPoint+Extensions.swift +// GraphTest +// +// Created by Andrei Salavei on 4/11/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +extension CGPoint { + public init(vector: CGVector) { + self.init(x: vector.dx, y: vector.dy) + } + + + public init(angle: CGFloat) { + self.init(x: cos(angle), y: sin(angle)) + } + + + public mutating func offset(dx: CGFloat, dy: CGFloat) -> CGPoint { + x += dx + y += dy + return self + } + + public func length() -> CGFloat { + return sqrt(x*x + y*y) + } + + public func lengthSquared() -> CGFloat { + return x*x + y*y + } + + func normalized() -> CGPoint { + let len = length() + return len>0 ? self / len : CGPoint.zero + } + + public mutating func normalize() -> CGPoint { + self = normalized() + return self + } + + public func distanceTo(_ point: CGPoint) -> CGFloat { + return (self - point).length() + } + + public var angle: CGFloat { + return atan2(y, x) + } + + public var cgSize: CGSize { + return CGSize(width: x, height: y) + } + + func rotate(origin: CGPoint, angle: CGFloat) -> CGPoint { + let point = self - origin + let s = sin(angle) + let c = cos(angle) + return CGPoint(x: c * point.x - s * point.y, + y: s * point.x + c * point.y) + origin + } +} + +extension CGSize { + public var cgPoint: CGPoint { + return CGPoint(x: width, y: height) + } + + public init(point: CGPoint) { + self.init(width: point.x, height: point.y) + } +} + +public func + (left: CGPoint, right: CGPoint) -> CGPoint { + return CGPoint(x: left.x + right.x, y: left.y + right.y) +} + +public func += (left: inout CGPoint, right: CGPoint) { + left = left + right +} + +public func + (left: CGPoint, right: CGVector) -> CGPoint { + return CGPoint(x: left.x + right.dx, y: left.y + right.dy) +} + +public func += (left: inout CGPoint, right: CGVector) { + left = left + right +} + +public func - (left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x - right.x, y: left.y - right.y) } +public func - (left: CGSize, right: CGSize) -> CGSize { return CGSize(width: left.width - right.width, height: left.height - right.height) } +public func - (left: CGSize, right: CGPoint) -> CGSize { return CGSize(width: left.width - right.x, height: left.height - right.x) } +public func - (left: CGPoint, right: CGSize) -> CGPoint { return CGPoint(x: left.x - right.width, y: left.y - right.height) } + +public func -= (left: inout CGPoint, right: CGPoint) { + left = left - right +} + +public func - (left: CGPoint, right: CGVector) -> CGPoint { + return CGPoint(x: left.x - right.dx, y: left.y - right.dy) +} + +public func -= (left: inout CGPoint, right: CGVector) { + left = left - right +} + +public func *= (left: inout CGPoint, right: CGPoint) { + left = left * right +} + +public func * (point: CGPoint, scalar: CGFloat) -> CGPoint { return CGPoint(x: point.x * scalar, y: point.y * scalar) } +public func * (point: CGSize, scalar: CGFloat) -> CGSize { return CGSize(width: point.width * scalar, height: point.height * scalar) } + +public func *= (point: inout CGPoint, scalar: CGFloat) { point = point * scalar } + +public func * (left: CGPoint, right: CGVector) -> CGPoint { + return CGPoint(x: left.x * right.dx, y: left.y * right.dy) +} + +public func *= (left: inout CGPoint, right: CGVector) { + left = left * right +} + +public func / (left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x / right.x, y: left.y / right.y) } +public func / (left: CGSize, right: CGSize) -> CGSize { return CGSize(width: left.width / right.width, height: left.height / right.height) } +public func / (left: CGPoint, right: CGSize) -> CGPoint { return CGPoint(x: left.x / right.width, y: left.y / right.height) } +public func / (left: CGSize, right: CGPoint) -> CGSize { return CGSize(width: left.width / right.x, height: left.height / right.y) } +public func /= (left: inout CGPoint, right: CGPoint) { left = left / right } +public func /= (left: inout CGSize, right: CGSize) { left = left / right } +public func /= (left: inout CGSize, right: CGPoint) { left = left / right } +public func /= (left: inout CGPoint, right: CGSize) { left = left / right } + + +public func / (point: CGPoint, scalar: CGFloat) -> CGPoint { return CGPoint(x: point.x / scalar, y: point.y / scalar) } +public func / (point: CGSize, scalar: CGFloat) -> CGSize { return CGSize(width: point.width / scalar, height: point.height / scalar) } + +public func /= (point: inout CGPoint, scalar: CGFloat) { + point = point / scalar +} + +public func / (left: CGPoint, right: CGVector) -> CGPoint { + return CGPoint(x: left.x / right.dx, y: left.y / right.dy) +} + +public func / (left: CGSize, right: CGVector) -> CGSize { + return CGSize(width: left.width / right.dx, height: left.height / right.dy) +} + +public func /= (left: inout CGPoint, right: CGVector) { + left = left / right +} + +public func * (left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x * right.x, y: left.y * right.y) } +public func * (left: CGPoint, right: CGSize) -> CGPoint { return CGPoint(x: left.x * right.width, y: left.y * right.height) } +public func *= (left: inout CGPoint, right: CGSize) { left = left * right } +public func * (left: CGSize, right: CGSize) -> CGSize { return CGSize(width: left.width * right.width, height: left.height * right.height) } +public func *= (left: inout CGSize, right: CGSize) { left = left * right } +public func * (left: CGSize, right: CGPoint) -> CGSize { return CGSize(width: left.width * right.x, height: left.height * right.y) } +public func *= (left: inout CGSize, right: CGPoint) { left = left * right } + + +public func lerp(start: CGPoint, end: CGPoint, t: CGFloat) -> CGPoint { + return start + (end - start) * t +} + +public func abs(_ point: CGPoint) -> CGPoint { + return CGPoint(x: abs(point.x), y: abs(point.y)) +} + +extension CGSize { + var isValid: Bool { + return width > 0 && height > 0 && width != .infinity && height != .infinity && width != .nan && height != .nan + } + + var ratio: CGFloat { + return width / height + } +} + + +extension CGRect { + static var identity: CGRect { + return CGRect(x: 0, y: 0, width: 1, height: 1) + } + + var center: CGPoint { + return origin + size.cgPoint / 2 + } + + var rounded: CGRect { + return CGRect(x: origin.x.rounded(), + y: origin.y.rounded(), + width: width.rounded(.up), + height: height.rounded(.up)) + } + + var mirroredVertically: CGRect { + return CGRect(x: origin.x, + y: 1.0 - (origin.y + height), + width: width, + height: height) + } +} + +extension CGAffineTransform { + func inverted(with size: CGSize) -> CGAffineTransform { + var transform = self + let transformedSize = CGRect(origin: .zero, size: size).applying(transform).size + transform.tx /= transformedSize.width; + transform.ty /= transformedSize.height; + transform = transform.inverted() + transform.tx *= transformedSize.width; + transform.ty *= transformedSize.height; + return transform + } +} diff --git a/submodules/Charts/Sources/Helpers/ClosedRange+Utils.swift b/submodules/Charts/Sources/Helpers/ClosedRange+Utils.swift new file mode 100644 index 0000000000..236d6c8a38 --- /dev/null +++ b/submodules/Charts/Sources/Helpers/ClosedRange+Utils.swift @@ -0,0 +1,15 @@ +// +// ClosedRange+Utils.swift +// GraphTest +// +// Created by Andrei Salavei on 3/11/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import Foundation + +extension ClosedRange where Bound: Numeric { + var distance: Bound { + return upperBound - lowerBound + } +} diff --git a/submodules/Charts/Sources/Helpers/CustomNavigationController.swift b/submodules/Charts/Sources/Helpers/CustomNavigationController.swift new file mode 100644 index 0000000000..977244dfcc --- /dev/null +++ b/submodules/Charts/Sources/Helpers/CustomNavigationController.swift @@ -0,0 +1,19 @@ +// +// CustomNavigationController.swift +// GraphTest +// +// Created by Andrew Solovey on 15/03/2019. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +class CustomNavigationController: UINavigationController { + override var preferredStatusBarStyle: UIStatusBarStyle { + return topViewController?.preferredStatusBarStyle ?? .default + } + + override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { + return topViewController?.preferredStatusBarUpdateAnimation ?? .fade + } +} diff --git a/submodules/Charts/Sources/Helpers/DisplayLinkService.swift b/submodules/Charts/Sources/Helpers/DisplayLinkService.swift new file mode 100644 index 0000000000..2c91471c39 --- /dev/null +++ b/submodules/Charts/Sources/Helpers/DisplayLinkService.swift @@ -0,0 +1,114 @@ +// +// DisplayLinkService.swift +// GraphTest +// +// Created by Andrei Salavei on 4/7/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit +import CoreGraphics + +public protocol DisplayLinkListner: class { + func update(delta: TimeInterval) +} + +// DispatchSource mares refreshes more accurate +class DisplayLinkService { + let listners = NSHashTable.weakObjects() + static let shared = DisplayLinkService() + + public func add(listner: DisplayLinkListner) { + listners.add(listner) + startDisplayLink() + } + + public func remove(listner: DisplayLinkListner) { + listners.remove(listner) + + if listners.count == 0 { + stopDisplayLink() + } + } + +// private init() { +// displayLink.add(to: .main, forMode: .common) +// displayLink.preferredFramesPerSecond = 60 +// displayLink.isPaused = true +// } +// +// // MARK: - Display Link +// private lazy var displayLink: CADisplayLink! = { CADisplayLink(target: self, selector: #selector(displayLinkDidFire)) } () +// private var previousTickTime = 0.0 +// +// private func startDisplayLink() { +// guard displayLink.isPaused else { +// return +// } +// previousTickTime = CACurrentMediaTime() +// displayLink.isPaused = false +// } +// +// @objc private func displayLinkDidFire(_ displayLink: CADisplayLink) { +// let currentTime = CACurrentMediaTime() +// let delta = currentTime - previousTickTime +// previousTickTime = currentTime +// let allListners = listners.allObjects +// var hasListners = false +// for listner in allListners { +// (listner as! DisplayLinkListner).update(delta: delta) +// hasListners = true +// } +// +// if !hasListners { +// stopDisplayLink() +// } +// } +// +// private func stopDisplayLink() { +// displayLink.isPaused = true +// } + + private init() { + dispatchSourceTimer.schedule(deadline: .now() + 1.0 / 60, repeating: 1.0 / 60) + dispatchSourceTimer.setEventHandler { + DispatchQueue.main.sync { + self.fire() + } + } + } + + private var dispatchSourceTimer = DispatchSource.makeTimerSource(flags: [], queue: .global(qos: .userInteractive)) + private var dispatchSourceTimerStarted: Bool = false + private var previousTickTime = 0.0 + + private func startDisplayLink() { + guard !dispatchSourceTimerStarted else { return } + dispatchSourceTimerStarted = true + previousTickTime = CACurrentMediaTime() + dispatchSourceTimer.resume() + } + + private func stopDisplayLink() { + guard dispatchSourceTimerStarted else { return } + dispatchSourceTimerStarted = false + dispatchSourceTimer.suspend() + } + + public func fire() { + let currentTime = CACurrentMediaTime() + + let delta = currentTime - previousTickTime + previousTickTime = currentTime + let allListners = listners.allObjects + var hasListners = false + for listner in allListners { + (listner as! DisplayLinkListner).update(delta: delta) + hasListners = true + } + + if !hasListners { + stopDisplayLink() + } + } +} diff --git a/submodules/Charts/Sources/Helpers/GlobalHelpers.swift b/submodules/Charts/Sources/Helpers/GlobalHelpers.swift new file mode 100644 index 0000000000..3f5c488ff9 --- /dev/null +++ b/submodules/Charts/Sources/Helpers/GlobalHelpers.swift @@ -0,0 +1,12 @@ +// +// GlobalHelpers.swift +// TrackingRecorder +// +// Created by Andrew Solovey on 07.09.2018. +// Copyright © 2018 Andrew Solovey. All rights reserved. +// + +public func crop(_ lower: Type, _ val: Type, _ upper: Type) -> Type where Type : Comparable { + assert(lower < upper, "Invalid lover and upper values") + return max(lower, min(upper, val)) +} diff --git a/submodules/Charts/Sources/Helpers/NumberFormatter+Utils.swift b/submodules/Charts/Sources/Helpers/NumberFormatter+Utils.swift new file mode 100644 index 0000000000..1254d44249 --- /dev/null +++ b/submodules/Charts/Sources/Helpers/NumberFormatter+Utils.swift @@ -0,0 +1,19 @@ +// +// NumberFormatter+Utils.swift +// GraphTest +// +// Created by Andrei Salavei on 4/12/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +extension NumberFormatter { + func string(from value: CGFloat) -> String { + return string(from: Double(value)) + } + + func string(from value: Double) -> String { + return string(from: NSNumber(value: Double(value))) ?? "" + } +} diff --git a/submodules/Charts/Sources/Helpers/OnePixelConstraint.swift b/submodules/Charts/Sources/Helpers/OnePixelConstraint.swift new file mode 100644 index 0000000000..03e33e6524 --- /dev/null +++ b/submodules/Charts/Sources/Helpers/OnePixelConstraint.swift @@ -0,0 +1,17 @@ +// +// OnePixelConstraint.swift +// GraphTest +// +// Created by Andrei Salavei on 4/13/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +public class OnePixelConstrain: NSLayoutConstraint { + public override func awakeFromNib() { + super.awakeFromNib() + + constant = UIView.oneDevicePixel + } +} diff --git a/submodules/Charts/Sources/Helpers/ScalesNumberFormatter.swift b/submodules/Charts/Sources/Helpers/ScalesNumberFormatter.swift new file mode 100644 index 0000000000..db067f8a95 --- /dev/null +++ b/submodules/Charts/Sources/Helpers/ScalesNumberFormatter.swift @@ -0,0 +1,32 @@ +// +// ScalesNumberFormatter.swift +// GraphTest +// +// Created by Andrei Salavei on 4/13/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +private let milionsScale = "M" +private let thousandsScale = "K" + +class ScalesNumberFormatter: NumberFormatter { + override func string(from number: NSNumber) -> String? { + let value = number.doubleValue + let pow = log10(value) + if pow >= 6 { + guard let string = super.string(from: NSNumber(value: value / 1_000_000)) else { + return nil + } + return string + milionsScale + } else if pow >= 4 { + guard let string = super.string(from: NSNumber(value: value / 1_000)) else { + return nil + } + return string + thousandsScale + } else { + return super.string(from: number) + } + } +} diff --git a/submodules/Charts/Sources/Helpers/TimeInterval+Utils.swift b/submodules/Charts/Sources/Helpers/TimeInterval+Utils.swift new file mode 100644 index 0000000000..204b1e861a --- /dev/null +++ b/submodules/Charts/Sources/Helpers/TimeInterval+Utils.swift @@ -0,0 +1,27 @@ +// +// TimeInterval+Utils.swift +// GraphTest +// +// Created by Andrei Salavei on 3/13/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import Foundation + +extension TimeInterval { + static let minute: TimeInterval = 60 + static let hour: TimeInterval = 60 * 60 + static let day: TimeInterval = 60 * 60 * 24 + static let osXDuration: TimeInterval = 0.25 + static let expandAnimationDuration: TimeInterval = 0.4 + static var animationDurationMultipler: Double = 1.0 + + static var defaultDuration: TimeInterval { + return innerDefaultDuration * animationDurationMultipler + } + private static var innerDefaultDuration: TimeInterval = osXDuration + + static func setDefaultSuration(_ duration: TimeInterval) { + innerDefaultDuration = duration + } +} diff --git a/submodules/Charts/Sources/Helpers/TimeZone.swift b/submodules/Charts/Sources/Helpers/TimeZone.swift new file mode 100644 index 0000000000..40ba9ab8f5 --- /dev/null +++ b/submodules/Charts/Sources/Helpers/TimeZone.swift @@ -0,0 +1,36 @@ +// +// TimeZone.swift +// GraphTest +// +// Created by Andrei Salavei on 4/9/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import Foundation + +extension TimeZone { + static let utc = TimeZone(secondsFromGMT: 0)! +} + +extension Locale { + static let posix = Locale(identifier: "en_US_POSIX") +} + +extension Calendar { + static let utc: Calendar = { + var calendar = Calendar.current + calendar.locale = Locale.posix + calendar.timeZone = TimeZone.utc + return calendar + }() +} + +extension DateFormatter { + static func utc(format: String = "") -> DateFormatter { + let formatter = DateFormatter() + formatter.calendar = Calendar.utc + formatter.dateFormat = format + formatter.timeZone = TimeZone.utc + return formatter + } +} diff --git a/submodules/Charts/Sources/Helpers/UIColor+Utils.swift b/submodules/Charts/Sources/Helpers/UIColor+Utils.swift new file mode 100644 index 0000000000..0a70421d5e --- /dev/null +++ b/submodules/Charts/Sources/Helpers/UIColor+Utils.swift @@ -0,0 +1,64 @@ +// +// UIColor+Utils.swift +// GraphTest +// +// Created by Andrei Salavei on 3/11/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +extension UIColor { + public convenience init?(hexString: String) { + let r, g, b, a: CGFloat + + if hexString.hasPrefix("#") { + let start = hexString.index(hexString.startIndex, offsetBy: 1) + let hexColor = String(hexString[start...]) + + if hexColor.count == 8 { + let scanner = Scanner(string: hexColor) + var hexNumber: UInt64 = 0 + + if scanner.scanHexInt64(&hexNumber) { + r = CGFloat((hexNumber & 0xff000000) >> 24) / 255 + g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255 + b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255 + a = CGFloat(hexNumber & 0x000000ff) / 255 + + self.init(red: r, green: g, blue: b, alpha: a) + return + } + } else if hexColor.count == 6 { + let scanner = Scanner(string: hexColor) + var hexNumber: UInt64 = 0 + + if scanner.scanHexInt64(&hexNumber) { + r = CGFloat((hexNumber & 0xff0000) >> 16) / 255 + g = CGFloat((hexNumber & 0x00ff00) >> 8) / 255 + b = CGFloat((hexNumber & 0x0000ff) >> 0) / 255 + + self.init(red: r, green: g, blue: b, alpha: 1.0) + return + } + } + } + return nil + } + + func image(_ size: CGSize = CGSize(width: 1, height: 1)) -> UIImage { + if #available(iOS 10.0, *) { + return UIGraphicsImageRenderer(size: size).image { rendererContext in + self.setFill() + rendererContext.fill(CGRect(origin: .zero, size: size)) + } + } else { + return UIImage() + } + } + + var redValue: CGFloat{ return CIColor(color: self).red } + var greenValue: CGFloat{ return CIColor(color: self).green } + var blueValue: CGFloat{ return CIColor(color: self).blue } + var alphaValue: CGFloat{ return CIColor(color: self).alpha } +} diff --git a/submodules/Charts/Sources/Helpers/UIImage+Utils.swift b/submodules/Charts/Sources/Helpers/UIImage+Utils.swift new file mode 100644 index 0000000000..50964025c2 --- /dev/null +++ b/submodules/Charts/Sources/Helpers/UIImage+Utils.swift @@ -0,0 +1,28 @@ +// +// UIImage+Utils.swift +// GraphTest +// +// Created by Andrei Salavei on 4/8/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +extension UIImage { + static let arrowRight = UIImage(bundleImageName: "Chart/arrow_right") + static let arrowLeft = UIImage(bundleImageName: "Chart/arrow_left") + + public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) { + let rect = CGRect(origin: .zero, size: size) + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0) + color.setFill() + UIRectFill(rect) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + guard let cgImage = image?.cgImage else { return nil } + self.init(cgImage: cgImage) + } + + +} diff --git a/submodules/Charts/Sources/Helpers/UIImageView+Utils.swift b/submodules/Charts/Sources/Helpers/UIImageView+Utils.swift new file mode 100644 index 0000000000..608f25a114 --- /dev/null +++ b/submodules/Charts/Sources/Helpers/UIImageView+Utils.swift @@ -0,0 +1,24 @@ +// +// UIImageView+Utils.swift +// GraphTest +// +// Created by Andrei Salavei on 4/9/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +extension UIImageView { + func setImage(_ image: UIImage?, animated: Bool) { + if self.image != image { + if animated { + let animation = CATransition() + animation.timingFunction = CAMediaTimingFunction.init(name: .linear) + animation.type = .fade + animation.duration = .defaultDuration + self.layer.add(animation, forKey: "kCATransitionImageFade") + } + self.image = image + } + } +} diff --git a/submodules/Charts/Sources/Helpers/UILabel+Utils.swift b/submodules/Charts/Sources/Helpers/UILabel+Utils.swift new file mode 100644 index 0000000000..6c41c73fd9 --- /dev/null +++ b/submodules/Charts/Sources/Helpers/UILabel+Utils.swift @@ -0,0 +1,37 @@ +// +// UILabel+Utils.swift +// GraphTest +// +// Created by Andrei Salavei on 4/9/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +extension UILabel { + func setTextColor(_ color: UIColor, animated: Bool) { + if self.textColor != color { + if animated { + let animation = CATransition() + animation.timingFunction = CAMediaTimingFunction.init(name: .linear) + animation.type = .fade + animation.duration = .defaultDuration + self.layer.add(animation, forKey: "kCATransitionColorFade") + } + self.textColor = color + } + } + + func setText(_ title: String?, animated: Bool) { + if self.text != title { + if animated { + let animation = CATransition() + animation.timingFunction = CAMediaTimingFunction.init(name: .linear) + animation.type = .fade + animation.duration = .defaultDuration + self.layer.add(animation, forKey: "kCATransitionTextFade") + } + self.text = title + } + } +} diff --git a/submodules/Charts/Sources/Helpers/UIView+Extensions.swift b/submodules/Charts/Sources/Helpers/UIView+Extensions.swift new file mode 100644 index 0000000000..bba2d1f886 --- /dev/null +++ b/submodules/Charts/Sources/Helpers/UIView+Extensions.swift @@ -0,0 +1,57 @@ +// +// UIView+Extensions.swift +// GraphTest +// +// Created by Andrei Salavei on 4/10/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +extension UIView { + static let oneDevicePixel: CGFloat = (1.0 / max(2, min(1, UIScreen.main.scale))) +} + +// MARK: UIView+Animation +public extension UIView { + func bringToFront() { + superview?.bringSubviewToFront(self) + } + + func layoutIfNeeded(animated: Bool) { + UIView.perform(animated: animated) { + self.layoutIfNeeded() + } + } + + func setVisible(_ visible: Bool, animated: Bool) { + let updatedAlpha: CGFloat = visible ? 1 : 0 + if self.alpha != updatedAlpha { + UIView.perform(animated: animated) { + self.alpha = updatedAlpha + } + } + } + + static func perform(animated: Bool, animations: @escaping () -> Void) { + perform(animated: animated, animations: animations, completion: { _ in }) + } + + static func perform(animated: Bool, animations: @escaping () -> Void, completion: @escaping (Bool) -> Void) { + if animated { + + UIView.animate(withDuration: .defaultDuration, delay: 0, animations: animations, completion: completion) + } else { + animations() + completion(true) + } + } + + var isVisibleInWindow: Bool { + guard let windowBounds = window?.bounds else { + return false + } + let frame = convert(bounds, to: nil) + return frame.intersects(windowBounds) + } +} diff --git a/submodules/Charts/Sources/Models/ChartLineData.swift b/submodules/Charts/Sources/Models/ChartLineData.swift new file mode 100644 index 0000000000..79813a2a35 --- /dev/null +++ b/submodules/Charts/Sources/Models/ChartLineData.swift @@ -0,0 +1,76 @@ +// +// ChartLineData.swift +// GraphTest +// +// Created by Andrei Salavei on 3/13/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +struct ChartLineData { + var title: String + var color: UIColor + var width: CGFloat? + var points: [CGPoint] +} + +extension ChartLineData { + static func horizontalRange(lines: [ChartLineData]) -> ClosedRange? { + guard let firstPoint = lines.first?.points.first else { return nil } + var hMin: CGFloat = firstPoint.x + var hMax: CGFloat = firstPoint.x + + for line in lines { + if let first = line.points.first, + let last = line.points.last { + hMin = min(hMin, first.x) + hMax = max(hMax, last.x) + } + } + + return hMin...hMax + } + + static func verticalRange(lines: [ChartLineData], calculatingRange: ClosedRange? = nil, addBounds: Bool = false) -> ClosedRange? { + if let calculatingRange = calculatingRange { + guard let initalStart = lines.first?.points.first(where: { $0.x > calculatingRange.lowerBound && + $0.x < calculatingRange.upperBound }) else { return nil } + var vMin: CGFloat = initalStart.y + var vMax: CGFloat = initalStart.y + for line in lines { + if var index = line.points.firstIndex(where: { $0.x > calculatingRange.lowerBound }) { + if addBounds { + index = max(0, index - 1) + } + while index < line.points.count { + let point = line.points[index] + if point.x < calculatingRange.upperBound { + vMin = min(vMin, point.y) + vMax = max(vMax, point.y) + } else if addBounds { + vMin = min(vMin, point.y) + vMax = max(vMax, point.y) + break + } else { + break + } + index += 1 + } + } + } + return vMin...vMax + } else { + guard let firstPoint = lines.first?.points.first else { return nil } + var vMin: CGFloat = firstPoint.y + var vMax: CGFloat = firstPoint.y + for line in lines { + for point in line.points { + vMin = min(vMin, point.y) + vMax = max(vMax, point.y) + } + } + return vMin...vMax + } + } +} diff --git a/submodules/Charts/Sources/Models/ColorMode.swift b/submodules/Charts/Sources/Models/ColorMode.swift new file mode 100644 index 0000000000..f0de7b52ae --- /dev/null +++ b/submodules/Charts/Sources/Models/ColorMode.swift @@ -0,0 +1,175 @@ +// +// ColorMode.swift +// GraphTest +// +// Created by Andrew Solovey on 15/03/2019. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit +import AppBundle + +protocol ColorModeContainer { + func apply(colorMode: ColorMode, animated: Bool) +} + +enum ColorMode { + case day + case night +} + +extension ColorMode { + var chartTitleColor: UIColor { // ТекÑÑ‚ Ñ Ð´Ð°Ñ‚Ð¾Ð¹ на чарте + switch self { + case .day: return .black + case .night: return .white + } + } + + var actionButtonColor: UIColor { // Кнопка Zoom Out/ Смена режима день/ночь + switch self { + case .day: return UIColor(red: 53/255.0, green: 120/255.0, blue: 246/255.0, alpha: 1.0) + case .night: return UIColor(red: 84/255.0, green: 164/255.0, blue: 247/255.0, alpha: 1.0) + } + } + + var tableBackgroundColor: UIColor { + switch self { + case .day: return UIColor(red: 239/255.0, green: 239/255.0, blue: 244/255.0, alpha: 1.0) + case .night: return UIColor(red: 24/255.0, green: 34/255.0, blue: 45/255.0, alpha: 1.0) + } + } + + var chartBackgroundColor: UIColor { + switch self { + case .day: return UIColor(red: 254/255.0, green: 254/255.0, blue: 254/255.0, alpha: 1.0) + case .night: return UIColor(red: 34/255.0, green: 47/255.0, blue: 63/255.0, alpha: 1.0) + } + } + + var sectionTitleColor: UIColor { + switch self { + case .day: return UIColor(red: 109/255.0, green: 109/255.0, blue: 114/255.0, alpha: 1.0) + case .night: return UIColor(red: 133/255.0, green: 150/255.0, blue: 171/255.0, alpha: 1.0) + } + } + + var tableSeparatorColor: UIColor { + switch self { + case .day: return UIColor(red: 200/255.0, green: 199/255.0, blue: 204/255.0, alpha: 1.0) + case .night: return UIColor(red: 18/255.0, green: 26/255.0, blue: 35/255.0, alpha: 1.0) + } + } + + var chartLabelsColor: UIColor { + switch self { + case .day: return UIColor(red: 37/255.0, green: 37/255.0, blue: 41/255.0, alpha: 0.5) + case .night: return UIColor(red: 186/255.0, green: 204/255.0, blue: 225/255.0, alpha: 0.6) + } + } + + var chartHelperLinesColor: UIColor { + switch self { + case .day: return UIColor(red: 24/255.0, green: 45/255.0, blue: 59/255.0, alpha: 0.1) + case .night: return UIColor(red: 133/255.0, green: 150/255.0, blue: 171/255.0, alpha: 0.20) + } + } + + var chartStrongLinesColor: UIColor { + switch self { + case .day: return UIColor(red: 24/255.0, green: 45/255.0, blue: 59/255.0, alpha: 0.35) + case .night: return UIColor(red: 186/255.0, green: 204/255.0, blue: 225/255.0, alpha: 0.45) + } + } + + var barChartStrongLinesColor: UIColor { + switch self { + case .day: return UIColor(red: 37/255.0, green: 37/255.0, blue: 41/255.0, alpha: 0.2) + case .night: return UIColor(red: 186/255.0, green: 204/255.0, blue: 225/255.0, alpha: 0.45) + } + } + + var chartDetailsTextColor: UIColor { + switch self { + case .day: return UIColor(red: 109/255.0, green: 109/255.0, blue: 114/255.0, alpha: 1.0) + case .night: return UIColor(red: 254/255.0, green: 254/255.0, blue: 254/255.0, alpha: 1.0) + } + } + + var chartDetailsArrowColor: UIColor { + switch self { + case .day: return UIColor(red: 197/255.0, green: 199/255.0, blue: 205/255.0, alpha: 1.0) + case .night: return UIColor(red: 76/255.0, green: 84/255.0, blue: 96/255.0, alpha: 1.0) + } + } + + var chartDetailsViewColor: UIColor { + switch self { + case .day: return UIColor(red: 245/255.0, green: 245/255.0, blue: 251/255.0, alpha: 1.0) + case .night: return UIColor(red: 25/255.0, green: 35/255.0, blue: 47/255.0, alpha: 1.0) + } + } + + var descriptionChatNameColor: UIColor { + switch self { + case .day: return .black + case .night: return UIColor(red: 254/255.0, green: 254/255.0, blue: 254/255.0, alpha: 1.0) + } + } + + var descriptionActionColor: UIColor { + switch self { + case .day: return UIColor(red: 1/255.0, green: 125/255.0, blue: 229/255.0, alpha: 1.0) + case .night: return UIColor(red: 24/255.0, green: 145/255.0, blue: 255/255.0, alpha: 1.0) + } + } + + var rangeViewBackgroundColor: UIColor { + switch self { + case .day: return UIColor(red: 254/255.0, green: 254/255.0, blue: 254/255.0, alpha: 1.0) + case .night: return UIColor(red: 34/255.0, green: 47/255.0, blue: 63/255.0, alpha: 1.0) + } + } + + var rangeViewFrameColor: UIColor { + switch self { + case .day: return UIColor(red: 202/255.0, green: 212/255.0, blue: 222/255.0, alpha: 1.0) + case .night: return UIColor(red: 53/255.0, green: 70/255.0, blue: 89/255.0, alpha: 1.0) + } + } + + var rangeViewTintColor: UIColor { + switch self { + case .day: return UIColor(red: 239/255.0, green: 239/255.0, blue: 244/255.0, alpha: 0.5) + case .night: return UIColor(red: 24/255.0, green: 34/255.0, blue: 45/255.0, alpha: 0.5) + } + } + + var rangeViewMarkerColor: UIColor { + switch self { + case .day: return UIColor.white + case .night: return UIColor.white + } + } + + var statusBarStyle: UIStatusBarStyle { + switch self { + case .day: return .default + case .night: return .lightContent + } + } + + var viewTintColor: UIColor { + switch self { + case .day: return .black + case .night: return UIColor(red: 254/255.0, green: 254/255.0, blue: 254/255.0, alpha: 1.0) + } + } + + var rangeCropImage: UIImage? { + switch self { + case .day: return UIImage(bundleImageName: "Chart/selection_frame_light") + case .night: return UIImage(bundleImageName: "Chart/selection_frame_dark") + } + } +} diff --git a/submodules/Charts/Sources/Models/LinesChartLabel.swift b/submodules/Charts/Sources/Models/LinesChartLabel.swift new file mode 100644 index 0000000000..6ace8c2c65 --- /dev/null +++ b/submodules/Charts/Sources/Models/LinesChartLabel.swift @@ -0,0 +1,25 @@ +// +// LinesChartLabel.swift +// GraphTest +// +// Created by Andrei Salavei on 3/18/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +struct LinesChartLabel: Hashable { + let value: CGFloat + let text: String +} + +class AnimatedLinesChartLabels { + var labels: [LinesChartLabel] + var isAppearing: Bool = false + let alphaAnimator: AnimationController + + init(labels: [LinesChartLabel], alphaAnimator: AnimationController) { + self.labels = labels + self.alphaAnimator = alphaAnimator + } +} diff --git a/submodules/Charts/Sources/Models/LinesSelectionLabel.swift b/submodules/Charts/Sources/Models/LinesSelectionLabel.swift new file mode 100644 index 0000000000..0fd7142eda --- /dev/null +++ b/submodules/Charts/Sources/Models/LinesSelectionLabel.swift @@ -0,0 +1,15 @@ +// +// LinesSelectionLabel.swift +// GraphTest +// +// Created by Andrei Salavei on 3/18/19. +// Copyright © 2019 Andrei Salavei. All rights reserved. +// + +import UIKit + +struct LinesSelectionLabel { + let coordinate: CGPoint + let valueText: String + let color: UIColor +} diff --git a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift index 2721ed5600..19fcbdb221 100644 --- a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift +++ b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift @@ -17,6 +17,8 @@ public enum ChatListSearchItemHeaderType: Int32 { case phoneNumber case exceptions case addToExceptions + case mapAddress + case nearbyVenues } public final class ChatListSearchItemHeader: ListViewItemHeader { @@ -30,7 +32,7 @@ public final class ChatListSearchItemHeader: ListViewItemHeader { public let height: CGFloat = 28.0 - public init(type: ChatListSearchItemHeaderType, theme: PresentationTheme, strings: PresentationStrings, actionTitle: String?, action: (() -> Void)?) { + public init(type: ChatListSearchItemHeaderType, theme: PresentationTheme, strings: PresentationStrings, actionTitle: String? = nil, action: (() -> Void)? = nil) { self.type = type self.id = Int64(self.type.rawValue) self.theme = theme @@ -42,14 +44,20 @@ public final class ChatListSearchItemHeader: ListViewItemHeader { public func node() -> ListViewItemHeaderNode { return ChatListSearchItemHeaderNode(type: self.type, theme: self.theme, strings: self.strings, actionTitle: self.actionTitle, action: self.action) } + + public func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) { + (node as? ChatListSearchItemHeaderNode)?.update(type: self.type, actionTitle: self.actionTitle, action: self.action) + } } public final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode { - private let type: ChatListSearchItemHeaderType + private var type: ChatListSearchItemHeaderType private var theme: PresentationTheme private var strings: PresentationStrings - private let actionTitle: String? - private let action: (() -> Void)? + private var actionTitle: String? + private var action: (() -> Void)? + + private var validLayout: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat)? private let sectionHeaderNode: ListSectionHeaderNode @@ -65,30 +73,34 @@ public final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode { super.init() switch type { - case .localPeers: - self.sectionHeaderNode.title = strings.DialogList_SearchSectionDialogs.uppercased() - case .members: - self.sectionHeaderNode.title = strings.Channel_Info_Members.uppercased() - case .contacts: - self.sectionHeaderNode.title = strings.Contacts_TopSection.uppercased() - case .bots: - self.sectionHeaderNode.title = strings.MemberSearch_BotSection.uppercased() - case .admins: - self.sectionHeaderNode.title = strings.Channel_Management_Title.uppercased() - case .globalPeers: - self.sectionHeaderNode.title = strings.DialogList_SearchSectionGlobal.uppercased() - case .deviceContacts: - self.sectionHeaderNode.title = strings.Contacts_NotRegisteredSection.uppercased() - case .messages: - self.sectionHeaderNode.title = strings.DialogList_SearchSectionMessages.uppercased() - case .recentPeers: - self.sectionHeaderNode.title = strings.DialogList_SearchSectionRecent.uppercased() - case .phoneNumber: - self.sectionHeaderNode.title = strings.Contacts_PhoneNumber.uppercased() - case .exceptions: - self.sectionHeaderNode.title = strings.GroupInfo_Permissions_Exceptions.uppercased() - case .addToExceptions: - self.sectionHeaderNode.title = strings.Exceptions_AddToExceptions.uppercased() + case .localPeers: + self.sectionHeaderNode.title = strings.DialogList_SearchSectionDialogs.uppercased() + case .members: + self.sectionHeaderNode.title = strings.Channel_Info_Members.uppercased() + case .contacts: + self.sectionHeaderNode.title = strings.Contacts_TopSection.uppercased() + case .bots: + self.sectionHeaderNode.title = strings.MemberSearch_BotSection.uppercased() + case .admins: + self.sectionHeaderNode.title = strings.Channel_Management_Title.uppercased() + case .globalPeers: + self.sectionHeaderNode.title = strings.DialogList_SearchSectionGlobal.uppercased() + case .deviceContacts: + self.sectionHeaderNode.title = strings.Contacts_NotRegisteredSection.uppercased() + case .messages: + self.sectionHeaderNode.title = strings.DialogList_SearchSectionMessages.uppercased() + case .recentPeers: + self.sectionHeaderNode.title = strings.DialogList_SearchSectionRecent.uppercased() + case .phoneNumber: + self.sectionHeaderNode.title = strings.Contacts_PhoneNumber.uppercased() + case .exceptions: + self.sectionHeaderNode.title = strings.GroupInfo_Permissions_Exceptions.uppercased() + case .addToExceptions: + self.sectionHeaderNode.title = strings.Exceptions_AddToExceptions.uppercased() + case .mapAddress: + self.sectionHeaderNode.title = strings.Map_AddressOnMap.uppercased() + case .nearbyVenues: + self.sectionHeaderNode.title = strings.Map_PlacesNearby.uppercased() } self.sectionHeaderNode.action = actionTitle @@ -102,7 +114,51 @@ public final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode { self.sectionHeaderNode.updateTheme(theme: theme) } + public func update(type: ChatListSearchItemHeaderType, actionTitle: String?, action: (() -> Void)?) { + self.actionTitle = actionTitle + self.action = action + + switch type { + case .localPeers: + self.sectionHeaderNode.title = strings.DialogList_SearchSectionDialogs.uppercased() + case .members: + self.sectionHeaderNode.title = strings.Channel_Info_Members.uppercased() + case .contacts: + self.sectionHeaderNode.title = strings.Contacts_TopSection.uppercased() + case .bots: + self.sectionHeaderNode.title = strings.MemberSearch_BotSection.uppercased() + case .admins: + self.sectionHeaderNode.title = strings.Channel_Management_Title.uppercased() + case .globalPeers: + self.sectionHeaderNode.title = strings.DialogList_SearchSectionGlobal.uppercased() + case .deviceContacts: + self.sectionHeaderNode.title = strings.Contacts_NotRegisteredSection.uppercased() + case .messages: + self.sectionHeaderNode.title = strings.DialogList_SearchSectionMessages.uppercased() + case .recentPeers: + self.sectionHeaderNode.title = strings.DialogList_SearchSectionRecent.uppercased() + case .phoneNumber: + self.sectionHeaderNode.title = strings.Contacts_PhoneNumber.uppercased() + case .exceptions: + self.sectionHeaderNode.title = strings.GroupInfo_Permissions_Exceptions.uppercased() + case .addToExceptions: + self.sectionHeaderNode.title = strings.Exceptions_AddToExceptions.uppercased() + case .mapAddress: + self.sectionHeaderNode.title = strings.Map_AddressOnMap.uppercased() + case .nearbyVenues: + self.sectionHeaderNode.title = strings.Map_PlacesNearby.uppercased() + } + + self.sectionHeaderNode.action = actionTitle + self.sectionHeaderNode.activateAction = action + + if let (size, leftInset, rightInset) = self.validLayout { + self.sectionHeaderNode.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset) + } + } + override public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + self.validLayout = (size, leftInset, rightInset) self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: size) self.sectionHeaderNode.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset) } diff --git a/submodules/ChatListSearchRecentPeersNode/BUCK b/submodules/ChatListSearchRecentPeersNode/BUCK index b167dabe08..6aefe829e5 100644 --- a/submodules/ChatListSearchRecentPeersNode/BUCK +++ b/submodules/ChatListSearchRecentPeersNode/BUCK @@ -17,6 +17,7 @@ static_library( "//submodules/MergeLists:MergeLists", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/ContextUI:ContextUI", + "//submodules/AccountContext:AccountContext", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift b/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift index c998b0cb76..554abd7173 100644 --- a/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift +++ b/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift @@ -11,6 +11,7 @@ import MergeLists import HorizontalPeerItem import ListSectionHeaderNode import ContextUI +import AccountContext private func calculateItemCustomWidth(width: CGFloat) -> CGFloat { let itemInsets = UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 6.0) @@ -78,8 +79,8 @@ private struct ChatListSearchRecentPeersEntry: Comparable, Identifiable { return lhs.index < rhs.index } - func item(account: Account, mode: HorizontalPeerItemMode, peerSelected: @escaping (Peer) -> Void, peerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, isPeerSelected: @escaping (PeerId) -> Bool) -> ListViewItem { - return HorizontalPeerItem(theme: self.theme, strings: self.strings, mode: mode, account: account, peer: self.peer, presence: self.presence, unreadBadge: self.unreadBadge, action: peerSelected, contextAction: { peer, node, gesture in + func item(context: AccountContext, mode: HorizontalPeerItemMode, peerSelected: @escaping (Peer) -> Void, peerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, isPeerSelected: @escaping (PeerId) -> Bool) -> ListViewItem { + return HorizontalPeerItem(theme: self.theme, strings: self.strings, mode: mode, context: context, peer: self.peer, presence: self.presence, unreadBadge: self.unreadBadge, action: peerSelected, contextAction: { peer, node, gesture in peerContextAction(peer, node, gesture) }, isPeerSelected: isPeerSelected, customWidth: self.itemCustomWidth) } @@ -93,12 +94,12 @@ private struct ChatListSearchRecentNodeTransition { let animated: Bool } -private func preparedRecentPeersTransition(account: Account, mode: HorizontalPeerItemMode, peerSelected: @escaping (Peer) -> Void, peerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, isPeerSelected: @escaping (PeerId) -> Bool, share: Bool = false, from fromEntries: [ChatListSearchRecentPeersEntry], to toEntries: [ChatListSearchRecentPeersEntry], firstTime: Bool, animated: Bool) -> ChatListSearchRecentNodeTransition { +private func preparedRecentPeersTransition(context: AccountContext, mode: HorizontalPeerItemMode, peerSelected: @escaping (Peer) -> Void, peerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, isPeerSelected: @escaping (PeerId) -> Bool, share: Bool = false, from fromEntries: [ChatListSearchRecentPeersEntry], to toEntries: [ChatListSearchRecentPeersEntry], firstTime: Bool, animated: Bool) -> ChatListSearchRecentNodeTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, mode: mode, peerSelected: peerSelected, peerContextAction: peerContextAction, isPeerSelected: isPeerSelected), directionHint: .Down) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, mode: mode, peerSelected: peerSelected, peerContextAction: peerContextAction, isPeerSelected: isPeerSelected), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, mode: mode, peerSelected: peerSelected, peerContextAction: peerContextAction, isPeerSelected: isPeerSelected), directionHint: .Down) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, mode: mode, peerSelected: peerSelected, peerContextAction: peerContextAction, isPeerSelected: isPeerSelected), directionHint: nil) } return ChatListSearchRecentNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, animated: animated) } @@ -122,7 +123,7 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { private var items: [ListViewItem] = [] private var queuedTransitions: [ChatListSearchRecentNodeTransition] = [] - public init(account: Account, theme: PresentationTheme, mode: HorizontalPeerItemMode, strings: PresentationStrings, peerSelected: @escaping (Peer) -> Void, peerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, isPeerSelected: @escaping (PeerId) -> Bool, share: Bool = false) { + public init(context: AccountContext, theme: PresentationTheme, mode: HorizontalPeerItemMode, strings: PresentationStrings, peerSelected: @escaping (Peer) -> Void, peerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, isPeerSelected: @escaping (PeerId) -> Bool, share: Bool = false) { self.theme = theme self.strings = strings self.themeAndStringsPromise = Promise((self.theme, self.strings)) @@ -145,7 +146,7 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { let peersDisposable = DisposableSet() - let recent: Signal<([Peer], [PeerId: (Int32, Bool)], [PeerId : PeerPresence]), NoError> = recentPeers(account: account) + let recent: Signal<([Peer], [PeerId: (Int32, Bool)], [PeerId : PeerPresence]), NoError> = recentPeers(account: context.account) |> filter { value -> Bool in switch value { case .disabled: @@ -159,8 +160,8 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { case .disabled: return .single(([], [:], [:])) case let .peers(peers): - return combineLatest(queue: .mainQueue(), peers.filter { !$0.isDeleted }.map {account.postbox.peerView(id: $0.id)}) |> mapToSignal { peerViews -> Signal<([Peer], [PeerId: (Int32, Bool)], [PeerId: PeerPresence]), NoError> in - return account.postbox.unreadMessageCountsView(items: peerViews.map { + return combineLatest(queue: .mainQueue(), peers.filter { !$0.isDeleted }.map {context.account.postbox.peerView(id: $0.id)}) |> mapToSignal { peerViews -> Signal<([Peer], [PeerId: (Int32, Bool)], [PeerId: PeerPresence]), NoError> in + return context.account.postbox.unreadMessageCountsView(items: peerViews.map { .peer($0.peerId) }) |> map { values in @@ -208,13 +209,13 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { let animated = !firstTime.swap(false) - let transition = preparedRecentPeersTransition(account: account, mode: mode, peerSelected: peerSelected, peerContextAction: peerContextAction, isPeerSelected: isPeerSelected, from: previous.swap(entries), to: entries, firstTime: !animated, animated: animated) + let transition = preparedRecentPeersTransition(context: context, mode: mode, peerSelected: peerSelected, peerContextAction: peerContextAction, isPeerSelected: isPeerSelected, from: previous.swap(entries), to: entries, firstTime: !animated, animated: animated) strongSelf.enqueueTransition(transition) } })) if case .actionSheet = mode { - peersDisposable.add(managedUpdatedRecentPeers(accountPeerId: account.peerId, postbox: account.postbox, network: account.network).start()) + peersDisposable.add(managedUpdatedRecentPeers(accountPeerId: context.account.peerId, postbox: context.account.postbox, network: context.account.network).start()) } self.disposable.set(peersDisposable) } diff --git a/submodules/ChatListUI/BUCK b/submodules/ChatListUI/BUCK index 5b26e2660a..77299792c5 100644 --- a/submodules/ChatListUI/BUCK +++ b/submodules/ChatListUI/BUCK @@ -41,6 +41,8 @@ static_library( "//submodules/AppBundle:AppBundle", "//submodules/ContextUI:ContextUI", "//submodules/PhoneNumberFormat:PhoneNumberFormat", + "//submodules/TelegramIntents:TelegramIntents", + "//submodules/ItemListPeerActionItem:ItemListPeerActionItem", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 1a70bab746..5d19de7a98 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -177,7 +177,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, source: ChatC if case .search = source { if let channel = peer as? TelegramChannel { items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_JoinChannel, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { _, f in - var createSignal = joinChannel(account: context.account, peerId: peerId) + var createSignal = context.peerChannelMemberCategoriesContextsManager.join(account: context.account, peerId: peerId) var cancelImpl: (() -> Void)? let progressSignal = Signal { subscriber in let presentationData = context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index b53fb53f8e..1eb8d5bb87 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -20,10 +20,7 @@ import LanguageSuggestionUI import ContextUI import AppBundle import LocalizedPeerData - -public func useSpecialTabBarIcons() -> Bool { - return (Date(timeIntervalSince1970: 1545642000)...Date(timeIntervalSince1970: 1546387200)).contains(Date()) -} +import TelegramIntents private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool { if searchNode.expansionProgress > 0.0 && searchNode.expansionProgress < 1.0 { @@ -98,7 +95,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent } } -public class ChatListControllerImpl: TelegramBaseController, ChatListController, UIViewControllerPreviewingDelegate { +public class ChatListControllerImpl: TelegramBaseController, ChatListController, UIViewControllerPreviewingDelegate/*, TabBarContainedController*/ { private var validLayout: ContainerViewLayout? public let context: AccountContext @@ -106,6 +103,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, private let hideNetworkActivityStatus: Bool public let groupId: PeerGroupId + public let filter: ChatListFilter? + public let previewing: Bool let openMessageFromSearchDisposable: MetaDisposable = MetaDisposable() @@ -122,9 +121,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, private var badgeIconDisposable: Disposable? private var dismissSearchOnDisappear = false - - private var didSetup3dTouch = false - + private var passcodeLockTooltipDisposable = MetaDisposable() private var didShowPasscodeLockTooltipController = false @@ -145,12 +142,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } } - public init(context: AccountContext, groupId: PeerGroupId, controlsHistoryPreload: Bool, hideNetworkActivityStatus: Bool = false, previewing: Bool = false, enableDebugActions: Bool) { + public init(context: AccountContext, groupId: PeerGroupId, filter: ChatListFilter? = nil, controlsHistoryPreload: Bool, hideNetworkActivityStatus: Bool = false, previewing: Bool = false, enableDebugActions: Bool) { self.context = context self.controlsHistoryPreload = controlsHistoryPreload self.hideNetworkActivityStatus = hideNetworkActivityStatus self.groupId = groupId + self.filter = filter + self.previewing = previewing self.presentationData = (context.sharedContext.currentPresentationData.with { $0 }) self.presentationDataValue.set(.single(self.presentationData)) @@ -162,7 +161,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style let title: String - if case .root = self.groupId { + if let filter = self.filter { + title = filter.title ?? "" + } else if self.groupId == .root { title = self.presentationData.strings.DialogList_Title self.navigationBar?.item = nil } else { @@ -173,12 +174,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, self.navigationItem.titleView = self.titleView if !previewing { - if case .root = groupId { + if self.groupId == .root && self.filter == nil { self.tabBarItem.title = self.presentationData.strings.DialogList_Title let icon: UIImage? - if (useSpecialTabBarIcons()) { - icon = UIImage(bundleImageName: "Chat List/Tabs/NY/IconChats") + if useSpecialTabBarIcons() { + icon = UIImage(bundleImageName: "Chat List/Tabs/Holiday/IconChats") } else { icon = UIImage(bundleImageName: "Chat List/Tabs/IconChats") } @@ -226,7 +227,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } strongSelf.chatListDisplayNode.chatListNode.scrollToPosition(.top) } - //.auto for unread navigation } self.longTapWithTabBar = { [weak self] in guard let strongSelf = self else { @@ -261,16 +261,27 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } if !self.hideNetworkActivityStatus { - self.titleDisposable = combineLatest(queue: .mainQueue(), context.account.networkState, hasProxy, passcode, self.chatListDisplayNode.chatListNode.state).start(next: { [weak self] networkState, proxy, passcode, state in + self.titleDisposable = combineLatest(queue: .mainQueue(), + context.account.networkState, + hasProxy, + passcode, + self.chatListDisplayNode.chatListNode.state, + self.chatListDisplayNode.chatListNode.chatListFilterSignal + ).start(next: { [weak self] networkState, proxy, passcode, state, chatListFilter in if let strongSelf = self { let defaultTitle: String - if case .root = strongSelf.groupId { - defaultTitle = strongSelf.presentationData.strings.DialogList_Title + if strongSelf.groupId == .root { + if let chatListFilter = chatListFilter { + let title: String = chatListFilter.title ?? "" + defaultTitle = title + } else { + defaultTitle = strongSelf.presentationData.strings.DialogList_Title + } } else { defaultTitle = strongSelf.presentationData.strings.ChatList_ArchivedChatsTitle } if state.editing { - if case .root = strongSelf.groupId { + if strongSelf.groupId == .root && strongSelf.filter == nil { strongSelf.navigationItem.rightBarButtonItem = nil } @@ -280,9 +291,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, var isRoot = false if case .root = strongSelf.groupId { isRoot = true - let rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(strongSelf.presentationData.theme), style: .plain, target: strongSelf, action: #selector(strongSelf.composePressed)) - rightBarButtonItem.accessibilityLabel = strongSelf.presentationData.strings.VoiceOver_Navigation_Compose - strongSelf.navigationItem.rightBarButtonItem = rightBarButtonItem + if strongSelf.filter == nil { + let rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(strongSelf.presentationData.theme), style: .plain, target: strongSelf, action: #selector(strongSelf.composePressed)) + rightBarButtonItem.accessibilityLabel = strongSelf.presentationData.strings.VoiceOver_Navigation_Compose + strongSelf.navigationItem.rightBarButtonItem = rightBarButtonItem + } } let (hasProxy, connectsViaProxy) = proxy @@ -305,10 +318,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, case .online: strongSelf.titleView.title = NetworkStatusTitle(text: defaultTitle, activity: false, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked) } - if case .root = groupId, checkProxy { - if strongSelf.proxyUnavailableTooltipController == nil && !strongSelf.didShowProxyUnavailableTooltipController && strongSelf.isNodeLoaded && strongSelf.displayNode.view.window != nil { + if groupId == .root && filter == nil && checkProxy { + if strongSelf.proxyUnavailableTooltipController == nil && !strongSelf.didShowProxyUnavailableTooltipController && strongSelf.isNodeLoaded && strongSelf.displayNode.view.window != nil && strongSelf.navigationController?.topViewController === self { strongSelf.didShowProxyUnavailableTooltipController = true - let tooltipController = TooltipController(content: .text(strongSelf.presentationData.strings.Proxy_TooltipUnavailable), timeout: 60.0, dismissByTapOutside: true) + let tooltipController = TooltipController(content: .text(strongSelf.presentationData.strings.Proxy_TooltipUnavailable), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, timeout: 60.0, dismissByTapOutside: true) strongSelf.proxyUnavailableTooltipController = tooltipController tooltipController.dismissed = { [weak tooltipController] _ in if let strongSelf = self, let tooltipController = tooltipController, strongSelf.proxyUnavailableTooltipController === tooltipController { @@ -371,11 +384,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } }) - self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.DialogList_SearchLabel, activate: { [weak self] in - self?.activateSearch() - }) - self.searchContentNode?.updateExpansionProgress(0.0) - self.navigationBar?.setContentNode(self.searchContentNode, animated: false) + if !previewing { + self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.DialogList_SearchLabel, activate: { [weak self] in + self?.activateSearch() + }) + self.searchContentNode?.updateExpansionProgress(0.0) + self.navigationBar?.setContentNode(self.searchContentNode, animated: false) + } if enableDebugActions { self.tabBarItemDebugTapAction = { @@ -425,7 +440,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) editItem.accessibilityLabel = self.presentationData.strings.Common_Edit } - if case .root = self.groupId { + if self.groupId == .root && self.filter == nil { self.navigationItem.leftBarButtonItem = editItem let rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.composePressed)) rightBarButtonItem.accessibilityLabel = self.presentationData.strings.VoiceOver_Navigation_Compose @@ -445,7 +460,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } override public func loadDisplayNode() { - self.displayNode = ChatListControllerNode(context: self.context, groupId: self.groupId, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, controller: self) + self.displayNode = ChatListControllerNode(context: self.context, groupId: self.groupId, filter: self.filter, previewing: self.previewing, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, controller: self) self.chatListDisplayNode.navigationBar = self.navigationBar @@ -588,7 +603,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, self.chatListDisplayNode.requestOpenRecentPeerOptions = { [weak self] peer in if let strongSelf = self { strongSelf.view.window?.endEditing(true) - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) actionSheet.setItemGroups([ ActionSheetItemGroup(items: [ @@ -601,7 +616,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) @@ -669,12 +684,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, case let .groupReference(groupReference): let chatListController = ChatListControllerImpl(context: strongSelf.context, groupId: groupReference.groupId, controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false) chatListController.navigationPresentation = .master - let contextController = ContextController(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: archiveContextMenuItems(context: strongSelf.context, groupId: groupReference.groupId, chatListController: strongSelf), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: archiveContextMenuItems(context: strongSelf.context, groupId: groupReference.groupId, chatListController: strongSelf), reactionItems: [], gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) case let .peer(peer): let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peer.peer.peerId), subject: nil, botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) - let contextController = ContextController(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peer.peerId, source: .chatList, chatListController: strongSelf), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peer.peerId, source: .chatList, chatListController: strongSelf), reactionItems: [], gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } } @@ -687,7 +702,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) - let contextController = ContextController(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, source: .search(source), chatListController: strongSelf), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, source: .search(source), chatListController: strongSelf), reactionItems: [], gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } @@ -760,17 +775,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, self.displayNodeDidLoad() } - override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { - if !self.didSetup3dTouch && self.traitCollection.forceTouchCapability != .unknown { - self.didSetup3dTouch = true - //self.registerForPreviewingNonNative(with: self, sourceView: self.view, theme: PeekControllerTheme(presentationTheme: self.presentationData.theme)) - } - } - } - override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -798,7 +802,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, if hasPasscode { let _ = ApplicationSpecificNotice.setPasscodeLockTips(accountManager: strongSelf.context.sharedContext.accountManager).start() - let tooltipController = TooltipController(content: .text(strongSelf.presentationData.strings.DialogList_PasscodeLockHelp), dismissByTapOutside: true) + let tooltipController = TooltipController(content: .text(strongSelf.presentationData.strings.DialogList_PasscodeLockHelp), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, dismissByTapOutside: true) strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceViewAndRect: { [weak self] in if let strongSelf = self { return (strongSelf.titleView, lockViewFrame.offsetBy(dx: 4.0, dy: 14.0)) @@ -884,13 +888,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, return true }) } - - if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { - if !self.didSetup3dTouch { - self.didSetup3dTouch = true - //self.registerForPreviewingNonNative(with: self, sourceView: self.view, theme: PeekControllerTheme(presentationTheme: self.presentationData.theme)) - } - } } override public func viewWillDisappear(_ animated: Bool) { @@ -975,7 +972,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, public func activateSearch() { if self.displayNavigationBar { - let _ = (self.chatListDisplayNode.chatListNode.ready + let _ = (self.chatListDisplayNode.chatListNode.contentsReady |> take(1) |> deliverOnMainQueue).start(completed: { [weak self] in guard let strongSelf = self else { @@ -1073,7 +1070,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, var sourceRect = selectedNode.view.superview!.convert(selectedNode.frame, to: sourceView) sourceRect.size.height -= UIScreenPixel switch item.content { - case let .peer(_, peer, _, _, _, _, _, _, _, _, _): + case let .peer(_, peer, _, _, _, _, _, _, _, _, _, _): if peer.peerId.namespace != Namespaces.Peer.SecretChat { let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(peer.peerId), subject: nil, botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) @@ -1199,7 +1196,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, self?.donePressed() }) } else if case .right = action, !peerIds.isEmpty { - let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteConfirmation(Int32(peerIds.count)), color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() @@ -1218,11 +1215,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, let text = strongSelf.presentationData.strings.ChatList_DeletedChats(Int32(peerIds.count)) - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: text), elevatedLayout: false, animateInAsReplacement: true, action: { shouldCommit in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: text), elevatedLayout: false, animateInAsReplacement: true, action: { value in guard let strongSelf = self else { - return + return false } - if shouldCommit { + if value == .commit { let context = strongSelf.context let presentationData = strongSelf.presentationData let progressSignal = Signal { subscriber in @@ -1250,7 +1247,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } let _ = (signal |> deliverOnMainQueue).start() - } else { + return true + } else if value == .undo { strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerIds.first!) strongSelf.chatListDisplayNode.chatListNode.updateState({ state in var state = state @@ -1260,7 +1258,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, return state }) self?.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerIds.first!) + return true } + return false }), in: .current) strongSelf.donePressed() @@ -1269,7 +1269,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, actionSheet.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) @@ -1330,11 +1330,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, }) if value { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .hidArchive(title: strongSelf.presentationData.strings.ChatList_UndoArchiveHiddenTitle, text: strongSelf.presentationData.strings.ChatList_UndoArchiveHiddenText, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] shouldCommit in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .hidArchive(title: strongSelf.presentationData.strings.ChatList_UndoArchiveHiddenTitle, text: strongSelf.presentationData.strings.ChatList_UndoArchiveHiddenText, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in guard let strongSelf = self else { - return + return false } - if !shouldCommit { + if value == .undo { let _ = (strongSelf.context.account.postbox.transaction { transaction -> Bool in var updatedValue = false updateChatArchiveSettings(transaction: transaction, { settings in @@ -1345,10 +1345,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, }) return updatedValue }).start() + return true } + return false }), in: .current) } else { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .revealedArchive(title: strongSelf.presentationData.strings.ChatList_UndoArchiveRevealedTitle, text: strongSelf.presentationData.strings.ChatList_UndoArchiveRevealedText, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .revealedArchive(title: strongSelf.presentationData.strings.ChatList_UndoArchiveRevealedTitle, text: strongSelf.presentationData.strings.ChatList_UndoArchiveRevealedText, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) } }) @@ -1385,7 +1387,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, if let user = chatPeer as? TelegramUser, user.botInfo == nil, canRemoveGlobally { strongSelf.maybeAskForPeerChatRemoval(peer: peer, completion: { _ in }, removed: {}) } else { - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] var canClear = true var canStop = false @@ -1442,11 +1444,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, return true }) - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: strongSelf.presentationData.strings.Undo_ChatCleared), elevatedLayout: false, animateInAsReplacement: true, action: { shouldCommit in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: strongSelf.presentationData.strings.Undo_ChatCleared), elevatedLayout: false, animateInAsReplacement: true, action: { value in guard let strongSelf = self else { - return + return false } - if shouldCommit { + if value == .commit { let _ = clearHistoryInteractively(postbox: strongSelf.context.account.postbox, peerId: peerId, type: type).start(completed: { guard let strongSelf = self else { return @@ -1457,18 +1459,21 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, return state }) }) - } else { + return true + } else if value == .undo { strongSelf.chatListDisplayNode.chatListNode.updateState({ state in var state = state state.pendingClearHistoryPeerIds.remove(peer.peerId) return state }) + return true } + return false }), in: .current) } if canRemoveGlobally { - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .clearHistory, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder)) @@ -1484,14 +1489,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, actionSheet.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) ]) strongSelf.present(actionSheet, in: .window(.root)) } else { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: { @@ -1529,7 +1534,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) @@ -1556,7 +1561,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } if canRemoveGlobally { - let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: mainPeer, chatPeer: chatPeer, action: .delete, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder)) @@ -1565,7 +1570,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, guard let strongSelf = self else { return } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText, actions: [ + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText, actions: [ TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { completion(false) }), @@ -1588,7 +1593,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, actionSheet.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() completion(false) }) @@ -1596,7 +1601,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, ]) self.present(actionSheet, in: .window(.root)) } else if peer.peerId == self.context.account.peerId { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: self.presentationData.theme), title: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ + self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { completion(false) }), @@ -1634,11 +1639,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil) - let action: (Bool) -> Void = { shouldCommit in + for peerId in peerIds { + deleteSendMessageIntents(peerId: peerId) + } + + let action: (UndoOverlayAction) -> Bool = { value in guard let strongSelf = self else { - return + return false } - if !shouldCommit { + if value == .undo { strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerIds[0]) let _ = (postbox.transaction { transaction -> Void in for peerId in peerIds { @@ -1651,6 +1660,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, } strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil) }) + return true + } else { + return false } } @@ -1737,11 +1749,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, return true }) - self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: statusText), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] shouldCommit in + self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: statusText), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in guard let strongSelf = self else { - return + return false } - if shouldCommit { + if value == .commit { strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerId) if let channel = chatPeer as? TelegramChannel { strongSelf.context.peerChannelMemberCategoriesContextsManager.externallyRemoved(peerId: channel.id, memberId: strongSelf.context.account.peerId) @@ -1755,18 +1767,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, state.pendingRemovalPeerIds.remove(peer.peerId) return state }) - self?.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil) + strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil) + + deleteSendMessageIntents(peerId: peerId) }) completion() - } else { + return true + } else if value == .undo { strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(peerId) strongSelf.chatListDisplayNode.chatListNode.updateState({ state in var state = state state.pendingRemovalPeerIds.remove(peer.peerId) return state }) - self?.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil) + strongSelf.chatListDisplayNode.chatListNode.setCurrentRemovingPeerId(nil) + return true } + return false }), in: .current) } @@ -1786,4 +1803,51 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, return nil } } + + /*public func presentTabBarPreviewingController(sourceNodes: [ASDisplayNode]) { + if self.isNodeLoaded { + let _ = (self.context.account.postbox.transaction { transaction -> [ChatListFilter] in + let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default + return settings.filters + } + |> deliverOnMainQueue).start(next: { [weak self] presetList in + guard let strongSelf = self else { + return + } + let controller = TabBarChatListFilterController(context: strongSelf.context, sourceNodes: sourceNodes, presetList: presetList, currentPreset: strongSelf.chatListDisplayNode.chatListNode.chatListFilter, setup: { + guard let strongSelf = self else { + return + } + strongSelf.push(chatListFilterPresetListController(context: strongSelf.context, updated: { presets in + guard let strongSelf = self else { + return + } + /*if let currentPreset = strongSelf.chatListDisplayNode.chatListNode.chatListFilter { + var found = false + if let index = presets.index(where: { $0.id == currentPreset.id }) { + found = true + if currentPreset != presets[index] { + strongSelf.chatListDisplayNode.chatListNode.chatListFilter = presets[index] + } + } + if !found { + strongSelf.chatListDisplayNode.chatListNode.chatListFilter = nil + } + }*/ + })) + }, updatePreset: { value in + guard let strongSelf = self else { + return + } + strongSelf.push(ChatListControllerImpl(context: strongSelf.context, groupId: .root, filter: value, controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: false, enableDebugActions: false)) + //strongSelf.chatListDisplayNode.chatListNode.chatListFilter = value + }) + strongSelf.context.sharedContext.mainWindow?.present(controller, on: .root) + }) + } + } + + public func updateTabBarPreviewingControllerPresentation(_ update: TabBarContainedControllerPresentationUpdate) { + + }*/ } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 24211ad5c8..9eb336e963 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -72,12 +72,12 @@ final class ChatListControllerNode: ASDisplayNode { let debugListView = ListView() - init(context: AccountContext, groupId: PeerGroupId, controlsHistoryPreload: Bool, presentationData: PresentationData, controller: ChatListControllerImpl) { + init(context: AccountContext, groupId: PeerGroupId, filter: ChatListFilter?, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, controller: ChatListControllerImpl) { self.context = context self.groupId = groupId self.presentationData = presentationData - self.chatListNode = ChatListNode(context: context, groupId: groupId, controlsHistoryPreload: controlsHistoryPreload, mode: .chatList, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations) + self.chatListNode = ChatListNode(context: context, groupId: groupId, chatListFilter: filter, previewing: previewing, controlsHistoryPreload: controlsHistoryPreload, mode: .chatList, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations) self.controller = controller @@ -151,7 +151,7 @@ final class ChatListControllerNode: ASDisplayNode { self.backgroundColor = self.presentationData.theme.chatList.backgroundColor - self.chatListNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations) + self.chatListNode.updateThemeAndStrings(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations) self.searchDisplayController?.updatePresentationData(presentationData) self.chatListEmptyNode?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) @@ -215,29 +215,8 @@ final class ChatListControllerNode: ASDisplayNode { self.chatListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.chatListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve) self.chatListNode.visualInsets = UIEdgeInsets(top: visualNavigationHeight, left: 0.0, bottom: 0.0, right: 0.0) self.chatListNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) @@ -265,6 +244,7 @@ final class ChatListControllerNode: ASDisplayNode { self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ChatListSearchContainerNode(context: self.context, filter: [], groupId: self.groupId, openPeer: { [weak self] peer, dismissSearch in self?.requestOpenPeerFromSearch?(peer, dismissSearch) + }, openDisabledPeer: { _ in }, openRecentPeerOptions: { [weak self] peer in self?.requestOpenRecentPeerOptions?(peer) }, openMessage: { [weak self] peer, messageId in diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift new file mode 100644 index 0000000000..321f7685c2 --- /dev/null +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -0,0 +1,408 @@ +/*import Foundation +import UIKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import ItemListUI +import AccountContext +import TelegramUIPreferences +import ItemListPeerItem +import ItemListPeerActionItem + +private final class ChatListFilterPresetControllerArguments { + let context: AccountContext + let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void + let openAddPeer: () -> Void + let deleteAdditionalPeer: (PeerId) -> Void + let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void + + init(context: AccountContext, updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void, openAddPeer: @escaping () -> Void, deleteAdditionalPeer: @escaping (PeerId) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void) { + self.context = context + self.updateState = updateState + self.openAddPeer = openAddPeer + self.deleteAdditionalPeer = deleteAdditionalPeer + self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions + } +} + +private enum ChatListFilterPresetControllerSection: Int32 { + case name + case categories + case excludeCategories + case additionalPeers +} + +private func filterEntry(presentationData: ItemListPresentationData, arguments: ChatListFilterPresetControllerArguments, title: String, value: Bool, filter: ChatListFilterPeerCategories, section: Int32) -> ItemListCheckboxItem { + return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: section, action: { + arguments.updateState { current in + var state = current + if state.includeCategories.contains(filter) { + state.includeCategories.remove(filter) + } else { + state.includeCategories.insert(filter) + } + return state + } + }) +} + +private enum ChatListFilterPresetEntryStableId: Hashable { + case index(Int) + case peer(PeerId) + case additionalPeerInfo +} + +private enum ChatListFilterPresetEntry: ItemListNodeEntry { + case nameHeader(String) + case name(placeholder: String, value: String) + case filterPrivateChats(title: String, value: Bool) + case filterSecretChats(title: String, value: Bool) + case filterPrivateGroups(title: String, value: Bool) + case filterBots(title: String, value: Bool) + case filterPublicGroups(title: String, value: Bool) + case filterChannels(title: String, value: Bool) + case filterMuted(title: String, value: Bool) + case filterRead(title: String, value: Bool) + case additionalPeersHeader(String) + case addAdditionalPeer(title: String) + case additionalPeer(index: Int, peer: RenderedPeer, isRevealed: Bool) + case additionalPeerInfo(String) + + var section: ItemListSectionId { + switch self { + case .nameHeader, .name: + return ChatListFilterPresetControllerSection.name.rawValue + case .filterPrivateChats, .filterSecretChats, .filterPrivateGroups, .filterBots, .filterPublicGroups, .filterChannels: + return ChatListFilterPresetControllerSection.categories.rawValue + case .filterMuted, .filterRead: + return ChatListFilterPresetControllerSection.excludeCategories.rawValue + case .additionalPeersHeader, .addAdditionalPeer, .additionalPeer, .additionalPeerInfo: + return ChatListFilterPresetControllerSection.additionalPeers.rawValue + } + } + + var stableId: ChatListFilterPresetEntryStableId { + switch self { + case .nameHeader: + return .index(0) + case .name: + return .index(1) + case .filterPrivateChats: + return .index(2) + case .filterSecretChats: + return .index(3) + case .filterPrivateGroups: + return .index(4) + case .filterBots: + return .index(5) + case .filterPublicGroups: + return .index(6) + case .filterChannels: + return .index(7) + case .filterMuted: + return .index(8) + case .filterRead: + return .index(9) + case .additionalPeersHeader: + return .index(10) + case .addAdditionalPeer: + return .index(11) + case let .additionalPeer(additionalPeer): + return .peer(additionalPeer.peer.peerId) + case .additionalPeerInfo: + return .additionalPeerInfo + } + } + + static func <(lhs: ChatListFilterPresetEntry, rhs: ChatListFilterPresetEntry) -> Bool { + switch lhs.stableId { + case let .index(lhsIndex): + switch rhs.stableId { + case let .index(rhsIndex): + return lhsIndex < rhsIndex + case .peer: + return true + case .additionalPeerInfo: + return true + } + case .peer: + switch lhs { + case let .additionalPeer(lhsIndex, _, _): + switch rhs.stableId { + case .index: + return false + case .additionalPeerInfo: + return true + case .peer: + switch rhs { + case let .additionalPeer(rhsIndex, _, _): + return lhsIndex < rhsIndex + default: + preconditionFailure() + } + } + default: + preconditionFailure() + } + case .additionalPeerInfo: + switch rhs.stableId { + case .index: + return false + case .peer: + return false + case .additionalPeerInfo: + return false + } + } + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! ChatListFilterPresetControllerArguments + switch self { + case let .nameHeader(title): + return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) + case let .name(placeholder, value): + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, type: .regular(capitalization: true, autocorrection: false), sectionId: self.section, textUpdated: { value in + arguments.updateState { current in + var state = current + state.name = value + return state + } + }, action: {}) + case let .filterPrivateChats(title, value): + return filterEntry(presentationData: presentationData, arguments: arguments, title: title, value: value, filter: .privateChats, section: self.section) + case let .filterSecretChats(title, value): + return filterEntry(presentationData: presentationData, arguments: arguments, title: title, value: value, filter: .secretChats, section: self.section) + case let .filterPrivateGroups(title, value): + return filterEntry(presentationData: presentationData, arguments: arguments, title: title, value: value, filter: .privateGroups, section: self.section) + case let .filterBots(title, value): + return filterEntry(presentationData: presentationData, arguments: arguments, title: title, value: value, filter: .bots, section: self.section) + case let .filterPublicGroups(title, value): + return filterEntry(presentationData: presentationData, arguments: arguments, title: title, value: value, filter: .publicGroups, section: self.section) + case let .filterChannels(title, value): + return filterEntry(presentationData: presentationData, arguments: arguments, title: title, value: value, filter: .channels, section: self.section) + case let .filterMuted(title, value): + return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { _ in + arguments.updateState { current in + var state = current + state.excludeMuted = !state.excludeMuted + return state + } + }) + case let .filterRead(title, value): + return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { _ in + arguments.updateState { current in + var state = current + state.excludeRead = !state.excludeRead + return state + } + }) + case let .additionalPeersHeader(title): + return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) + case let .addAdditionalPeer(title): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addPersonIcon(presentationData.theme), title: title, alwaysPlain: false, sectionId: self.section, height: .peerList, editing: false, action: { + arguments.openAddPeer() + }) + case let .additionalPeer(title, peer, isRevealed): + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: "."), nameDisplayOrder: .firstLast, context: arguments.context, peer: peer.chatMainPeer!, height: .peerList, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: isRevealed), revealOptions: ItemListPeerItemRevealOptions(options: [ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Common_Delete, action: { + arguments.deleteAdditionalPeer(peer.peerId) + })]), switchValue: nil, enabled: true, selectable: false, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { lhs, rhs in + arguments.setPeerIdWithRevealedOptions(lhs, rhs) + }, removePeer: { id in + arguments.deleteAdditionalPeer(id) + }) + case let .additionalPeerInfo(text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + } + } +} + +private struct ChatListFilterPresetControllerState: Equatable { + var name: String + var includeCategories: ChatListFilterPeerCategories + var excludeMuted: Bool + var excludeRead: Bool + var additionallyIncludePeers: [PeerId] + + var revealedPeerId: PeerId? + + var isComplete: Bool { + if self.name.isEmpty { + return false + } + if self.includeCategories.isEmpty && self.additionallyIncludePeers.isEmpty && !self.excludeMuted && !self.excludeRead { + return false + } + return true + } +} + +private func chatListFilterPresetControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetControllerState, peers: [RenderedPeer]) -> [ChatListFilterPresetEntry] { + var entries: [ChatListFilterPresetEntry] = [] + + entries.append(.nameHeader("NAME")) + entries.append(.name(placeholder: "Preset Name", value: state.name)) + + entries.append(.filterPrivateChats(title: "Private Chats", value: state.includeCategories.contains(.privateChats))) + entries.append(.filterSecretChats(title: "Secret Chats", value: state.includeCategories.contains(.secretChats))) + entries.append(.filterPrivateGroups(title: "Private Groups", value: state.includeCategories.contains(.privateGroups))) + entries.append(.filterBots(title: "Bots", value: state.includeCategories.contains(.bots))) + entries.append(.filterPublicGroups(title: "Public Groups", value: state.includeCategories.contains(.publicGroups))) + entries.append(.filterChannels(title: "Channels", value: state.includeCategories.contains(.channels))) + + entries.append(.filterMuted(title: "Exclude Muted", value: state.excludeMuted)) + entries.append(.filterRead(title: "Exclude Read", value: state.excludeRead)) + + entries.append(.additionalPeersHeader("ALWAYS INCLUDE")) + entries.append(.addAdditionalPeer(title: "Add")) + + for peer in peers { + entries.append(.additionalPeer(index: entries.count, peer: peer, isRevealed: state.revealedPeerId == peer.peerId)) + } + + entries.append(.additionalPeerInfo("These chats will always be included in the list.")) + + return entries +} + +func chatListFilterPresetController(context: AccountContext, currentPreset: ChatListFilter?, updated: @escaping ([ChatListFilter]) -> Void) -> ViewController { + let initialName: String + if let currentPreset = currentPreset { + initialName = currentPreset.title ?? "" + } else { + initialName = "New Preset" + } + let initialState = ChatListFilterPresetControllerState(name: initialName, includeCategories: currentPreset?.categories ?? .all, excludeMuted: currentPreset?.excludeMuted ?? false, excludeRead: currentPreset?.excludeRead ?? false, additionallyIncludePeers: currentPreset?.includePeers ?? []) + let stateValue = Atomic(value: initialState) + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + let actionsDisposable = DisposableSet() + + let addPeerDisposable = MetaDisposable() + actionsDisposable.add(addPeerDisposable) + + var presentControllerImpl: ((ViewController, Any?) -> Void)? + var dismissImpl: (() -> Void)? + + let arguments = ChatListFilterPresetControllerArguments( + context: context, + updateState: { f in + updateState(f) + }, + openAddPeer: { + let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .peerSelection(searchChatList: true, searchGroups: true), options: [])) + addPeerDisposable.set((controller.result + |> take(1) + |> deliverOnMainQueue).start(next: { [weak controller] peerIds in + controller?.dismiss() + updateState { state in + var state = state + for peerId in peerIds { + switch peerId { + case let .peer(id): + if !state.additionallyIncludePeers.contains(id) { + state.additionallyIncludePeers.append(id) + } + default: + break + } + } + return state + } + })) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, + deleteAdditionalPeer: { peerId in + updateState { state in + var state = state + if let index = state.additionallyIncludePeers.index(of: peerId) { + state.additionallyIncludePeers.remove(at: index) + } + return state + } + }, + setPeerIdWithRevealedOptions: { peerId, fromPeerId in + updateState { state in + var state = state + if (peerId == nil && fromPeerId == state.revealedPeerId) || (peerId != nil && fromPeerId == nil) { + state.revealedPeerId = peerId + } + return state + } + } + ) + + let statePeers = statePromise.get() + |> map { state -> [PeerId] in + return state.additionallyIncludePeers + } + |> distinctUntilChanged + |> mapToSignal { peerIds -> Signal<[RenderedPeer], NoError> in + return context.account.postbox.transaction { transaction -> [RenderedPeer] in + var result: [RenderedPeer] = [] + for peerId in peerIds { + if let peer = transaction.getPeer(peerId) { + result.append(RenderedPeer(peer: peer)) + } + } + return result + } + } + + let signal = combineLatest(queue: .mainQueue(), + context.sharedContext.presentationData, + statePromise.get(), + statePeers + ) + |> deliverOnMainQueue + |> map { presentationData, state, statePeers -> (ItemListControllerState, (ItemListNodeState, Any)) in + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + dismissImpl?() + }) + let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: state.isComplete, action: { + let state = stateValue.with { $0 } + let preset = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, includePeers: state.additionallyIncludePeers) + let _ = (updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { settings in + var preset = preset + if currentPreset == nil { + preset.id = max(2, settings.filters.map({ $0.id }).max() ?? 2) + } + var settings = settings + settings.filters = settings.filters.filter { $0 != preset && $0 != currentPreset } + settings.filters.append(preset) + return settings + }) + |> deliverOnMainQueue).start(next: { settings in + updated(settings.filters) + dismissImpl?() + }) + }) + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.SocksProxySetup_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetControllerEntries(presentationData: presentationData, state: state, peers: statePeers), style: .blocks, emptyStateItem: nil, animateChanges: true) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(context: context, state: signal) + controller.navigationPresentation = .modal + presentControllerImpl = { [weak controller] c, d in + controller?.present(c, in: .window(.root), with: d) + } + dismissImpl = { [weak controller] in + let _ = controller?.dismiss() + } + + return controller +} + +*/ diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift new file mode 100644 index 0000000000..3ca80247d1 --- /dev/null +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -0,0 +1,209 @@ +/*import Foundation +import UIKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import AccountContext + +private final class ChatListFilterPresetListControllerArguments { + let context: AccountContext + + let openPreset: (ChatListFilter) -> Void + let addNew: () -> Void + let setItemWithRevealedOptions: (Int32?, Int32?) -> Void + let removePreset: (Int32) -> Void + + init(context: AccountContext, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void) { + self.context = context + self.openPreset = openPreset + self.addNew = addNew + self.setItemWithRevealedOptions = setItemWithRevealedOptions + self.removePreset = removePreset + } +} + +private enum ChatListFilterPresetListSection: Int32 { + case list +} + +private func stringForUserCount(_ peers: [PeerId: SelectivePrivacyPeer], strings: PresentationStrings) -> String { + if peers.isEmpty { + return strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder + } else { + var result = 0 + for (_, peer) in peers { + result += peer.userCount + } + return strings.UserCount(Int32(result)) + } +} + +private enum ChatListFilterPresetListEntryStableId: Hashable { + case listHeader + case preset(Int32) + case addItem + case listFooter +} + +private enum ChatListFilterPresetListEntry: ItemListNodeEntry { + case listHeader(String) + case preset(index: Int, title: String?, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool) + case addItem(String) + case listFooter(String) + + var section: ItemListSectionId { + switch self { + case .listHeader, .preset, .addItem, .listFooter: + return ChatListFilterPresetListSection.list.rawValue + } + } + + var sortId: Int { + switch self { + case .listHeader: + return 0 + case let .preset(preset): + return 1 + preset.index + case .addItem: + return 1000 + case .listFooter: + return 1001 + } + } + + var stableId: ChatListFilterPresetListEntryStableId { + switch self { + case .listHeader: + return .listHeader + case let .preset(preset): + return .preset(preset.preset.id) + case .addItem: + return .addItem + case .listFooter: + return .listFooter + } + } + + static func <(lhs: ChatListFilterPresetListEntry, rhs: ChatListFilterPresetListEntry) -> Bool { + return lhs.sortId < rhs.sortId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! ChatListFilterPresetListControllerArguments + switch self { + case let .listHeader(text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section) + case let .preset(index, title, preset, canBeReordered, canBeDeleted): + return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title ?? "", editing: ChatListFilterPresetListItemEditing(editable: true, editing: false, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, sectionId: self.section, action: { + arguments.openPreset(preset) + }, setItemWithRevealedOptions: { lhs, rhs in + arguments.setItemWithRevealedOptions(lhs, rhs) + }, remove: { + arguments.removePreset(preset.id) + }) + case let .addItem(text): + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.addNew() + }) + case let .listFooter(text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + } + } +} + +private struct ChatListFilterPresetListControllerState: Equatable { + var revealedPreset: Int32? = nil +} + +private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, settings: ChatListFiltersState) -> [ChatListFilterPresetListEntry] { + var entries: [ChatListFilterPresetListEntry] = [] + + entries.append(.listHeader("FILTERS")) + for preset in settings.filters { + entries.append(.preset(index: entries.count, title: preset.title, preset: preset, canBeReordered: settings.filters.count > 1, canBeDeleted: true)) + } + entries.append(.addItem("Add New")) + entries.append(.listFooter("Add custom presets")) + + return entries +} + +func chatListFilterPresetListController(context: AccountContext, updated: @escaping ([ChatListFilter]) -> Void) -> ViewController { + let initialState = ChatListFilterPresetListControllerState() + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((ChatListFilterPresetListControllerState) -> ChatListFilterPresetListControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var dismissImpl: (() -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController, Any?) -> Void)? + + let arguments = ChatListFilterPresetListControllerArguments(context: context, openPreset: { preset in + pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: preset, updated: updated)) + }, addNew: { + pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: nil, updated: updated)) + }, setItemWithRevealedOptions: { preset, fromPreset in + updateState { state in + var state = state + if (preset == nil && fromPreset == state.revealedPreset) || (preset != nil && fromPreset == nil) { + state.revealedPreset = preset + } + return state + } + }, removePreset: { id in + let _ = (updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { settings in + var settings = settings + if let index = settings.filters.index(where: { $0.id == id }) { + settings.filters.remove(at: index) + } + return settings + }) + |> deliverOnMainQueue).start(next: { settings in + updated(settings.filters) + }) + }) + + let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFilters]) + + let signal = combineLatest(queue: .mainQueue(), + context.sharedContext.presentationData, + statePromise.get(), + preferences + ) + |> map { presentationData, state, preferences -> (ItemListControllerState, (ItemListNodeState, Any)) in + let settings = preferences.values[PreferencesKeys.chatListFilters] as? ChatListFiltersState ?? ChatListFiltersState.default + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Close), style: .regular, enabled: true, action: { + let _ = replaceRemoteChatListFilters(account: context.account).start() + dismissImpl?() + }) + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Filter Presets"), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, settings: settings), style: .blocks, animateChanges: true) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + } + + let controller = ItemListController(context: context, state: signal) + controller.navigationPresentation = .modal + pushControllerImpl = { [weak controller] c in + controller?.push(c) + } + presentControllerImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + dismissImpl = { [weak controller] in + controller?.dismiss() + } + + return controller +} +*/ diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift new file mode 100644 index 0000000000..adad852881 --- /dev/null +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift @@ -0,0 +1,437 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import ItemListUI +import TelegramUIPreferences + +struct ChatListFilterPresetListItemEditing: Equatable { + let editable: Bool + let editing: Bool + let revealed: Bool +} + +final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { + let presentationData: ItemListPresentationData + let preset: ChatListFilter + let title: String + let editing: ChatListFilterPresetListItemEditing + let canBeReordered: Bool + let canBeDeleted: Bool + let sectionId: ItemListSectionId + let action: () -> Void + let setItemWithRevealedOptions: (Int32?, Int32?) -> Void + let remove: () -> Void + + init( + presentationData: ItemListPresentationData, + preset: ChatListFilter, + title: String, + editing: ChatListFilterPresetListItemEditing, + canBeReordered: Bool, + canBeDeleted: Bool, + sectionId: ItemListSectionId, + action: @escaping () -> Void, + setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, + remove: @escaping () -> Void + ) { + self.presentationData = presentationData + self.preset = preset + self.title = title + self.editing = editing + self.canBeReordered = canBeReordered + self.canBeDeleted = canBeDeleted + self.sectionId = sectionId + self.action = action + self.setItemWithRevealedOptions = setItemWithRevealedOptions + self.remove = remove + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = ChatListFilterPresetListItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply(false) }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? ChatListFilterPresetListItemNode { + let makeLayout = nodeValue.asyncLayout() + + var animated = true + if case .None = animation { + animated = false + } + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply(animated) + }) + } + } + } + } + } + + var selectable: Bool = true + + func selected(listView: ListView){ + listView.clearHighlightAnimated(true) + self.action() + } +} + +private let titleFont = Font.regular(17.0) + +private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let maskNode: ASImageNode + + private let titleNode: TextNode + + private let activateArea: AccessibilityAreaNode + + private var editableControlNode: ItemListEditableControlNode? + private var reorderControlNode: ItemListEditableReorderControlNode? + + private var item: ChatListFilterPresetListItem? + private var layoutParams: ListViewItemLayoutParams? + + override var canBeSelected: Bool { + if self.editableControlNode != nil { + return false + } + return true + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.activateArea = AccessibilityAreaNode() + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.titleNode) + self.addSubnode(self.activateArea) + + self.activateArea.activate = { [weak self] in + self?.item?.action() + return true + } + } + + func asyncLayout() -> (_ item: ChatListFilterPresetListItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) + let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) + + let currentItem = self.item + + return { item, params, neighbors in + var updatedTheme: PresentationTheme? + + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + let peerRevealOptions: [ItemListRevealOption] + if item.editing.editable && item.canBeDeleted { + peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] + } else { + peerRevealOptions = [] + } + + let titleAttributedString = NSMutableAttributedString() + titleAttributedString.append(NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)) + + var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)? + var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)? + + let editingOffset: CGFloat = 0.0 + var reorderInset: CGFloat = 0.0 + + if item.editing.editing && item.canBeReordered { + /*let sizeAndApply = editableControlLayout(item.presentationData.theme, false) + editableControlSizeAndApply = sizeAndApply + editingOffset = sizeAndApply.0*/ + + let reorderSizeAndApply = reorderControlLayout(item.presentationData.theme) + reorderControlSizeAndApply = reorderSizeAndApply + reorderInset = reorderSizeAndApply.0 + } + + let leftInset: CGFloat = 16.0 + params.leftInset + let rightInset: CGFloat = params.rightInset + max(reorderInset, 55.0) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let insets = itemListNeighborsGroupedInsets(neighbors) + let contentSize = CGSize(width: params.width, height: titleLayout.size.height + 11.0 * 2.0) + let separatorHeight = UIScreenPixel + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] animated in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + strongSelf.activateArea.accessibilityLabel = "\(titleAttributedString.string))" + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + } + + let revealOffset = strongSelf.revealOffset + + let transition: ContainedViewLayoutTransition + if animated { + transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + + if let editableControlSizeAndApply = editableControlSizeAndApply { + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height)) + if strongSelf.editableControlNode == nil { + let editableControlNode = editableControlSizeAndApply.1(layout.contentSize.height) + editableControlNode.tapped = { + if let strongSelf = self { + strongSelf.setRevealOptionsOpened(true, animated: true) + strongSelf.revealOptionsInteractivelyOpened() + } + } + strongSelf.editableControlNode = editableControlNode + strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.titleNode) + editableControlNode.frame = editableControlFrame + transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY)) + editableControlNode.alpha = 0.0 + transition.updateAlpha(node: editableControlNode, alpha: 1.0) + } else { + strongSelf.editableControlNode?.frame = editableControlFrame + } + strongSelf.editableControlNode?.isHidden = !item.editing.editable + } else if let editableControlNode = strongSelf.editableControlNode { + var editableControlFrame = editableControlNode.frame + editableControlFrame.origin.x = -editableControlFrame.size.width + strongSelf.editableControlNode = nil + transition.updateAlpha(node: editableControlNode, alpha: 0.0) + transition.updateFrame(node: editableControlNode, frame: editableControlFrame, completion: { [weak editableControlNode] _ in + editableControlNode?.removeFromSupernode() + }) + } + + if let reorderControlSizeAndApply = reorderControlSizeAndApply { + if strongSelf.reorderControlNode == nil { + let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate) + strongSelf.reorderControlNode = reorderControlNode + strongSelf.addSubnode(reorderControlNode) + reorderControlNode.alpha = 0.0 + transition.updateAlpha(node: reorderControlNode, alpha: 1.0) + } + let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0, y: 0.0), size: CGSize(width: reorderControlSizeAndApply.0, height: layout.contentSize.height)) + strongSelf.reorderControlNode?.frame = reorderControlFrame + } else if let reorderControlNode = strongSelf.reorderControlNode { + strongSelf.reorderControlNode = nil + transition.updateAlpha(node: reorderControlNode, alpha: 0.0, completion: { [weak reorderControlNode] _ in + reorderControlNode?.removeFromSupernode() + }) + } + + let _ = titleApply() + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + editingOffset + bottomStripeOffset = -separatorHeight + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) + transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) + + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 11.0), size: titleLayout.size)) + + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 0.0), size: CGSize(width: params.width - params.rightInset - 56.0 - (leftInset + revealOffset + editingOffset), height: layout.contentSize.height)) + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)) + + strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + + strongSelf.setRevealOptions((left: [], right: peerRevealOptions)) + strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) + } + }) + } + } + + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + super.updateRevealOffset(offset: offset, transition: transition) + + guard let params = self.layoutParams else { + return + } + + let leftInset: CGFloat = 16.0 + params.leftInset + + let editingOffset: CGFloat + if let editableControlNode = self.editableControlNode { + editingOffset = editableControlNode.bounds.size.width + var editableControlFrame = editableControlNode.frame + editableControlFrame.origin.x = params.leftInset + offset + transition.updateFrame(node: editableControlNode, frame: editableControlFrame) + } else { + editingOffset = 0.0 + } + + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + offset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size)) + } + + override func revealOptionsInteractivelyOpened() { + if let item = self.item { + item.setItemWithRevealedOptions(item.preset.id, nil) + } + } + + override func revealOptionsInteractivelyClosed() { + if let item = self.item { + item.setItemWithRevealedOptions(nil, item.preset.id) + } + } + + override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { + self.setRevealOptionsOpened(false, animated: true) + self.revealOptionsInteractivelyClosed() + + if let item = self.item { + item.remove() + } + } + + override func isReorderable(at point: CGPoint) -> Bool { + if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point), !self.isDisplayingRevealedOptions { + return true + } + return false + } +} diff --git a/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift b/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift index b0d339b63c..a6a88dc501 100644 --- a/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift @@ -9,21 +9,22 @@ import SyncCore import TelegramPresentationData import ChatListSearchRecentPeersNode import ContextUI +import AccountContext class ChatListRecentPeersListItem: ListViewItem { let theme: PresentationTheme let strings: PresentationStrings - let account: Account + let context: AccountContext let peers: [Peer] let peerSelected: (Peer) -> Void let peerContextAction: (Peer, ASDisplayNode, ContextGesture?) -> Void let header: ListViewItemHeader? - init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peers: [Peer], peerSelected: @escaping (Peer) -> Void, peerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, context: AccountContext, peers: [Peer], peerSelected: @escaping (Peer) -> Void, peerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) { self.theme = theme self.strings = strings - self.account = account + self.context = context self.peers = peers self.peerSelected = peerSelected self.peerContextAction = peerContextAction @@ -115,7 +116,7 @@ class ChatListRecentPeersListItemNode: ListViewItemNode { peersNode = currentPeersNode peersNode.updateThemeAndStrings(theme: item.theme, strings: item.strings) } else { - peersNode = ChatListSearchRecentPeersNode(account: item.account, theme: item.theme, mode: .list, strings: item.strings, peerSelected: { peer in + peersNode = ChatListSearchRecentPeersNode(context: item.context, theme: item.theme, mode: .list, strings: item.strings, peerSelected: { peer in self?.item?.peerSelected(peer) }, peerContextAction: { peer, node, gesture in self?.item?.peerContextAction(peer, node, gesture) diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 0208ceef80..0826e73dfe 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -16,6 +16,7 @@ import ChatListSearchItemHeader import ContactListUI import ContextUI import PhoneNumberFormat +import ItemListUI private enum ChatListRecentEntryStableId: Hashable { case topPeers @@ -80,10 +81,10 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } } - func item(context: AccountContext, filter: ChatListNodePeersFilter, peerSelected: @escaping (Peer) -> Void, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, deletePeer: @escaping (PeerId) -> Void) -> ListViewItem { + func item(context: AccountContext, presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, peerSelected: @escaping (Peer) -> Void, disaledPeerSelected: @escaping (Peer) -> Void, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, deletePeer: @escaping (PeerId) -> Void) -> ListViewItem { switch self { case let .topPeers(peers, theme, strings): - return ChatListRecentPeersListItem(theme: theme, strings: strings, account: context.account, peers: peers, peerSelected: { peer in + return ChatListRecentPeersListItem(theme: theme, strings: strings, context: context, peers: peers, peerSelected: { peer in peerSelected(peer) }, peerContextAction: { peer, node, gesture in if let peerContextAction = peerContextAction { @@ -132,6 +133,12 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } } + if filter.contains(.excludeChannels) { + if let channel = primaryPeer as? TelegramChannel, case .broadcast = channel.info { + enabled = false + } + } + let status: ContactsPeerItemStatus if let user = primaryPeer as? TelegramUser { let servicePeer = isServicePeer(primaryPeer) @@ -174,16 +181,22 @@ private enum ChatListRecentEntry: Comparable, Identifiable { badge = ContactsPeerItemBadge(count: peer.unreadCount, type: isMuted ? .inactive : .active) } - return ContactsPeerItem(theme: theme, strings: strings, sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, account: context.account, peerMode: .generalSearch, peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: status, badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: true, editing: false, revealed: hasRevealControls), index: nil, header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear, action: { + return ContactsPeerItem(presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: status, badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: true, editing: false, revealed: hasRevealControls), index: nil, header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear, action: { clearRecentlySearchedPeers() }), action: { _ in if let chatPeer = peer.peer.peers[peer.peer.peerId] { peerSelected(chatPeer) } + }, disabledAction: { _ in + if let chatPeer = peer.peer.peers[peer.peer.peerId] { + disaledPeerSelected(chatPeer) + } }, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer, contextAction: peerContextAction.flatMap { peerContextAction in return { node, gesture in - if let chatPeer = peer.peer.peers[peer.peer.peerId] { + if let chatPeer = peer.peer.peers[peer.peer.peerId], chatPeer.id.namespace != Namespaces.Peer.SecretChat { peerContextAction(chatPeer, .recentSearch, node, gesture) + } else { + gesture?.cancel() } } }) @@ -227,17 +240,23 @@ public enum ChatListSearchEntryStableId: Hashable { } } +public enum ChatListSearchSectionExpandType { + case none + case expand + case collapse +} + public enum ChatListSearchEntry: Comparable, Identifiable { - case localPeer(Peer, Peer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder) - case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder) + case localPeer(Peer, Peer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType) + case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType) case message(Message, RenderedPeer, CombinedPeerReadState?, ChatListPresentationData) case addContact(String, PresentationTheme, PresentationStrings) public var stableId: ChatListSearchEntryStableId { switch self { - case let .localPeer(peer, _, _, _, _, _, _, _): + case let .localPeer(peer, _, _, _, _, _, _, _, _): return .localPeerId(peer.id) - case let .globalPeer(peer, _, _, _, _, _, _): + case let .globalPeer(peer, _, _, _, _, _, _, _): return .globalPeerId(peer.peer.id) case let .message(message, _, _, _): return .messageId(message.id) @@ -248,14 +267,14 @@ public enum ChatListSearchEntry: Comparable, Identifiable { public static func ==(lhs: ChatListSearchEntry, rhs: ChatListSearchEntry) -> Bool { switch lhs { - case let .localPeer(lhsPeer, lhsAssociatedPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder): - if case let .localPeer(rhsPeer, rhsAssociatedPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder) = rhs, lhsPeer.isEqual(rhsPeer) && arePeersEqual(lhsAssociatedPeer, rhsAssociatedPeer) && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 { + case let .localPeer(lhsPeer, lhsAssociatedPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType): + if case let .localPeer(rhsPeer, rhsAssociatedPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType) = rhs, lhsPeer.isEqual(rhsPeer) && arePeersEqual(lhsAssociatedPeer, rhsAssociatedPeer) && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType { return true } else { return false } - case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder): - if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 { + case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType): + if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType { return true } else { return false @@ -301,17 +320,17 @@ public enum ChatListSearchEntry: Comparable, Identifiable { public static func <(lhs: ChatListSearchEntry, rhs: ChatListSearchEntry) -> Bool { switch lhs { - case let .localPeer(_, _, _, lhsIndex, _, _, _, _): - if case let .localPeer(_, _, _, rhsIndex, _, _, _, _) = rhs { + case let .localPeer(_, _, _, lhsIndex, _, _, _, _, _): + if case let .localPeer(_, _, _, rhsIndex, _, _, _, _, _) = rhs { return lhsIndex <= rhsIndex } else { return true } - case let .globalPeer(_, _, lhsIndex, _, _, _, _): + case let .globalPeer(_, _, lhsIndex, _, _, _, _, _): switch rhs { case .localPeer: return false - case let .globalPeer(_, _, rhsIndex, _, _, _, _): + case let .globalPeer(_, _, rhsIndex, _, _, _, _, _): return lhsIndex <= rhsIndex case .message, .addContact: return true @@ -329,9 +348,9 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } } - public func item(context: AccountContext, enableHeaders: Bool, filter: ChatListNodePeersFilter, interaction: ChatListNodeInteraction, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem { + public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, interaction: ChatListNodeInteraction, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void) -> ListViewItem { switch self { - case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder): + case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType): let primaryPeer: Peer var chatPeer: Peer? if let associatedPeer = associatedPeer { @@ -376,25 +395,36 @@ public enum ChatListSearchEntry: Comparable, Identifiable { badge = ContactsPeerItemBadge(count: unreadBadge.0, type: unreadBadge.1 ? .inactive : .active) } - let header:ChatListSearchItemHeader? + let header: ChatListSearchItemHeader? if filter.contains(.removeSearchHeader) { header = nil } else { - header = ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings, actionTitle: nil, action: nil) + let actionTitle: String? + switch expandType { + case .none: + actionTitle = nil + case .expand: + actionTitle = strings.ChatList_Search_ShowMore + case .collapse: + actionTitle = strings.ChatList_Search_ShowLess + } + header = ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings, actionTitle: actionTitle, action: actionTitle == nil ? nil : { + toggleExpandLocalResults() + }) } - return ContactsPeerItem(theme: theme, strings: strings, sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, account: context.account, peerMode: .generalSearch, peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: .none, badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: .none, badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in interaction.peerSelected(peer) }, contextAction: peerContextAction.flatMap { peerContextAction in return { node, gesture in - if let chatPeer = chatPeer { + if let chatPeer = chatPeer, chatPeer.id.namespace != Namespaces.Peer.SecretChat { peerContextAction(chatPeer, .search, node, gesture) } else { gesture?.cancel() } } }) - case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder): + case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType): var enabled = true if filter.contains(.onlyWriteable) { enabled = canSendMessagesToPeer(peer.peer) @@ -428,14 +458,25 @@ public enum ChatListSearchEntry: Comparable, Identifiable { badge = ContactsPeerItemBadge(count: unreadBadge.0, type: unreadBadge.1 ? .inactive : .active) } - let header:ChatListSearchItemHeader? + let header: ChatListSearchItemHeader? if filter.contains(.removeSearchHeader) { header = nil } else { - header = ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: nil, action: nil) + let actionTitle: String? + switch expandType { + case .none: + actionTitle = nil + case .expand: + actionTitle = strings.ChatList_Search_ShowMore + case .collapse: + actionTitle = strings.ChatList_Search_ShowLess + } + header = ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: actionTitle, action: actionTitle == nil ? nil : { + toggleExpandGlobalResults() + }) } - return ContactsPeerItem(theme: theme, strings: strings, sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, account: context.account, peerMode: .generalSearch, peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .addressName(suffixString), badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .addressName(suffixString), badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in interaction.peerSelected(peer.peer) }, contextAction: peerContextAction.flatMap { peerContextAction in return { node, gesture in @@ -443,7 +484,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } }) case let .message(message, peer, readState, presentationData): - return ChatListItem(presentationData: presentationData, context: context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: message.index), content: .peer(message: message, peer: peer, combinedReadState: readState, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: true, displayAsMessage: false), editing: false, hasActiveRevealControls: false, selected: false, header: enableHeaders ? ChatListSearchItemHeader(type: .messages, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) + return ChatListItem(presentationData: presentationData, context: context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: message.index), content: .peer(message: message, peer: peer, combinedReadState: readState, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: true, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: enableHeaders ? ChatListSearchItemHeader(type: .messages, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) case let .addContact(phoneNumber, theme, strings): return ContactsAddItem(theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { interaction.addContact(phoneNumber) @@ -472,22 +513,22 @@ public struct ChatListSearchContainerTransition { } } -private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ChatListRecentEntry], to toEntries: [ChatListRecentEntry], context: AccountContext, filter: ChatListNodePeersFilter, peerSelected: @escaping (Peer) -> Void, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, deletePeer: @escaping (PeerId) -> Void) -> ChatListSearchContainerRecentTransition { +private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [ChatListRecentEntry], to toEntries: [ChatListRecentEntry], context: AccountContext, presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, peerSelected: @escaping (Peer) -> Void, disaledPeerSelected: @escaping (Peer) -> Void, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, deletePeer: @escaping (PeerId) -> Void) -> ChatListSearchContainerRecentTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, filter: filter, peerSelected: peerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, filter: filter, peerSelected: peerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, peerSelected: peerSelected, disaledPeerSelected: disaledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, peerSelected: peerSelected, disaledPeerSelected: disaledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer), directionHint: nil) } return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates) } -public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, context: AccountContext, enableHeaders: Bool, filter: ChatListNodePeersFilter, interaction: ChatListNodeInteraction, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?) -> ChatListSearchContainerTransition { +public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, interaction: ChatListNodeInteraction, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void) -> ChatListSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, enableHeaders: enableHeaders, filter: filter, interaction: interaction, peerContextAction: peerContextAction), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, enableHeaders: enableHeaders, filter: filter, interaction: interaction, peerContextAction: peerContextAction), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, interaction: interaction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, interaction: interaction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults), directionHint: nil) } return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults) } @@ -511,6 +552,11 @@ private struct ChatListSearchContainerNodeState: Equatable { } } +private struct ChatListSearchContainerNodeSearchState: Equatable { + var expandLocalSearch: Bool = false + var expandGlobalSearch: Bool = false +} + private func doesPeerMatchFilter(peer: Peer, filter: ChatListNodePeersFilter) -> Bool { var enabled = true if filter.contains(.onlyWriteable), !canSendMessagesToPeer(peer) { @@ -571,6 +617,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private let presentationDataPromise: Promise private var stateValue = ChatListSearchContainerNodeState() private let statePromise: ValuePromise + private var searchStateValue = ChatListSearchContainerNodeSearchState() + private let searchStatePromise: ValuePromise private let _isSearching = ValuePromise(false, ignoreRepeated: true) override public var isSearching: Signal { @@ -579,13 +627,29 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private let filter: ChatListNodePeersFilter - public init(context: AccountContext, filter: ChatListNodePeersFilter, groupId: PeerGroupId, openPeer: @escaping (Peer, Bool) -> Void, openRecentPeerOptions: @escaping (Peer) -> Void, openMessage: @escaping (Peer, MessageId) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?) { + public init(context: AccountContext, filter: ChatListNodePeersFilter, groupId: PeerGroupId, openPeer originalOpenPeer: @escaping (Peer, Bool) -> Void, openDisabledPeer: @escaping (Peer) -> Void, openRecentPeerOptions: @escaping (Peer) -> Void, openMessage originalOpenMessage: @escaping (Peer, MessageId) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?) { self.context = context self.filter = filter self.dimNode = ASDisplayNode() + let openPeer: (Peer, Bool) -> Void = { peer, value in + originalOpenPeer(peer, value) + + if peer.id.namespace != Namespaces.Peer.SecretChat { + addAppLogEvent(postbox: context.account.postbox, time: Date().timeIntervalSince1970, type: "search_global_open_peer", peerId: peer.id, data: .dictionary([:])) + } + } + + let openMessage: (Peer, MessageId) -> Void = { peer, messageId in + originalOpenMessage(peer, messageId) + + if peer.id.namespace != Namespaces.Peer.SecretChat { + addAppLogEvent(postbox: context.account.postbox, time: Date().timeIntervalSince1970, type: "search_global_open_message", peerId: peer.id, data: .dictionary(["msg_id": .number(Double(messageId.id))])) + } + } + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.presentationDataPromise = Promise(ChatListPresentationData(theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations)) + self.presentationDataPromise = Promise(ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations)) self.recentListNode = ListView() self.recentListNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor @@ -593,6 +657,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor self.statePromise = ValuePromise(self.stateValue, ignoreRepeated: true) + self.searchStatePromise = ValuePromise(self.searchStateValue, ignoreRepeated: true) super.init() @@ -645,6 +710,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo let currentRemotePeers = Atomic<([FoundPeer], [FoundPeer])?>(value: nil) let presentationDataPromise = self.presentationDataPromise + let searchStatePromise = self.searchStatePromise let foundItems = self.searchQuery.get() |> mapToSignal { query -> Signal<([ChatListSearchEntry], Bool)?, NoError> in guard let query = query, !query.isEmpty else { @@ -661,7 +727,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo return (views, local) } } - |> mapToSignal{ viewsAndPeers -> Signal<(peers: [RenderedPeer], unread: [PeerId: (Int32, Bool)]), NoError> in + |> mapToSignal { viewsAndPeers -> Signal<(peers: [RenderedPeer], unread: [PeerId: (Int32, Bool)]), NoError> in return context.account.postbox.unreadMessageCountsView(items: viewsAndPeers.0.map {.peer($0.peerId)}) |> map { values in var unread: [PeerId: (Int32, Bool)] = [:] for peerView in viewsAndPeers.0 { @@ -704,6 +770,10 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo if filter.contains(.doNotSearchMessages) { foundRemoteMessages = .single((([], [:], 0), false)) } else { + if !query.isEmpty { + addAppLogEvent(postbox: context.account.postbox, time: Date().timeIntervalSince1970, type: "search_global_query", peerId: nil, data: .dictionary([:])) + } + let searchSignal = searchMessages(account: context.account, location: location, query: query, state: nil, limit: 50) |> map { result, updatedState -> ChatListSearchMessagesResult in return ChatListSearchMessagesResult(query: query, messages: result.messages.sorted(by: { $0.index > $1.index }), readStates: result.readStates, hasMore: !result.completed, state: updatedState) @@ -746,8 +816,18 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo ) } - return combineLatest(accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationDataPromise.get()) - |> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationData -> ([ChatListSearchEntry], Bool)? in + let resolvedMessage = .single(nil) + |> then(context.sharedContext.resolveUrl(account: context.account, url: query) + |> mapToSignal { resolvedUrl -> Signal in + if case let .channelMessage(peerId, messageId) = resolvedUrl { + return downloadMessage(postbox: context.account.postbox, network: context.account.network, messageId: messageId) + } else { + return .single(nil) + } + }) + + return combineLatest(accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationDataPromise.get(), searchStatePromise.get(), resolvedMessage) + |> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationData, searchState, resolvedMessage -> ([ChatListSearchEntry], Bool)? in var entries: [ChatListSearchEntry] = [] let isSearching = foundRemotePeers.2 || foundRemoteMessages.1 var index = 0 @@ -779,21 +859,65 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } } + if filter.contains(.excludeChannels) { + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + return false + } + } + return true } var existingPeerIds = Set() + var totalNumberOfLocalPeers = 0 + for renderedPeer in foundLocalPeers.peers { + if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != context.account.peerId, filteredPeer(peer, accountPeer) { + if !existingPeerIds.contains(peer.id) { + existingPeerIds.insert(peer.id) + totalNumberOfLocalPeers += 1 + } + } + } + for peer in foundRemotePeers.0 { + if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, accountPeer) { + existingPeerIds.insert(peer.peer.id) + totalNumberOfLocalPeers += 1 + } + } + + var totalNumberOfGlobalPeers = 0 + for peer in foundRemotePeers.1 { + if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, accountPeer) { + totalNumberOfGlobalPeers += 1 + } + } + + existingPeerIds.removeAll() + + let localExpandType: ChatListSearchSectionExpandType = .none + let globalExpandType: ChatListSearchSectionExpandType + if totalNumberOfGlobalPeers > 3 { + globalExpandType = searchState.expandGlobalSearch ? .collapse : .expand + } else { + globalExpandType = .none + } + let lowercasedQuery = query.lowercased() if presentationData.strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery) { if !existingPeerIds.contains(accountPeer.id), filteredPeer(accountPeer, accountPeer) { existingPeerIds.insert(accountPeer.id) - entries.append(.localPeer(accountPeer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder)) + entries.append(.localPeer(accountPeer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) index += 1 } } + var numberOfLocalPeers = 0 for renderedPeer in foundLocalPeers.peers { + if case .expand = localExpandType, numberOfLocalPeers >= 5 { + break + } + if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != context.account.peerId, filteredPeer(peer, accountPeer) { if !existingPeerIds.contains(peer.id) { existingPeerIds.insert(peer.id) @@ -801,29 +925,52 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo if let associatedPeerId = peer.associatedPeerId { associatedPeer = renderedPeer.peers[associatedPeerId] } - entries.append(.localPeer(peer, associatedPeer, foundLocalPeers.unread[peer.id], index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder)) + entries.append(.localPeer(peer, associatedPeer, foundLocalPeers.unread[peer.id], index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) index += 1 + numberOfLocalPeers += 1 } } } for peer in foundRemotePeers.0 { + if case .expand = localExpandType, numberOfLocalPeers >= 5 { + break + } + if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, accountPeer) { existingPeerIds.insert(peer.peer.id) - entries.append(.localPeer(peer.peer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder)) + entries.append(.localPeer(peer.peer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) index += 1 + numberOfLocalPeers += 1 } } + var numberOfGlobalPeers = 0 index = 0 for peer in foundRemotePeers.1 { + if case .expand = globalExpandType, numberOfGlobalPeers >= 3 { + break + } + if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, accountPeer) { existingPeerIds.insert(peer.peer.id) - entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder)) + entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType)) index += 1 + numberOfGlobalPeers += 1 } } + if let message = resolvedMessage { + var peer = RenderedPeer(message: message) + if let group = message.peers[message.id.peerId] as? TelegramGroup, let migrationReference = group.migrationReference { + if let channelPeer = message.peers[migrationReference.peerId] { + peer = RenderedPeer(peer: channelPeer) + } + } + entries.append(.message(message, peer, nil, presentationData)) + index += 1 + } + if !foundRemotePeers.2 { index = 0 for message in foundRemoteMessages.0.0 { @@ -854,6 +1001,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo openPeer(peer, false) let _ = addRecentlySearchedPeer(postbox: context.account.postbox, peerId: peer.id).start() self?.listNode.clearHighlightAnimated(true) + }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, messageSelected: { [weak self] peer, message, _ in self?.view.endEditing(true) @@ -970,16 +1118,21 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.updatedRecentPeersDisposable.set(managedUpdatedRecentPeers(accountPeerId: context.account.peerId, postbox: context.account.postbox, network: context.account.network).start()) - self.recentDisposable.set((recentItems - |> deliverOnMainQueue).start(next: { [weak self] entries in + self.recentDisposable.set((combineLatest(queue: .mainQueue(), + presentationDataPromise.get(), + recentItems + ) + |> deliverOnMainQueue).start(next: { [weak self] presentationData, entries in if let strongSelf = self { let previousEntries = previousRecentItems.swap(entries) let firstTime = previousEntries == nil - let transition = chatListSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, context: context, filter: filter, peerSelected: { peer in + let transition = chatListSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, context: context, presentationData: presentationData, filter: filter, peerSelected: { peer in openPeer(peer, true) let _ = addRecentlySearchedPeer(postbox: context.account.postbox, peerId: peer.id).start() self?.recentListNode.clearHighlightAnimated(true) + }, disaledPeerSelected: { peer in + openDisabledPeer(peer) }, peerContextAction: peerContextAction, clearRecentlySearchedPeers: { self?.clearRecentSearch() @@ -1002,7 +1155,26 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo let previousEntries = previousSearchItems.swap(entriesAndFlags?.0) let firstTime = previousEntries == nil - let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entriesAndFlags?.0 ?? [], displayingResults: entriesAndFlags?.0 != nil, context: context, enableHeaders: true, filter: filter, interaction: interaction, peerContextAction: peerContextAction) + let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entriesAndFlags?.0 ?? [], displayingResults: entriesAndFlags?.0 != nil, context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: filter, interaction: interaction, peerContextAction: peerContextAction, + toggleExpandLocalResults: { + guard let strongSelf = self else { + return + } + strongSelf.updateSearchState { state in + var state = state + state.expandLocalSearch = !state.expandLocalSearch + return state + } + }, toggleExpandGlobalResults: { + guard let strongSelf = self else { + return + } + strongSelf.updateSearchState { state in + var state = state + state.expandGlobalSearch = !state.expandGlobalSearch + return state + } + }) strongSelf.enqueueTransition(transition, firstTime: firstTime) } })) @@ -1012,7 +1184,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo if let strongSelf = self { let previousTheme = strongSelf.presentationData.theme strongSelf.presentationData = presentationData - strongSelf.presentationDataPromise.set(.single(ChatListPresentationData(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations))) + strongSelf.presentationDataPromise.set(.single(ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations))) if previousTheme !== presentationData.theme { strongSelf.updateTheme(theme: presentationData.theme) @@ -1053,6 +1225,17 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.dimNode.backgroundColor = self.filter.contains(.excludeRecent) ? UIColor.black.withAlphaComponent(0.5) : theme.chatList.backgroundColor self.recentListNode.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor self.listNode.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor + + self.listNode.forEachItemHeaderNode({ itemHeaderNode in + if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode { + itemHeaderNode.updateTheme(theme: theme) + } + }) + self.recentListNode.forEachItemHeaderNode({ itemHeaderNode in + if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode { + itemHeaderNode.updateTheme(theme: theme) + } + }) } private func updateState(_ f: (ChatListSearchContainerNodeState) -> ChatListSearchContainerNodeState) { @@ -1063,6 +1246,14 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } } + private func updateSearchState(_ f: (ChatListSearchContainerNodeSearchState) -> ChatListSearchContainerNodeSearchState) { + let state = f(self.searchStateValue) + if state != self.searchStateValue { + self.searchStateValue = state + self.searchStatePromise.set(state) + } + } + override public func searchTextUpdated(text: String) { let searchQuery: String? = !text.isEmpty ? text : nil self.interaction?.searchTextHighightState = searchQuery @@ -1135,33 +1326,13 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo let topInset = navigationBarHeight transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) self.recentListNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if !hadValidLayout { while !self.enqueuedRecentTransitions.isEmpty { @@ -1209,7 +1380,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo bounds = selectedItemNode.bounds } switch item.content { - case let .peer(message, peer, _, _, _, _, _, _, _, _, _): + case let .peer(message, peer, _, _, _, _, _, _, _, _, _, _): return (selectedItemNode.view, bounds, message?.id ?? peer.peerId) case let .groupReference(groupId, _, _, _, _): return (selectedItemNode.view, bounds, groupId) diff --git a/submodules/ChatListUI/Sources/Node/ChatListBadgeNode.swift b/submodules/ChatListUI/Sources/Node/ChatListBadgeNode.swift index 92d837b2d9..8e1ff49f1f 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListBadgeNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListBadgeNode.swift @@ -33,8 +33,6 @@ private func measureString(_ string: String) -> String { } } -private let badgeFont = Font.regular(14.0) - final class ChatListBadgeNode: ASDisplayNode { private let backgroundNode: ASImageNode private let textNode: TextNode @@ -63,13 +61,13 @@ final class ChatListBadgeNode: ASDisplayNode { self.addSubnode(self.textNode) } - func asyncLayout() -> (CGSize, UIImage?, ChatListBadgeContent) -> (CGSize, (Bool, Bool) -> Void) { + func asyncLayout() -> (CGSize, CGFloat, UIFont, UIImage?, ChatListBadgeContent) -> (CGSize, (Bool, Bool) -> Void) { let textLayout = TextNode.asyncLayout(self.textNode) let measureTextLayout = TextNode.asyncLayout(self.measureTextNode) let currentContent = self.content - return { [weak self] boundingSize, backgroundImage, content in + return { [weak self] boundingSize, imageWidth, badgeFont, backgroundImage, content in var badgeWidth: CGFloat = 0.0 var textLayoutAndApply: (TextNodeLayout, () -> TextNode)? @@ -79,14 +77,14 @@ final class ChatListBadgeNode: ASDisplayNode { let (measureLayout, _) = measureTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: measureString(text.string), font: badgeFont, textColor: .black), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: boundingSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - badgeWidth = max(20.0, measureLayout.size.width + 10.0) + badgeWidth = max(imageWidth, measureLayout.size.width + imageWidth / 2.0) case .mention, .blank: - badgeWidth = 20.0 + badgeWidth = imageWidth case .none: badgeWidth = 0.0 } - return (CGSize(width: badgeWidth, height: 20.0), { animated, bounce in + return (CGSize(width: badgeWidth, height: imageWidth), { animated, bounce in if let strongSelf = self { strongSelf.content = content @@ -98,7 +96,7 @@ final class ChatListBadgeNode: ASDisplayNode { return } - let badgeWidth = max(20.0, badgeWidth) + let badgeWidth = max(imageWidth, badgeWidth) let previousBadgeWidth = !strongSelf.backgroundNode.frame.width.isZero ? strongSelf.backgroundNode.frame.width : badgeWidth var animateTextNode = false diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index f678a36ab7..1b4ef38ca5 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -20,12 +20,12 @@ import ChatListSearchItemNode import ContextUI public enum ChatListItemContent { - case peer(message: Message?, peer: RenderedPeer, combinedReadState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, presence: PeerPresence?, summaryInfo: ChatListMessageTagSummaryInfo, embeddedState: PeerChatListEmbeddedInterfaceState?, inputActivities: [(Peer, PeerInputActivity)]?, isAd: Bool, ignoreUnreadBadge: Bool, displayAsMessage: Bool) + case peer(message: Message?, peer: RenderedPeer, combinedReadState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, presence: PeerPresence?, summaryInfo: ChatListMessageTagSummaryInfo, embeddedState: PeerChatListEmbeddedInterfaceState?, inputActivities: [(Peer, PeerInputActivity)]?, isAd: Bool, ignoreUnreadBadge: Bool, displayAsMessage: Bool, hasFailedMessages: Bool) case groupReference(groupId: PeerGroupId, peers: [ChatListGroupReferencePeer], message: Message?, unreadState: PeerGroupUnreadCountersCombinedSummary, hiddenByDefault: Bool) public var chatLocation: ChatLocation? { switch self { - case let .peer(_, peer, _, _, _, _, _, _, _, _, _): + case let .peer(_, peer, _, _, _, _, _, _, _, _, _, _): return .peer(peer.peerId) case .groupReference: return nil @@ -38,7 +38,7 @@ public class ChatListItem: ListViewItem, ChatListSearchItemNeighbour { let context: AccountContext let peerGroupId: PeerGroupId let index: ChatListIndex - let content: ChatListItemContent + public let content: ChatListItemContent let editing: Bool let hasActiveRevealControls: Bool let selected: Bool @@ -122,7 +122,7 @@ public class ChatListItem: ListViewItem, ChatListSearchItemNeighbour { public func selected(listView: ListView) { switch self.content { - case let .peer(message, peer, _, _, _, _, _, _, isAd, _, _): + case let .peer(message, peer, _, _, _, _, _, _, isAd, _, _, _): if let message = message, let peer = peer.peer { self.interaction.messageSelected(peer, message, isAd) } else if let peer = peer.peer { @@ -163,11 +163,6 @@ public class ChatListItem: ListViewItem, ChatListSearchItemNeighbour { } } -private let titleFont = Font.medium(16.0) -private let textFont = Font.regular(15.0) -private let dateFont = Font.regular(14.0) -private let badgeFont = Font.regular(14.0) - private let pinIcon = ItemListRevealOptionIcon.animation(animation: "anim_pin", scale: 0.33333, offset: 0.0, keysToColor: nil, flip: false) private let unpinIcon = ItemListRevealOptionIcon.animation(animation: "anim_unpin", scale: 0.33333, offset: 0.0, keysToColor: ["un Outlines.Group 1.Stroke 1"], flip: false) private let muteIcon = ItemListRevealOptionIcon.animation(animation: "anim_mute", scale: 0.33333, offset: 0.0, keysToColor: ["un Outlines.Group 1.Stroke 1"], flip: false) @@ -197,8 +192,6 @@ private enum RevealOptionKey: Int32 { case unhide } -private let itemHeight: CGFloat = 76.0 - private func canArchivePeer(id: PeerId, accountPeerId: PeerId) -> Bool { if id.namespace == Namespaces.Peer.CloudUser && id.id == 777000 { return false @@ -320,6 +313,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let avatarNode: AvatarNode let titleNode: TextNode let authorNode: TextNode + let measureNode: TextNode + private var currentItemHeight: CGFloat? let textNode: TextNode let contentImageNode: TransformImageNode let inputActivitiesNode: ChatListInputActivitiesNode @@ -438,6 +433,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.contextContainer = ContextControllerSourceNode() + self.measureNode = TextNode() + self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = true @@ -525,7 +522,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var displayAsMessage = false var enablePreview = true switch item.content { - case let .peer(message, peerValue, _, _, _, _, _, _, _, _, displayAsMessageValue): + case let .peer(message, peerValue, _, _, _, _, _, _, _, _, displayAsMessageValue, _): displayAsMessage = displayAsMessageValue if displayAsMessage, let author = message?.author as? TelegramUser { peer = author @@ -540,7 +537,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { UIView.transition(with: self.avatarNode.view, duration: 0.3, options: [.transitionCrossDissolve], animations: { }, completion: nil) } - self.avatarNode.setPeer(account: item.context.account, theme: item.presentationData.theme, peer: peer, overrideImage: .archivedChatsIcon(hiddenByDefault: groupReference.hiddenByDefault), emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads) + self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: .archivedChatsIcon(hiddenByDefault: groupReference.hiddenByDefault), emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads) } if let peer = peer { @@ -550,7 +547,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } else if peer.isDeleted { overrideImage = .deletedIcon } - self.avatarNode.setPeer(account: item.context.account, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads) + self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: 60.0, height: 60.0)) } self.contextContainer.isGestureEnabled = enablePreview @@ -640,6 +637,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let textLayout = TextNode.asyncLayout(self.textNode) let titleLayout = TextNode.asyncLayout(self.titleNode) let authorLayout = TextNode.asyncLayout(self.authorNode) + let makeMeasureLayout = TextNode.asyncLayout(self.measureNode) let inputActivitiesLayout = self.inputActivitiesNode.asyncLayout() let badgeLayout = self.badgeNode.asyncLayout() let mentionBadgeLayout = self.mentionBadgeNode.asyncLayout() @@ -653,6 +651,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let currentChatListSearchResult = self.cachedChatListSearchResult return { item, params, first, last, firstWithHeader, nextIsPinned in + let titleFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0)) + let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + let dateFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + let badgeFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + let account = item.context.account var message: Message? enum ContentPeer { @@ -670,11 +673,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let isPeerGroup: Bool let isAd: Bool let displayAsMessage: Bool + let hasFailedMessages: Bool var groupHiddenByDefault = false switch item.content { - case let .peer(messageValue, peerValue, combinedReadStateValue, notificationSettingsValue, peerPresenceValue, summaryInfoValue, embeddedStateValue, inputActivitiesValue, isAdValue, ignoreUnreadBadge, displayAsMessageValue): + case let .peer(messageValue, peerValue, combinedReadStateValue, notificationSettingsValue, peerPresenceValue, summaryInfoValue, embeddedStateValue, inputActivitiesValue, isAdValue, ignoreUnreadBadge, displayAsMessageValue, hasFailedMessagesValue): message = messageValue contentPeer = .chat(peerValue) combinedReadState = combinedReadStateValue @@ -695,6 +699,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { isPeerGroup = false isAd = isAdValue displayAsMessage = displayAsMessageValue + hasFailedMessages = messageValue?.flags.contains(.Failed) ?? false // hasFailedMessagesValue case let .groupReference(_, peers, messageValue, unreadState, hiddenByDefault): if let _ = messageValue, !peers.isEmpty { contentPeer = .chat(peers[0].peer) @@ -714,6 +719,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { peerPresence = nil isAd = false displayAsMessage = false + hasFailedMessages = false } if let messageValue = message { @@ -749,7 +755,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var currentSecretIconImage: UIImage? var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)? - var reorderControlSizeAndApply: (CGSize, (Bool) -> ItemListEditableReorderControlNode)? + var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)? let editingOffset: CGFloat var reorderInset: CGFloat = 0.0 @@ -761,9 +767,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { editingOffset = sizeAndApply.0 if item.index.pinningIndex != nil && !isAd && !isPeerGroup { - let sizeAndApply = reorderControlLayout(itemHeight, item.presentationData.theme) + let sizeAndApply = reorderControlLayout(item.presentationData.theme) reorderControlSizeAndApply = sizeAndApply - reorderInset = sizeAndApply.0.width + reorderInset = sizeAndApply.0 } } else { editingOffset = 0.0 @@ -771,7 +777,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let enableChatListPhotos = item.context.sharedContext.immediateExperimentalUISettings.chatListPhotos - let leftInset: CGFloat = params.leftInset + 78.0 + let avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0)) + let avatarLeftInset = 18.0 + avatarDiameter + + let badgeDiameter = floor(item.presentationData.fontSize.baseDisplaySize * 20.0 / 17.0) + + let leftInset: CGFloat = params.leftInset + avatarLeftInset enum ContentData { case chat(itemPeer: RenderedPeer, peer: Peer?, hideAuthor: Bool, messageText: String) @@ -969,7 +980,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if message.flags.isSending && !message.isSentOrAcknowledged { statusState = .clock(PresentationResourcesChatList.clockFrameImage(item.presentationData.theme), PresentationResourcesChatList.clockMinImage(item.presentationData.theme)) } else if message.id.peerId != account.peerId { - if message.flags.contains(.Failed) { + if hasFailedMessages { statusState = .failed(item.presentationData.theme.chatList.failedFillColor, item.presentationData.theme.chatList.failedForegroundColor) } else { if let combinedReadState = combinedReadState, combinedReadState.isOutgoingMessageIndexRead(message.index) { @@ -986,10 +997,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } else { let badgeTextColor: UIColor if unreadCount.muted { - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme) + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: badgeDiameter) badgeTextColor = theme.unreadBadgeInactiveTextColor } else { - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme) + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme, diameter: badgeDiameter) badgeTextColor = theme.unreadBadgeActiveTextColor } let unreadCountText = compactNumericCountString(Int(unreadCount.count), decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) @@ -1003,7 +1014,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let mutedCount = unreadCount.mutedCount, mutedCount > 0 { let mutedUnreadCountText = compactNumericCountString(Int(mutedCount), decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) - currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme) + currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: badgeDiameter) mentionBadgeContent = .text(NSAttributedString(string: mutedUnreadCountText, font: badgeFont, textColor: theme.unreadBadgeInactiveTextColor)) } } @@ -1015,13 +1026,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if !isPeerGroup { if totalMentionCount > 0 { if Namespaces.PeerGroup.archive == item.peerGroupId { - currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundInactiveMention(item.presentationData.theme) + currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundInactiveMention(item.presentationData.theme, diameter: badgeDiameter) } else { - currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundMention(item.presentationData.theme) + currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundMention(item.presentationData.theme, diameter: badgeDiameter) } mentionBadgeContent = .mention } else if item.index.pinningIndex != nil && !isAd && currentBadgeBackgroundImage == nil { - currentPinnedIconImage = PresentationResourcesChatList.badgeBackgroundPinned(item.presentationData.theme) + currentPinnedIconImage = PresentationResourcesChatList.badgeBackgroundPinned(item.presentationData.theme, diameter: badgeDiameter) } } @@ -1053,7 +1064,22 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { currentSecretIconImage = PresentationResourcesChatList.secretIcon(item.presentationData.theme) } var credibilityIconOffset: CGFloat = 0.0 - if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer { + if displayAsMessage { + switch item.content { + case let .peer(message, _, _, _, _, _, _, _, _, _, _, _): + if let peer = message?.author { + if peer.isScam { + currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, type: .regular) + credibilityIconOffset = 2.0 + } else if peer.isVerified { + currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme) + credibilityIconOffset = 3.0 + } + } + default: + break + } + } else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer { if peer.isScam { currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, type: .regular) credibilityIconOffset = 2.0 @@ -1076,13 +1102,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let layoutOffset: CGFloat = 0.0 - let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: layoutOffset + 8.0), size: CGSize(width: params.width - leftInset - params.rightInset - 10.0 - 1.0 - editingOffset, height: itemHeight - 12.0 - 9.0)) + let rawContentOriginX = 2.0 + let rawContentWidth = params.width - leftInset - params.rightInset - 10.0 - editingOffset - let (dateLayout, dateApply) = dateLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentRect.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (dateLayout, dateApply) = dateLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (badgeLayout, badgeApply) = badgeLayout(CGSize(width: rawContentRect.width, height: CGFloat.greatestFiniteMagnitude), currentBadgeBackgroundImage, badgeContent) + let (badgeLayout, badgeApply) = badgeLayout(CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), badgeDiameter, badgeFont, currentBadgeBackgroundImage, badgeContent) - let (mentionBadgeLayout, mentionBadgeApply) = mentionBadgeLayout(CGSize(width: rawContentRect.width, height: CGFloat.greatestFiniteMagnitude), currentMentionBadgeImage, mentionBadgeContent) + let (mentionBadgeLayout, mentionBadgeApply) = mentionBadgeLayout(CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), badgeDiameter, badgeFont, currentMentionBadgeImage, mentionBadgeContent) var badgeSize: CGFloat = 0.0 if !badgeLayout.width.isZero { @@ -1106,21 +1133,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } badgeSize = max(badgeSize, reorderInset) - let (authorLayout, authorApply) = authorLayout(TextNodeLayoutArguments(attributedString: hideAuthor ? nil : authorAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) + let (authorLayout, authorApply) = authorLayout(TextNodeLayoutArguments(attributedString: hideAuthor ? nil : authorAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth - badgeSize, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) var textCutout: TextNodeCutout? if !textLeftCutout.isZero { textCutout = TextNodeCutout(topLeft: CGSize(width: textLeftCutout, height: 4.0), topRight: nil, bottomRight: nil) } - let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: authorAttributedString == nil ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: textCutout, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) + let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: authorAttributedString == nil ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth - badgeSize, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: textCutout, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) - let titleRect = CGRect(origin: rawContentRect.origin, size: CGSize(width: rawContentRect.width - dateLayout.size.width - 10.0 - statusWidth - titleIconsWidth, height: rawContentRect.height)) - let (titleLayout, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRect.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let titleRectWidth = rawContentWidth - dateLayout.size.width - 10.0 - statusWidth - titleIconsWidth + let (titleLayout, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var inputActivitiesSize: CGSize? var inputActivitiesApply: (() -> Void)? if let inputActivities = inputActivities, !inputActivities.isEmpty { - let (size, apply) = inputActivitiesLayout(CGSize(width: rawContentRect.width - badgeSize, height: 40.0), item.presentationData.strings, item.presentationData.theme.chatList.messageTextColor, item.index.messageIndex.id.peerId, inputActivities) + let (size, apply) = inputActivitiesLayout(CGSize(width: rawContentWidth - badgeSize, height: 40.0), item.presentationData, item.presentationData.theme.chatList.messageTextColor, item.index.messageIndex.id.peerId, inputActivities) inputActivitiesSize = size inputActivitiesApply = apply } @@ -1131,7 +1158,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let peerRevealOptions: [ItemListRevealOption] let peerLeftRevealOptions: [ItemListRevealOption] switch item.content { - case let .peer(_, renderedPeer, _, _, presence, _ ,_ ,_, _, _, displayAsMessage): + case let .peer(_, renderedPeer, _, _, presence, _ ,_ ,_, _, _, displayAsMessage, _): if !displayAsMessage, let peer = renderedPeer.peer as? TelegramUser, let presence = presence as? TelegramUserPresence, !isServicePeer(peer) && !peer.flags.contains(.isSupport) && peer.id != item.context.account.peerId { let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: timestamp) if case .online = relativeStatus { @@ -1178,6 +1205,25 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { animateContent = true } + let measureString = NSAttributedString(string: "A", font: titleFont, textColor: .black) + let (measureLayout, measureApply) = makeMeasureLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let titleSpacing: CGFloat = -1.0 + let authorSpacing: CGFloat = -3.0 + var itemHeight: CGFloat = 8.0 * 2.0 + 1.0 + itemHeight += measureLayout.size.height * 3.0 + itemHeight += titleSpacing + itemHeight += authorSpacing + + /*if authorLayout.size.height.isZero { + itemHeight += textLayout.size.height + } else { + itemHeight += authorLayout.size.height + itemHeight += authorSpacing + textLayout.size.height + }*/ + + let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: layoutOffset + 8.0), size: CGSize(width: rawContentWidth, height: itemHeight - 12.0 - 9.0)) + let insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader) var heightOffset: CGFloat = 0.0 if item.hiddenOffset { @@ -1198,6 +1244,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { return (layout, { [weak self] synchronousLoads, animated in if let strongSelf = self { strongSelf.layoutParams = (item, first, last, firstWithHeader, nextIsPinned, params, countersSize) + strongSelf.currentItemHeight = itemHeight strongSelf.contentImageMedia = contentImageMedia strongSelf.cachedChatListText = chatListText strongSelf.cachedChatListSearchResult = chatListSearchResult @@ -1282,9 +1329,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var animateBadges = animateContent if let reorderControlSizeAndApply = reorderControlSizeAndApply { - let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0.width, y: layoutOffset), size: reorderControlSizeAndApply.0) + let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0, y: layoutOffset), size: CGSize(width: reorderControlSizeAndApply.0, height: layout.contentSize.height)) if strongSelf.reorderControlNode == nil { - let reorderControlNode = reorderControlSizeAndApply.1(false) + let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate) strongSelf.reorderControlNode = reorderControlNode strongSelf.addSubnode(reorderControlNode) reorderControlNode.frame = reorderControlFrame @@ -1297,7 +1344,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { transition.updateAlpha(node: strongSelf.pinnedIconNode, alpha: 0.0) transition.updateAlpha(node: strongSelf.statusNode, alpha: 0.0) } else if let reorderControlNode = strongSelf.reorderControlNode { - let _ = reorderControlSizeAndApply.1(false) + let _ = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate) transition.updateFrame(node: reorderControlNode, frame: reorderControlFrame) } } else if let reorderControlNode = strongSelf.reorderControlNode { @@ -1313,7 +1360,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { transition.updateAlpha(node: strongSelf.statusNode, alpha: 1.0) } - let avatarFrame = CGRect(origin: CGPoint(x: leftInset - 78.0 + editingOffset + 10.0 + revealOffset, y: layoutOffset + 7.0), size: CGSize(width: 60.0, height: 60.0)) + let avatarFrame = CGRect(origin: CGPoint(x: leftInset - avatarLeftInset + editingOffset + 10.0 + revealOffset, y: floor((itemHeight - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)) transition.updateFrame(node: strongSelf.avatarNode, frame: avatarFrame) let onlineFrame = CGRect(origin: CGPoint(x: avatarFrame.maxX - onlineLayout.width - 2.0, y: avatarFrame.maxY - onlineLayout.height - 2.0), size: onlineLayout) @@ -1328,7 +1375,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular) } strongSelf.onlineNode.setImage(onlineIcon) - + + let _ = measureApply() let _ = dateApply() let _ = textApply() let _ = authorApply() @@ -1343,6 +1391,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let statusSize = CGSize(width: 24.0, height: 24.0) strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width - statusSize.width, y: contentRect.origin.y + 2.0 - UIScreenPixel + floor((dateLayout.size.height - statusSize.height) / 2.0)), size: statusSize) + strongSelf.statusNode.fontSize = item.presentationData.fontSize.itemListBaseFontSize let _ = strongSelf.statusNode.transitionToState(statusState, animated: animateContent) if let _ = currentBadgeBackgroundImage { @@ -1391,7 +1440,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.secretIconNode = iconNode } iconNode.image = currentSecretIconImage - transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.origin.y + 4.0), size: currentSecretIconImage.size)) + transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.origin.y + floor((titleLayout.size.height - currentSecretIconImage.size.height) / 2.0)), size: currentSecretIconImage.size)) titleOffset += currentSecretIconImage.size.width + 3.0 } else if let secretIconNode = strongSelf.secretIconNode { strongSelf.secretIconNode = nil @@ -1590,7 +1639,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) - if let _ = self.item, let params = self.layoutParams?.5, let countersSize = self.layoutParams?.6 { + if let item = self.item, let params = self.layoutParams?.5, let countersSize = self.layoutParams?.6 { let editingOffset: CGFloat if let selectableControlNode = self.selectableControlNode { editingOffset = selectableControlNode.bounds.size.width @@ -1609,14 +1658,17 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: reorderControlNode, frame: reorderControlFrame) } - let leftInset: CGFloat = params.leftInset + 78.0 + let avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0)) + let avatarLeftInset = 18.0 + avatarDiameter - let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: layoutOffset + 8.0), size: CGSize(width: params.width - leftInset - params.rightInset - 10.0 - 1.0 - editingOffset, height: itemHeight - 12.0 - 9.0)) + let leftInset: CGFloat = params.leftInset + avatarLeftInset + + let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: layoutOffset + 8.0), size: CGSize(width: params.width - leftInset - params.rightInset - 10.0 - 1.0 - editingOffset, height: self.bounds.size.height - 12.0 - 9.0)) let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + offset, dy: 0.0) var avatarFrame = self.avatarNode.frame - avatarFrame.origin.x = leftInset - 78.0 + editingOffset + 10.0 + offset + avatarFrame.origin.x = leftInset - avatarLeftInset + editingOffset + 10.0 + offset transition.updateFrame(node: self.avatarNode, frame: avatarFrame) var onlineFrame = self.onlineNode.frame @@ -1806,7 +1858,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let item = self.item { if case .groupReference = item.content { - self.layer.sublayerTransform = CATransform3DMakeTranslation(0.0, currentValue - itemHeight, 0.0) + self.layer.sublayerTransform = CATransform3DMakeTranslation(0.0, currentValue - (self.currentItemHeight ?? 0.0), 0.0) } else { var separatorFrame = self.separatorNode.frame separatorFrame.origin.y = currentValue - UIScreenPixel @@ -1820,6 +1872,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.revealOptionSelected(ItemListRevealOption(key: action.key, title: "", icon: .none, color: .black, textColor: .white), animated: false) } } + + override func snapshotForReordering() -> UIView? { + self.backgroundNode.alpha = 0.9 + let result = self.view.snapshotContentTree() + self.backgroundNode.alpha = 1.0 + return result + } } private func foldLineBreaks(_ text: String) -> String { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index d927b769f1..73d9d2f377 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -40,7 +40,9 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: processed = true break inner case let .Audio(isVoice, _, title, performer, _): - if isVoice { + if !message.text.isEmpty { + messageText = "🎤 \(messageText)" + } else if isVoice { if message.text.isEmpty { messageText = strings.Message_Audio } else { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index f1bf72fee4..fdd814e57a 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -12,6 +12,7 @@ import AccountContext import TelegramNotices import ContactsPeerItem import ContextUI +import ItemListUI public enum ChatListNodeMode { case chatList @@ -45,6 +46,7 @@ final class ChatListHighlightedLocation { public final class ChatListNodeInteraction { let activateSearch: () -> Void let peerSelected: (Peer) -> Void + let disabledPeerSelected: (Peer) -> Void let togglePeerSelected: (PeerId) -> Void let messageSelected: (Peer, Message, Bool) -> Void let groupSelected: (PeerGroupId) -> Void @@ -61,9 +63,10 @@ public final class ChatListNodeInteraction { public var searchTextHighightState: String? var highlightedChatLocation: ChatListHighlightedLocation? - public init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer) -> Void, togglePeerSelected: @escaping (PeerId) -> Void, messageSelected: @escaping (Peer, Message, Bool) -> Void, groupSelected: @escaping (PeerGroupId) -> Void, addContact: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setItemPinned: @escaping (PinnedItemId, Bool) -> Void, setPeerMuted: @escaping (PeerId, Bool) -> Void, deletePeer: @escaping (PeerId) -> Void, updatePeerGrouping: @escaping (PeerId, Bool) -> Void, togglePeerMarkedUnread: @escaping (PeerId, Bool) -> Void, toggleArchivedFolderHiddenByDefault: @escaping () -> Void, activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?) -> Void) { + public init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer) -> Void, disabledPeerSelected: @escaping (Peer) -> Void, togglePeerSelected: @escaping (PeerId) -> Void, messageSelected: @escaping (Peer, Message, Bool) -> Void, groupSelected: @escaping (PeerGroupId) -> Void, addContact: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setItemPinned: @escaping (PinnedItemId, Bool) -> Void, setPeerMuted: @escaping (PeerId, Bool) -> Void, deletePeer: @escaping (PeerId) -> Void, updatePeerGrouping: @escaping (PeerId, Bool) -> Void, togglePeerMarkedUnread: @escaping (PeerId, Bool) -> Void, toggleArchivedFolderHiddenByDefault: @escaping () -> Void, activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?) -> Void) { self.activateSearch = activateSearch self.peerSelected = peerSelected + self.disabledPeerSelected = disabledPeerSelected self.togglePeerSelected = togglePeerSelected self.messageSelected = messageSelected self.groupSelected = groupSelected @@ -140,10 +143,10 @@ public struct ChatListNodeState: Equatable { private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, peerGroupId: PeerGroupId, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { - case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, presence, summaryInfo, editing, hasActiveRevealControls, selected, inputActivities, isAd): + case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, presence, summaryInfo, editing, hasActiveRevealControls, selected, inputActivities, isAd, hasFailedMessages): switch mode { case .chatList: - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, context: context, peerGroupId: peerGroupId, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, presence: presence, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities, isAd: isAd, ignoreUnreadBadge: false, displayAsMessage: false), editing: editing, hasActiveRevealControls: hasActiveRevealControls, selected: selected, header: nil, enableContextActions: true, hiddenOffset: false, interaction: nodeInteraction), directionHint: entry.directionHint) + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, context: context, peerGroupId: peerGroupId, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, presence: presence, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities, isAd: isAd, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: hasFailedMessages), editing: editing, hasActiveRevealControls: hasActiveRevealControls, selected: selected, header: nil, enableContextActions: true, hiddenOffset: false, interaction: nodeInteraction), directionHint: entry.directionHint) case let .peers(filter): let itemPeer = peer.chatMainPeer var chatPeer: Peer? @@ -201,11 +204,22 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL enabled = false } } + if filter.contains(.excludeChannels) { + if let peer = peer.peers[peer.peerId] { + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + enabled = false + } + } + } - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: presentationData.theme, strings: presentationData.strings, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, account: context.account, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) } + }, disabledAction: { _ in + if let chatPeer = chatPeer { + nodeInteraction.disabledPeerSelected(chatPeer) + } }), directionHint: entry.directionHint) } case let .HoleEntry(_, theme): @@ -221,10 +235,10 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, peerGroupId: PeerGroupId, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { - case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, presence, summaryInfo, editing, hasActiveRevealControls, selected, inputActivities, isAd): + case let .PeerEntry(index, presentationData, message, combinedReadState, notificationSettings, embeddedState, peer, presence, summaryInfo, editing, hasActiveRevealControls, selected, inputActivities, isAd, hasFailedMessages): switch mode { case .chatList: - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, context: context, peerGroupId: peerGroupId, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, presence: presence, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities, isAd: isAd, ignoreUnreadBadge: false, displayAsMessage: false), editing: editing, hasActiveRevealControls: hasActiveRevealControls, selected: selected, header: nil, enableContextActions: true, hiddenOffset: false, interaction: nodeInteraction), directionHint: entry.directionHint) + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, context: context, peerGroupId: peerGroupId, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, presence: presence, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities, isAd: isAd, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: hasFailedMessages), editing: editing, hasActiveRevealControls: hasActiveRevealControls, selected: selected, header: nil, enableContextActions: true, hiddenOffset: false, interaction: nodeInteraction), directionHint: entry.directionHint) case let .peers(filter): let itemPeer = peer.chatMainPeer var chatPeer: Peer? @@ -241,10 +255,19 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL enabled = false } } - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: presentationData.theme, strings: presentationData.strings, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, account: context.account, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in + if filter.contains(.excludeChannels) { + if let peer = peer.chatMainPeer as? TelegramChannel, case .broadcast = peer.info { + enabled = false + } + } + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) } + }, disabledAction: { _ in + if let chatPeer = chatPeer { + nodeInteraction.disabledPeerSelected(chatPeer) + } }), directionHint: entry.directionHint) } case let .HoleEntry(_, theme): @@ -310,7 +333,14 @@ public final class ChatListNode: ListView { return _ready.get() } + private let _contentsReady = ValuePromise() + private var didSetContentsReady = false + public var contentsReady: Signal { + return _contentsReady.get() + } + public var peerSelected: ((PeerId, Bool, Bool) -> Void)? + public var disabledPeerSelected: ((Peer) -> Void)? public var groupSelected: ((PeerGroupId) -> Void)? public var addContact: ((String) -> Void)? public var activateSearch: (() -> Void)? @@ -336,6 +366,11 @@ public final class ChatListNode: ListView { } private var currentLocation: ChatListNodeLocation? + let chatListFilter: ChatListFilter? + private let chatListFilterValue = Promise() + var chatListFilterSignal: Signal { + return self.chatListFilterValue.get() + } private let chatListLocation = ValuePromise() private let chatListDisposable = MetaDisposable() private var activityStatusesDisposable: Disposable? @@ -378,13 +413,15 @@ public final class ChatListNode: ListView { private var hapticFeedback: HapticFeedback? - public init(context: AccountContext, groupId: PeerGroupId, controlsHistoryPreload: Bool, mode: ChatListNodeMode, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool) { + public init(context: AccountContext, groupId: PeerGroupId, chatListFilter: ChatListFilter? = nil, previewing: Bool, controlsHistoryPreload: Bool, mode: ChatListNodeMode, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool) { self.context = context self.groupId = groupId + self.chatListFilter = chatListFilter + self.chatListFilterValue.set(.single(chatListFilter)) self.controlsHistoryPreload = controlsHistoryPreload self.mode = mode - self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: false, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), peerInputActivities: nil, pendingRemovalPeerIds: Set(), pendingClearHistoryPeerIds: Set(), archiveShouldBeTemporaryRevealed: false) + self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: false, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), peerInputActivities: nil, pendingRemovalPeerIds: Set(), pendingClearHistoryPeerIds: Set(), archiveShouldBeTemporaryRevealed: false) self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) self.theme = theme @@ -402,6 +439,10 @@ public final class ChatListNode: ListView { if let strongSelf = self, let peerSelected = strongSelf.peerSelected { peerSelected(peer.id, true, false) } + }, disabledPeerSelected: { [weak self] peer in + if let strongSelf = self, let disabledPeerSelected = strongSelf.disabledPeerSelected { + disabledPeerSelected(peer) + } }, togglePeerSelected: { [weak self] peerId in self?.updateState { state in var state = state @@ -490,8 +531,11 @@ public final class ChatListNode: ListView { let chatListViewUpdate = self.chatListLocation.get() |> distinctUntilChanged - |> mapToSignal { location in + |> mapToSignal { location -> Signal<(ChatListNodeViewUpdate, ChatListFilter?), NoError> in return chatListViewForLocation(groupId: groupId, location: location, account: context.account) + |> map { update in + return (update, location.filter) + } } let previousState = Atomic(value: self.currentState) @@ -541,14 +585,15 @@ public final class ChatListNode: ListView { let currentPeerId: PeerId = context.account.peerId let chatListNodeViewTransition = combineLatest(queue: viewProcessingQueue, hideArchivedFolderByDefault, displayArchiveIntro, savedMessagesPeer, chatListViewUpdate, self.statePromise.get()) - |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, savedMessagesPeer, update, state) -> Signal in + |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, savedMessagesPeer, updateAndFilter, state) -> Signal in + let (update, filter) = updateAndFilter let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault) let (rawEntries, isLoading) = chatListNodeEntriesForView(update.view, state: state, savedMessagesPeer: savedMessagesPeer, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, mode: mode) let entries = rawEntries.filter { entry in switch entry { - case let .PeerEntry(_, _, _, _, _, _, peer, _, _, _, _, _, _, _): + case let .PeerEntry(_, _, _, _, _, _, peer, _, _, _, _, _, _, _, _): switch mode { case .chatList: return true @@ -577,6 +622,11 @@ public final class ChatListNode: ListView { } } + if filter.contains(.excludeChannels) { + if let peer = peer.chatMainPeer as? TelegramChannel, case .broadcast = peer.info { + } + } + if filter.contains(.onlyWriteable) && filter.contains(.excludeDisabled) { if let peer = peer.peers[peer.peerId] { if !canSendMessagesToPeer(peer) { @@ -594,7 +644,7 @@ public final class ChatListNode: ListView { } } - let processedView = ChatListNodeView(originalView: update.view, filteredEntries: entries, isLoading: isLoading) + let processedView = ChatListNodeView(originalView: update.view, filteredEntries: entries, isLoading: isLoading, filter: filter) let previousView = previousView.swap(processedView) let previousState = previousState.swap(state) @@ -653,7 +703,7 @@ public final class ChatListNode: ListView { var didIncludeHiddenByDefaultArchive = false if let previous = previousView { for entry in previous.filteredEntries { - if case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _) = entry { + if case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = entry { if index.pinningIndex != nil { previousPinnedChats.append(index.messageIndex.id.peerId) } @@ -669,11 +719,11 @@ public final class ChatListNode: ListView { var doesIncludeArchive = false var doesIncludeHiddenByDefaultArchive = false for entry in processedView.filteredEntries { - if case let .PeerEntry(index, _, _, _, _, _, _, _, _ , _, _, _, _, _) = entry { - if index.pinningIndex != nil { - updatedPinnedChats.append(index.messageIndex.id.peerId) + if case let .PeerEntry(peerEntry) = entry { + if peerEntry.index.pinningIndex != nil { + updatedPinnedChats.append(peerEntry.index.messageIndex.id.peerId) } - if index.messageIndex.id.peerId == removingPeerId { + if peerEntry.index.messageIndex.id.peerId == removingPeerId { doesIncludeRemovingPeerId = true } } else if case let .GroupReferenceEntry(entry) = entry { @@ -707,7 +757,12 @@ public final class ChatListNode: ListView { searchMode = true } - return preparedChatListNodeViewTransition(from: previousView, to: processedView, reason: reason, disableAnimations: disableAnimations, account: context.account, scrollPosition: updatedScrollPosition, searchMode: searchMode) + if filter != previousView?.filter { + disableAnimations = true + updatedScrollPosition = nil + } + + return preparedChatListNodeViewTransition(from: previousView, to: processedView, reason: reason, previewing: previewing, disableAnimations: disableAnimations, account: context.account, scrollPosition: updatedScrollPosition, searchMode: searchMode) |> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, peerGroupId: groupId, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue) } @@ -725,9 +780,9 @@ public final class ChatListNode: ListView { if let range = range.loadedRange { var location: ChatListNodeLocation? if range.firstIndex < 5 && originalView.laterIndex != nil { - location = .navigation(index: originalView.entries[originalView.entries.count - 1].index) + location = .navigation(index: originalView.entries[originalView.entries.count - 1].index, filter: strongSelf.chatListFilter) } else if range.firstIndex >= 5 && range.lastIndex >= originalView.entries.count - 5 && originalView.earlierIndex != nil { - location = .navigation(index: originalView.entries[0].index) + location = .navigation(index: originalView.entries[0].index, filter: strongSelf.chatListFilter) } if let location = location, location != strongSelf.currentLocation { @@ -748,7 +803,7 @@ public final class ChatListNode: ListView { continue } switch chatListView.filteredEntries[entryCount - i - 1] { - case let .PeerEntry(_, _, _, readState, notificationSettings, _, _, _, _, _, _, _, _, _): + case let .PeerEntry(_, _, _, readState, notificationSettings, _, _, _, _, _, _, _, _, _, _): if let readState = readState { let count = readState.count rawUnreadCount += count @@ -783,10 +838,10 @@ public final class ChatListNode: ListView { let initialLocation: ChatListNodeLocation switch mode { - case .chatList: - initialLocation = .initial(count: 50) - case .peers: - initialLocation = .initial(count: 200) + case .chatList: + initialLocation = .initial(count: 50, filter: self.chatListFilter) + case .peers: + initialLocation = .initial(count: 200, filter: self.chatListFilter) } self.setChatListLocation(initialLocation) @@ -894,7 +949,7 @@ public final class ChatListNode: ListView { var referenceId: PinnedItemId? var beforeAll = false switch toEntry { - case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, isAd): + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, isAd, _): if isAd { beforeAll = true } else { @@ -912,7 +967,7 @@ public final class ChatListNode: ListView { var itemId: PinnedItemId? switch fromEntry { - case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _): itemId = .peer(index.messageIndex.id.peerId) /*case let .GroupReferenceEntry(_, _, groupId, _, _, _, _): itemId = .group(groupId)*/ @@ -1070,7 +1125,7 @@ public final class ChatListNode: ListView { self.activityStatusesDisposable?.dispose() } - public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool) { + public func updateThemeAndStrings(theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool) { if theme !== self.currentState.presentationData.theme || strings !== self.currentState.presentationData.strings || dateTimeFormat != self.currentState.presentationData.dateTimeFormat || disableAnimations != self.currentState.presentationData.disableAnimations { self.theme = theme if self.keepTopItemOverscrollBackground != nil { @@ -1080,7 +1135,7 @@ public final class ChatListNode: ListView { self.updateState { state in var state = state - state.presentationData = ChatListPresentationData(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations) + state.presentationData = ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations) return state } } @@ -1157,6 +1212,10 @@ public final class ChatListNode: ListView { strongSelf.didSetReady = true strongSelf._ready.set(true) } + if !strongSelf.didSetContentsReady { + strongSelf.didSetContentsReady = true + strongSelf._contentsReady.set(true) + } var isEmpty = false if transition.chatListView.filteredEntries.isEmpty { @@ -1261,13 +1320,12 @@ public final class ChatListNode: ListView { if view.laterIndex == nil { self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } else { - let location: ChatListNodeLocation = .scroll(index: .absoluteUpperBound, sourceIndex: .absoluteLowerBound - , scrollPosition: .top(0.0), animated: true) + let location: ChatListNodeLocation = .scroll(index: .absoluteUpperBound, sourceIndex: .absoluteLowerBound, scrollPosition: .top(0.0), animated: true, filter: self.chatListFilter) self.setChatListLocation(location) } } else { let location: ChatListNodeLocation = .scroll(index: .absoluteUpperBound, sourceIndex: .absoluteLowerBound - , scrollPosition: .top(0.0), animated: true) + , scrollPosition: .top(0.0), animated: true, filter: self.chatListFilter) self.setChatListLocation(location) } } @@ -1303,11 +1361,11 @@ public final class ChatListNode: ListView { if let index = index { let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: self?.currentlyVisibleLatestChatListIndex() ?? .absoluteUpperBound - , scrollPosition: .center(.top), animated: true) + , scrollPosition: .center(.top), animated: true, filter: strongSelf.chatListFilter) strongSelf.setChatListLocation(location) } else { let location: ChatListNodeLocation = .scroll(index: .absoluteUpperBound, sourceIndex: .absoluteLowerBound - , scrollPosition: .top(0.0), animated: true) + , scrollPosition: .top(0.0), animated: true, filter: strongSelf.chatListFilter) strongSelf.setChatListLocation(location) } }) @@ -1337,7 +1395,7 @@ public final class ChatListNode: ListView { continue } switch chatListView.filteredEntries[entryCount - i - 1] { - case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _): + case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _): if interaction.highlightedChatLocation?.location == ChatLocation.peer(peer.peerId) { current = (index, peer.peerId, entryCount - i - 1) break outer @@ -1363,17 +1421,17 @@ public final class ChatListNode: ListView { guard let strongSelf = self, let index = index else { return } - let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: strongSelf.currentlyVisibleLatestChatListIndex() ?? .absoluteUpperBound, scrollPosition: .center(.top), animated: true) + let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: strongSelf.currentlyVisibleLatestChatListIndex() ?? .absoluteUpperBound, scrollPosition: .center(.top), animated: true, filter: strongSelf.chatListFilter) strongSelf.setChatListLocation(location) strongSelf.peerSelected?(index.messageIndex.id.peerId, false, false) }) case .previous(unread: false), .next(unread: false): var target: (ChatListIndex, PeerId)? = nil if let current = current, entryCount > 1 { - if current.2 > 0, case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _) = chatListView.filteredEntries[current.2 - 1] { + if current.2 > 0, case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[current.2 - 1] { next = (index, peer.peerId) } - if current.2 <= entryCount - 2, case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _) = chatListView.filteredEntries[current.2 + 1] { + if current.2 <= entryCount - 2, case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[current.2 + 1] { previous = (index, peer.peerId) } if case .previous = option { @@ -1382,12 +1440,12 @@ public final class ChatListNode: ListView { target = next } } else if entryCount > 0 { - if case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _) = chatListView.filteredEntries[entryCount - 1] { + if case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[entryCount - 1] { target = (index, peer.peerId) } } if let target = target { - let location: ChatListNodeLocation = .scroll(index: target.0, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true) + let location: ChatListNodeLocation = .scroll(index: target.0, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true, filter: self.chatListFilter) self.setChatListLocation(location) self.peerSelected?(target.1, false, false) } @@ -1397,17 +1455,23 @@ public final class ChatListNode: ListView { guard index < 10 else { return } - let _ = (chatListViewForLocation(groupId: self.groupId, location: .initial(count: 10), account: self.context.account) + let _ = (self.chatListFilterValue.get() |> take(1) - |> deliverOnMainQueue).start(next: { update in - let entries = update.view.entries - if entries.count > index, case let .MessageEntry(index, _, _, _, _, renderedPeer, _, _) = entries[10 - index - 1] { - let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true) - self.setChatListLocation(location) - self.peerSelected?(renderedPeer.peerId, false, false) + |> deliverOnMainQueue).start(next: { [weak self] filter in + guard let self = self else { + return } + let _ = (chatListViewForLocation(groupId: self.groupId, location: .initial(count: 10, filter: filter), account: self.context.account) + |> take(1) + |> deliverOnMainQueue).start(next: { update in + let entries = update.view.entries + if entries.count > index, case let .MessageEntry(index, _, _, _, _, renderedPeer, _, _, _) = entries[10 - index - 1] { + let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true, filter: filter) + self.setChatListLocation(location) + self.peerSelected?(renderedPeer.peerId, false, false) + } + }) }) - break } } @@ -1444,7 +1508,7 @@ public final class ChatListNode: ListView { continue } switch chatListView.filteredEntries[entryCount - i - 1] { - case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _): return index default: break diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index b0da7f230d..7798787502 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -13,14 +13,14 @@ enum ChatListNodeEntryId: Hashable { } enum ChatListNodeEntry: Comparable, Identifiable { - case PeerEntry(index: ChatListIndex, presentationData: ChatListPresentationData, message: Message?, readState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedInterfaceState: PeerChatListEmbeddedInterfaceState?, peer: RenderedPeer, presence: PeerPresence?, summaryInfo: ChatListMessageTagSummaryInfo, editing: Bool, hasActiveRevealControls: Bool, selected: Bool, inputActivities: [(Peer, PeerInputActivity)]?, isAd: Bool) + case PeerEntry(index: ChatListIndex, presentationData: ChatListPresentationData, message: Message?, readState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedInterfaceState: PeerChatListEmbeddedInterfaceState?, peer: RenderedPeer, presence: PeerPresence?, summaryInfo: ChatListMessageTagSummaryInfo, editing: Bool, hasActiveRevealControls: Bool, selected: Bool, inputActivities: [(Peer, PeerInputActivity)]?, isAd: Bool, hasFailedMessages: Bool) case HoleEntry(ChatListHole, theme: PresentationTheme) case GroupReferenceEntry(index: ChatListIndex, presentationData: ChatListPresentationData, groupId: PeerGroupId, peers: [ChatListGroupReferencePeer], message: Message?, editing: Bool, unreadState: PeerGroupUnreadCountersCombinedSummary, revealed: Bool, hiddenByDefault: Bool) case ArchiveIntro(presentationData: ChatListPresentationData) var sortIndex: ChatListIndex { switch self { - case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _): return index case let .HoleEntry(hole, _): return ChatListIndex(pinningIndex: nil, messageIndex: hole.index) @@ -33,7 +33,7 @@ enum ChatListNodeEntry: Comparable, Identifiable { var stableId: ChatListNodeEntryId { switch self { - case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _): return .PeerId(index.messageIndex.id.peerId.toInt64()) case let .HoleEntry(hole, _): return .Hole(Int64(hole.index.id.id)) @@ -50,9 +50,9 @@ enum ChatListNodeEntry: Comparable, Identifiable { static func ==(lhs: ChatListNodeEntry, rhs: ChatListNodeEntry) -> Bool { switch lhs { - case let .PeerEntry(lhsIndex, lhsPresentationData, lhsMessage, lhsUnreadCount, lhsNotificationSettings, lhsEmbeddedState, lhsPeer, lhsPresence, lhsSummaryInfo, lhsEditing, lhsHasRevealControls, lhsSelected, lhsInputActivities, lhsAd): + case let .PeerEntry(lhsIndex, lhsPresentationData, lhsMessage, lhsUnreadCount, lhsNotificationSettings, lhsEmbeddedState, lhsPeer, lhsPresence, lhsSummaryInfo, lhsEditing, lhsHasRevealControls, lhsSelected, lhsInputActivities, lhsAd, lhsHasFailedMessages): switch rhs { - case let .PeerEntry(rhsIndex, rhsPresentationData, rhsMessage, rhsUnreadCount, rhsNotificationSettings, rhsEmbeddedState, rhsPeer, rhsPresence, rhsSummaryInfo, rhsEditing, rhsHasRevealControls, rhsSelected, rhsInputActivities, rhsAd): + case let .PeerEntry(rhsIndex, rhsPresentationData, rhsMessage, rhsUnreadCount, rhsNotificationSettings, rhsEmbeddedState, rhsPeer, rhsPresence, rhsSummaryInfo, rhsEditing, rhsHasRevealControls, rhsSelected, rhsInputActivities, rhsAd, rhsHasFailedMessages): if lhsIndex != rhsIndex { return false } @@ -65,6 +65,20 @@ enum ChatListNodeEntry: Comparable, Identifiable { if lhsMessage?.id != rhsMessage?.id || lhsMessage?.flags != rhsMessage?.flags || lhsUnreadCount != rhsUnreadCount { return false } + if let lhsMessage = lhsMessage, let rhsMessage = rhsMessage { + if lhsMessage.associatedMessages.count != rhsMessage.associatedMessages.count { + return false + } + for (id, message) in lhsMessage.associatedMessages { + if let otherMessage = rhsMessage.associatedMessages[id] { + if message.stableVersion != otherMessage.stableVersion { + return false + } + } else { + return false + } + } + } if let lhsNotificationSettings = lhsNotificationSettings, let rhsNotificationSettings = rhsNotificationSettings { if !lhsNotificationSettings.isEqual(to: rhsNotificationSettings) { return false @@ -119,7 +133,9 @@ enum ChatListNodeEntry: Comparable, Identifiable { if lhsAd != rhsAd { return false } - + if lhsHasFailedMessages != rhsHasFailedMessages { + return false + } return true default: return false @@ -203,7 +219,7 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, } loop: for entry in view.entries { switch entry { - case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo): + case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed): if let savedMessagesPeer = savedMessagesPeer, savedMessagesPeer.id == index.messageIndex.id.peerId { continue loop } @@ -216,7 +232,7 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, updatedMessage = nil updatedCombinedReadState = nil } - result.append(.PeerEntry(index: offsetPinnedIndex(index, offset: pinnedIndexOffset), presentationData: state.presentationData, message: updatedMessage, readState: updatedCombinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, presence: peerPresence, summaryInfo: summaryInfo, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions, selected: state.selectedPeerIds.contains(index.messageIndex.id.peerId), inputActivities: state.peerInputActivities?.activities[index.messageIndex.id.peerId], isAd: false)) + result.append(.PeerEntry(index: offsetPinnedIndex(index, offset: pinnedIndexOffset), presentationData: state.presentationData, message: updatedMessage, readState: updatedCombinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, presence: peerPresence, summaryInfo: summaryInfo, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions, selected: state.selectedPeerIds.contains(index.messageIndex.id.peerId), inputActivities: state.peerInputActivities?.activities[index.messageIndex.id.peerId], isAd: false, hasFailedMessages: hasFailed)) case let .HoleEntry(hole): if hole.index.timestamp == Int32.max - 1 { return ([], true) @@ -228,13 +244,13 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, var pinningIndex: UInt16 = UInt16(pinnedIndexOffset == 0 ? 0 : (pinnedIndexOffset - 1)) if let savedMessagesPeer = savedMessagesPeer { - result.append(.PeerEntry(index: ChatListIndex.absoluteUpperBound.predecessor, presentationData: state.presentationData, message: nil, readState: nil, notificationSettings: nil, embeddedInterfaceState: nil, peer: RenderedPeer(peerId: savedMessagesPeer.id, peers: SimpleDictionary([savedMessagesPeer.id: savedMessagesPeer])), presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(), editing: state.editing, hasActiveRevealControls: false, selected: false, inputActivities: nil, isAd: false)) + result.append(.PeerEntry(index: ChatListIndex.absoluteUpperBound.predecessor, presentationData: state.presentationData, message: nil, readState: nil, notificationSettings: nil, embeddedInterfaceState: nil, peer: RenderedPeer(peerId: savedMessagesPeer.id, peers: SimpleDictionary([savedMessagesPeer.id: savedMessagesPeer])), presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(), editing: state.editing, hasActiveRevealControls: false, selected: false, inputActivities: nil, isAd: false, hasFailedMessages: false)) } else { if !view.additionalItemEntries.isEmpty { for entry in view.additionalItemEntries.reversed() { switch entry { - case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo): - result.append(.PeerEntry(index: ChatListIndex(pinningIndex: pinningIndex, messageIndex: index.messageIndex), presentationData: state.presentationData, message: message, readState: combinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, presence: peerPresence, summaryInfo: summaryInfo, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions, selected: state.selectedPeerIds.contains(index.messageIndex.id.peerId), inputActivities: state.peerInputActivities?.activities[index.messageIndex.id.peerId], isAd: true)) + case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed): + result.append(.PeerEntry(index: ChatListIndex(pinningIndex: pinningIndex, messageIndex: index.messageIndex), presentationData: state.presentationData, message: message, readState: combinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, presence: peerPresence, summaryInfo: summaryInfo, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions, selected: state.selectedPeerIds.contains(index.messageIndex.id.peerId), inputActivities: state.peerInputActivities?.activities[index.messageIndex.id.peerId], isAd: true, hasFailedMessages: hasFailed)) if pinningIndex != 0 { pinningIndex -= 1 } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift index 45a78f684b..5878d7df08 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift @@ -4,23 +4,21 @@ import TelegramCore import SyncCore import SwiftSignalKit import Display +import TelegramUIPreferences enum ChatListNodeLocation: Equatable { - case initial(count: Int) - case navigation(index: ChatListIndex) - case scroll(index: ChatListIndex, sourceIndex: ChatListIndex, scrollPosition: ListViewScrollPosition, animated: Bool) + case initial(count: Int, filter: ChatListFilter?) + case navigation(index: ChatListIndex, filter: ChatListFilter?) + case scroll(index: ChatListIndex, sourceIndex: ChatListIndex, scrollPosition: ListViewScrollPosition, animated: Bool, filter: ChatListFilter?) - static func ==(lhs: ChatListNodeLocation, rhs: ChatListNodeLocation) -> Bool { - switch lhs { - case let .navigation(index): - switch rhs { - case .navigation(index): - return true - default: - return false - } - default: - return false + var filter: ChatListFilter? { + switch self { + case let .initial(initial): + return initial.filter + case let .navigation(navigation): + return navigation.filter + case let .scroll(scroll): + return scroll.filter } } } @@ -32,17 +30,90 @@ struct ChatListNodeViewUpdate { } func chatListViewForLocation(groupId: PeerGroupId, location: ChatListNodeLocation, account: Account) -> Signal { + let filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)? + if let filter = location.filter { + let includePeers = Set(filter.includePeers) + filterPredicate = { peer, notificationSettings, isUnread in + if includePeers.contains(peer.id) { + return true + } + if filter.excludeRead { + if !isUnread { + return false + } + } + if filter.excludeMuted { + if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings { + if case .muted = notificationSettings.muteState { + return false + } + } else { + return false + } + } + if !filter.categories.contains(.privateChats) { + if let user = peer as? TelegramUser { + if user.botInfo == nil { + return false + } + } + } + if !filter.categories.contains(.secretChats) { + if let _ = peer as? TelegramSecretChat { + return false + } + } + if !filter.categories.contains(.bots) { + if let user = peer as? TelegramUser { + if user.botInfo != nil { + return false + } + } + } + if !filter.categories.contains(.privateGroups) { + if let _ = peer as? TelegramGroup { + return false + } else if let channel = peer as? TelegramChannel { + if case .group = channel.info { + if channel.username == nil { + return false + } + } + } + } + if !filter.categories.contains(.publicGroups) { + if let channel = peer as? TelegramChannel { + if case .group = channel.info { + if channel.username != nil { + return false + } + } + } + } + if !filter.categories.contains(.channels) { + if let channel = peer as? TelegramChannel { + if case .broadcast = channel.info { + return false + } + } + } + return true + } + } else { + filterPredicate = nil + } + switch location { - case let .initial(count): + case let .initial(count, _): let signal: Signal<(ChatListView, ViewUpdateType), NoError> - signal = account.viewTracker.tailChatListView(groupId: groupId, count: count) + signal = account.viewTracker.tailChatListView(groupId: groupId, filterPredicate: filterPredicate, count: count) return signal |> map { view, updateType -> ChatListNodeViewUpdate in return ChatListNodeViewUpdate(view: view, type: updateType, scrollPosition: nil) } - case let .navigation(index): + case let .navigation(index, _): var first = true - return account.viewTracker.aroundChatListView(groupId: groupId, index: index, count: 80) + return account.viewTracker.aroundChatListView(groupId: groupId, filterPredicate: filterPredicate, index: index, count: 80) |> map { view, updateType -> ChatListNodeViewUpdate in let genericType: ViewUpdateType if first { @@ -53,11 +124,11 @@ func chatListViewForLocation(groupId: PeerGroupId, location: ChatListNodeLocatio } return ChatListNodeViewUpdate(view: view, type: genericType, scrollPosition: nil) } - case let .scroll(index, sourceIndex, scrollPosition, animated): + case let .scroll(index, sourceIndex, scrollPosition, animated, _): let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up let chatScrollPosition: ChatListNodeViewScrollPosition = .index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated) var first = true - return account.viewTracker.aroundChatListView(groupId: groupId, index: index, count: 80) + return account.viewTracker.aroundChatListView(groupId: groupId, filterPredicate: filterPredicate, index: index, count: 80) |> map { view, updateType -> ChatListNodeViewUpdate in let genericType: ViewUpdateType let scrollPosition: ChatListNodeViewScrollPosition? = first ? chatScrollPosition : nil diff --git a/submodules/ChatListUI/Sources/Node/ChatListPresentationData.swift b/submodules/ChatListUI/Sources/Node/ChatListPresentationData.swift index da0d405e97..5c7f1190b2 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListPresentationData.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListPresentationData.swift @@ -5,14 +5,16 @@ import TelegramUIPreferences public final class ChatListPresentationData { public let theme: PresentationTheme + public let fontSize: PresentationFontSize public let strings: PresentationStrings public let dateTimeFormat: PresentationDateTimeFormat public let nameSortOrder: PresentationPersonNameOrder public let nameDisplayOrder: PresentationPersonNameOrder public let disableAnimations: Bool - public init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool) { + public init(theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool) { self.theme = theme + self.fontSize = fontSize self.strings = strings self.dateTimeFormat = dateTimeFormat self.nameSortOrder = nameSortOrder diff --git a/submodules/ChatListUI/Sources/Node/ChatListStatusNode.swift b/submodules/ChatListUI/Sources/Node/ChatListStatusNode.swift index fdd9b7d121..a4f6cb2a2a 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListStatusNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListStatusNode.swift @@ -34,6 +34,8 @@ enum ChatListStatusNodeState: Equatable { private let transitionDuration = 0.2 class ChatListStatusContentNode: ASDisplayNode { + var fontSize: CGFloat = 17.0 + override init() { super.init() @@ -57,6 +59,13 @@ class ChatListStatusContentNode: ASDisplayNode { final class ChatListStatusNode: ASDisplayNode { private(set) var state: ChatListStatusNodeState = .none + var fontSize: CGFloat = 17.0 { + didSet { + self.contentNode?.fontSize = self.fontSize + self.nextContentNode?.fontSize = self.fontSize + } + } + private var contentNode: ChatListStatusContentNode? private var nextContentNode: ChatListStatusContentNode? @@ -66,6 +75,7 @@ final class ChatListStatusNode: ASDisplayNode { self.state = state let contentNode = state.contentNode() + contentNode?.fontSize = self.fontSize if contentNode?.classForCoder != self.contentNode?.classForCoder { contentNode?.updateWithState(state, animated: animated) self.transitionToContentNode(contentNode, state: state, fromState: currentState, animated: animated, completion: completion) @@ -190,10 +200,12 @@ private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) { private final class StatusChecksNodeParameters: NSObject { let color: UIColor let progress: CGFloat + let fontSize: CGFloat - init(color: UIColor, progress: CGFloat) { + init(color: UIColor, progress: CGFloat, fontSize: CGFloat) { self.color = color self.progress = progress + self.fontSize = fontSize super.init() } @@ -214,6 +226,12 @@ private class ChatListStatusChecksNode: ChatListStatusContentNode { } } + override var fontSize: CGFloat { + didSet { + self.setNeedsDisplay() + } + } + init(color: UIColor) { self.color = color @@ -241,7 +259,7 @@ private class ChatListStatusChecksNode: ChatListStatusContentNode { } override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - return StatusChecksNodeParameters(color: self.color, progress: self.effectiveProgress) + return StatusChecksNodeParameters(color: self.color, progress: self.effectiveProgress, fontSize: self.fontSize) } override func didEnterHierarchy() { @@ -261,6 +279,11 @@ private class ChatListStatusChecksNode: ChatListStatusContentNode { return } + let scaleFactor = min(1.4, parameters.fontSize / 17.0) + context.translateBy(x: bounds.width / 2.0, y: bounds.height / 2.0) + context.scaleBy(x: scaleFactor, y: scaleFactor) + context.translateBy(x: -bounds.width / 2.0, y: -bounds.height / 2.0) + let progress = parameters.progress context.setStrokeColor(parameters.color.cgColor) diff --git a/submodules/ChatListUI/Sources/Node/ChatListTypingNode.swift b/submodules/ChatListUI/Sources/Node/ChatListTypingNode.swift index dbcb66e59c..e67e3f05c9 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListTypingNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListTypingNode.swift @@ -10,8 +10,6 @@ import TelegramPresentationData import ChatTitleActivityNode import LocalizedPeerData -private let textFont = Font.regular(15.0) - final class ChatListInputActivitiesNode: ASDisplayNode { private let activityNode: ChatTitleActivityNode @@ -23,8 +21,12 @@ final class ChatListInputActivitiesNode: ASDisplayNode { self.addSubnode(self.activityNode) } - func asyncLayout() -> (CGSize, PresentationStrings, UIColor, PeerId, [(Peer, PeerInputActivity)]) -> (CGSize, () -> Void) { - return { [weak self] boundingSize, strings, color, peerId, activities in + func asyncLayout() -> (CGSize, ChatListPresentationData, UIColor, PeerId, [(Peer, PeerInputActivity)]) -> (CGSize, () -> Void) { + return { [weak self] boundingSize, presentationData, color, peerId, activities in + let strings = presentationData.strings + + let textFont = Font.regular(floor(presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + var state = ChatTitleActivityNodeState.none if !activities.isEmpty { diff --git a/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift b/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift index 9e5adf7de0..d7997ee48c 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift @@ -6,11 +6,13 @@ import SwiftSignalKit import Display import MergeLists import SearchUI +import TelegramUIPreferences struct ChatListNodeView { let originalView: ChatListView let filteredEntries: [ChatListNodeEntry] let isLoading: Bool + let filter: ChatListFilter? } enum ChatListNodeViewTransitionReason { @@ -48,7 +50,7 @@ enum ChatListNodeViewScrollPosition { case index(index: ChatListIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) } -func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toView: ChatListNodeView, reason: ChatListNodeViewTransitionReason, disableAnimations: Bool, account: Account, scrollPosition: ChatListNodeViewScrollPosition?, searchMode: Bool) -> Signal { +func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toView: ChatListNodeView, reason: ChatListNodeViewTransitionReason, previewing: Bool, disableAnimations: Bool, account: Account, scrollPosition: ChatListNodeViewScrollPosition?, searchMode: Bool) -> Signal { return Signal { subscriber in let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries) @@ -165,7 +167,7 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV var fromEmptyView = false if let fromView = fromView { - if fromView.filteredEntries.isEmpty { + if fromView.filteredEntries.isEmpty || fromView.filter != toView.filter { options.remove(.AnimateInsertion) options.remove(.AnimateAlpha) fromEmptyView = true @@ -174,7 +176,7 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV fromEmptyView = true } - if !searchMode && fromEmptyView && scrollToItem == nil && toView.filteredEntries.count >= 1 { + if !previewing && !searchMode && fromEmptyView && scrollToItem == nil && toView.filteredEntries.count >= 1 { scrollToItem = ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up) } diff --git a/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift b/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift new file mode 100644 index 0000000000..6cb265b924 --- /dev/null +++ b/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift @@ -0,0 +1,701 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import AsyncDisplayKit +import TelegramPresentationData +import AccountContext +import SyncCore +import Postbox +import TelegramUIPreferences +import TelegramCore + +final class TabBarChatListFilterController: ViewController { + private var controllerNode: TabBarChatListFilterControllerNode { + return self.displayNode as! TabBarChatListFilterControllerNode + } + + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + + private let context: AccountContext + private let sourceNodes: [ASDisplayNode] + private let presetList: [ChatListFilter] + private let currentPreset: ChatListFilter? + private let setup: () -> Void + private let updatePreset: (ChatListFilter?) -> Void + + private var presentationData: PresentationData + private var didPlayPresentationAnimation = false + + private let hapticFeedback = HapticFeedback() + + public init(context: AccountContext, sourceNodes: [ASDisplayNode], presetList: [ChatListFilter], currentPreset: ChatListFilter?, setup: @escaping () -> Void, updatePreset: @escaping (ChatListFilter?) -> Void) { + self.context = context + self.sourceNodes = sourceNodes + self.presetList = presetList + self.currentPreset = currentPreset + self.setup = setup + self.updatePreset = updatePreset + + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + self.statusBar.ignoreInCall = true + + self.lockOrientation = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func loadDisplayNode() { + self.displayNode = TabBarChatListFilterControllerNode(context: self.context, presentationData: self.presentationData, cancel: { [weak self] in + self?.dismiss() + }, sourceNodes: self.sourceNodes, presetList: self.presetList, currentPreset: self.currentPreset, setup: { [weak self] in + self?.setup() + self?.dismiss(sourceNodes: [], fadeOutIcon: true) + }, updatePreset: { [weak self] filter in + self?.updatePreset(filter) + self?.dismiss() + }) + self._ready.set(self.controllerNode.isReady.get()) + self.displayNodeDidLoad() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.didPlayPresentationAnimation { + self.didPlayPresentationAnimation = true + + self.hapticFeedback.impact() + self.controllerNode.animateIn() + } + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, transition: transition) + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.dismiss(sourceNodes: [], fadeOutIcon: false) + } + + func dismiss(sourceNodes: [ASDisplayNode], fadeOutIcon: Bool) { + self.controllerNode.animateOut(sourceNodes: sourceNodes, fadeOutIcon: fadeOutIcon, completion: { [weak self] in + self?.didPlayPresentationAnimation = false + self?.presentingViewController?.dismiss(animated: false, completion: nil) + }) + } +} + +private let animationDurationFactor: Double = 1.0 + +private protocol AbstractTabBarChatListFilterItemNode { + func updateLayout(maxWidth: CGFloat) -> (CGFloat, CGFloat, (CGFloat) -> Void) +} + +private final class AddFilterItemNode: ASDisplayNode, AbstractTabBarChatListFilterItemNode { + private let action: () -> Void + + private let separatorNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let buttonNode: HighlightTrackingButtonNode + private let plusNode: ASImageNode + private let titleNode: ImmediateTextNode + + init(displaySeparator: Bool, presentationData: PresentationData, action: @escaping () -> Void) { + self.action = action + + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemSeparatorColor + self.separatorNode.isHidden = !displaySeparator + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor + self.highlightedBackgroundNode.alpha = 0.0 + + self.buttonNode = HighlightTrackingButtonNode() + + self.titleNode = ImmediateTextNode() + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.attributedText = NSAttributedString(string: "Setup", font: Font.regular(17.0), textColor: presentationData.theme.actionSheet.primaryTextColor) + + self.plusNode = ASImageNode() + self.plusNode.image = generateItemListPlusIcon(presentationData.theme.actionSheet.primaryTextColor) + + super.init() + + self.addSubnode(self.separatorNode) + self.addSubnode(self.highlightedBackgroundNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.plusNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.highlightedBackgroundNode.alpha = 1.0 + } else { + strongSelf.highlightedBackgroundNode.alpha = 0.0 + strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + } + } + + func updateLayout(maxWidth: CGFloat) -> (CGFloat, CGFloat, (CGFloat) -> Void) { + let leftInset: CGFloat = 16.0 + let rightInset: CGFloat = 10.0 + let iconInset: CGFloat = 60.0 + let titleSize = self.titleNode.updateLayout(CGSize(width: maxWidth - leftInset - rightInset, height: .greatestFiniteMagnitude)) + let height: CGFloat = 61.0 + + return (titleSize.width + leftInset + rightInset, height, { width in + self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize) + + if let image = self.plusNode.image { + self.plusNode.frame = CGRect(origin: CGPoint(x: floor(width - iconInset + (iconInset - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size) + } + + self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: height - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel)) + self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: height)) + self.buttonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: height)) + }) + } + + @objc private func buttonPressed() { + self.action() + } +} + +private final class FilterItemNode: ASDisplayNode, AbstractTabBarChatListFilterItemNode { + private let context: AccountContext + private let title: String + let preset: ChatListFilter? + private let isCurrent: Bool + private let presentationData: PresentationData + private let action: () -> Bool + + private let separatorNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let buttonNode: HighlightTrackingButtonNode + private let titleNode: ImmediateTextNode + private let checkNode: ASImageNode + + private let badgeBackgroundNode: ASImageNode + private let badgeTitleNode: ImmediateTextNode + private var badgeText: String = "" + + init(context: AccountContext, title: String, preset: ChatListFilter?, isCurrent: Bool, displaySeparator: Bool, presentationData: PresentationData, action: @escaping () -> Bool) { + self.context = context + self.title = title + self.preset = preset + self.isCurrent = isCurrent + self.presentationData = presentationData + self.action = action + + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemSeparatorColor + self.separatorNode.isHidden = !displaySeparator + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor + self.highlightedBackgroundNode.alpha = 0.0 + + self.buttonNode = HighlightTrackingButtonNode() + + self.titleNode = ImmediateTextNode() + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: presentationData.theme.actionSheet.primaryTextColor) + + self.checkNode = ASImageNode() + self.checkNode.image = generateItemListCheckIcon(color: presentationData.theme.actionSheet.primaryTextColor) + self.checkNode.isHidden = true//!isCurrent + + self.badgeBackgroundNode = ASImageNode() + self.badgeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: presentationData.theme.list.itemCheckColors.fillColor) + self.badgeTitleNode = ImmediateTextNode() + self.badgeBackgroundNode.isHidden = true + self.badgeTitleNode.isHidden = true + + super.init() + + self.addSubnode(self.separatorNode) + self.addSubnode(self.highlightedBackgroundNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.checkNode) + self.addSubnode(self.badgeBackgroundNode) + self.addSubnode(self.badgeTitleNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.highlightedBackgroundNode.alpha = 1.0 + } else { + strongSelf.highlightedBackgroundNode.alpha = 0.0 + strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + } + } + + func updateLayout(maxWidth: CGFloat) -> (CGFloat, CGFloat, (CGFloat) -> Void) { + let leftInset: CGFloat = 16.0 + + let badgeTitleSize = self.badgeTitleNode.updateLayout(CGSize(width: 100.0, height: .greatestFiniteMagnitude)) + let badgeMinSize = self.badgeBackgroundNode.image?.size.width ?? 20.0 + let badgeSize = CGSize(width: max(badgeMinSize, badgeTitleSize.width + 12.0), height: badgeMinSize) + + let rightInset: CGFloat = max(20.0, badgeSize.width + 20.0) + + let titleSize = self.titleNode.updateLayout(CGSize(width: maxWidth - leftInset - rightInset, height: .greatestFiniteMagnitude)) + + let height: CGFloat = 61.0 + + return (titleSize.width + leftInset + rightInset, height, { width in + self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize) + + if let image = self.checkNode.image { + self.checkNode.frame = CGRect(origin: CGPoint(x: width - rightInset + floor((rightInset - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size) + } + + let badgeBackgroundFrame = CGRect(origin: CGPoint(x: width - rightInset + floor((rightInset - badgeSize.width) / 2.0), y: floor((height - badgeSize.height) / 2.0)), size: badgeSize) + self.badgeBackgroundNode.frame = badgeBackgroundFrame + self.badgeTitleNode.frame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.minX + floor((badgeBackgroundFrame.width - badgeTitleSize.width) / 2.0), y: badgeBackgroundFrame.minY + floor((badgeBackgroundFrame.height - badgeTitleSize.height) / 2.0)), size: badgeTitleSize) + + self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: height - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel)) + self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: height)) + self.buttonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: height)) + }) + } + + @objc private func buttonPressed() { + let _ = self.action() + //self.checkNode.isHidden = !isCurrent + } + + func updateBadge(text: String) -> Bool { + if text != self.badgeText { + self.badgeText = text + self.badgeTitleNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor) + self.badgeBackgroundNode.isHidden = text.isEmpty + self.badgeTitleNode.isHidden = text.isEmpty + return true + } else { + return false + } + } +} + +private final class TabBarChatListFilterControllerNode: ViewControllerTracingNode { + private let presentationData: PresentationData + private let cancel: () -> Void + + private let effectView: UIVisualEffectView + private var propertyAnimator: AnyObject? + private var displayLinkAnimator: DisplayLinkAnimator? + private let dimNode: ASDisplayNode + + private let contentContainerNode: ASDisplayNode + private let contentNodes: [ASDisplayNode & AbstractTabBarChatListFilterItemNode] + + private var sourceNodes: [ASDisplayNode] + private var snapshotViews: [UIView] = [] + + private var validLayout: ContainerViewLayout? + + private var countsDisposable: Disposable? + let isReady = Promise() + private var didSetIsReady = false + + init(context: AccountContext, presentationData: PresentationData, cancel: @escaping () -> Void, sourceNodes: [ASDisplayNode], presetList: [ChatListFilter], currentPreset: ChatListFilter?, setup: @escaping () -> Void, updatePreset: @escaping (ChatListFilter?) -> Void) { + self.presentationData = presentationData + self.cancel = cancel + self.sourceNodes = sourceNodes + + self.effectView = UIVisualEffectView() + if #available(iOS 9.0, *) { + } else { + if presentationData.theme.rootController.keyboardColor == .dark { + self.effectView.effect = UIBlurEffect(style: .dark) + } else { + self.effectView.effect = UIBlurEffect(style: .light) + } + self.effectView.alpha = 0.0 + } + + self.dimNode = ASDisplayNode() + self.dimNode.alpha = 1.0 + if presentationData.theme.rootController.keyboardColor == .light { + self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.04) + } else { + self.dimNode.backgroundColor = presentationData.theme.chatList.backgroundColor.withAlphaComponent(0.2) + } + + self.contentContainerNode = ASDisplayNode() + self.contentContainerNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor + self.contentContainerNode.cornerRadius = 20.0 + self.contentContainerNode.clipsToBounds = true + + var contentNodes: [ASDisplayNode & AbstractTabBarChatListFilterItemNode] = [] + contentNodes.append(AddFilterItemNode(displaySeparator: true, presentationData: presentationData, action: { + setup() + })) + + for i in 0 ..< presetList.count { + let preset = presetList[i] + + let title: String = preset.title ?? "" + contentNodes.append(FilterItemNode(context: context, title: title, preset: preset, isCurrent: currentPreset == preset, displaySeparator: i != presetList.count - 1, presentationData: presentationData, action: { + updatePreset(preset) + return false + })) + } + self.contentNodes = contentNodes + + super.init() + + self.view.addSubview(self.effectView) + self.addSubnode(self.dimNode) + self.addSubnode(self.contentContainerNode) + self.contentNodes.forEach(self.contentContainerNode.addSubnode) + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + + var unreadCountItems: [UnreadMessageCountsItem] = [] + unreadCountItems.append(.total(nil)) + var additionalPeerIds = Set() + for preset in presetList { + additionalPeerIds.formUnion(preset.includePeers) + } + if !additionalPeerIds.isEmpty { + for peerId in additionalPeerIds { + unreadCountItems.append(.peer(peerId)) + } + } + let unreadKey: PostboxViewKey = .unreadCounts(items: unreadCountItems) + var keys: [PostboxViewKey] = [] + keys.append(unreadKey) + for peerId in additionalPeerIds { + keys.append(.basicPeer(peerId)) + } + + self.countsDisposable = (context.account.postbox.combinedView(keys: keys) + |> deliverOnMainQueue).start(next: { [weak self] view in + guard let strongSelf = self else { + return + } + + if let unreadCounts = view.views[unreadKey] as? UnreadMessageCountsView { + var peerTagAndCount: [PeerId: (PeerSummaryCounterTags, Int)] = [:] + + var totalState: ChatListTotalUnreadState? + for entry in unreadCounts.entries { + switch entry { + case let .total(_, totalStateValue): + totalState = totalStateValue + case let .peer(peerId, state): + if let state = state, state.isUnread { + if let peerView = view.views[.basicPeer(peerId)] as? BasicPeerView, let peer = peerView.peer { + let tag = context.account.postbox.seedConfiguration.peerSummaryCounterTags(peer) + var peerCount = Int(state.count) + if state.isUnread { + peerCount = max(1, peerCount) + } + peerTagAndCount[peerId] = (tag, peerCount) + } + } + } + } + + var totalUnreadChatCount = 0 + if let totalState = totalState { + for (_, counters) in totalState.filteredCounters { + totalUnreadChatCount += Int(counters.chatCount) + } + } + + var shouldUpdateLayout = false + for case let contentNode as FilterItemNode in strongSelf.contentNodes { + let badgeString: String + if let preset = contentNode.preset { + var tags: [PeerSummaryCounterTags] = [] + if preset.categories.contains(.privateChats) { + tags.append(.privateChat) + } + if preset.categories.contains(.secretChats) { + tags.append(.secretChat) + } + if preset.categories.contains(.privateGroups) { + tags.append(.privateGroup) + } + if preset.categories.contains(.bots) { + tags.append(.bot) + } + if preset.categories.contains(.publicGroups) { + tags.append(.publicGroup) + } + if preset.categories.contains(.channels) { + tags.append(.channel) + } + + var count = 0 + if let totalState = totalState { + for tag in tags { + if let value = totalState.filteredCounters[tag] { + count += Int(value.chatCount) + } + } + } + for peerId in preset.includePeers { + if let (tag, peerCount) = peerTagAndCount[peerId] { + if !tags.contains(tag) { + count += peerCount + } + } + } + if count != 0 { + badgeString = "\(count)" + } else { + badgeString = "" + } + } else { + badgeString = "" + } + if contentNode.updateBadge(text: badgeString) { + shouldUpdateLayout = true + } + } + + if shouldUpdateLayout { + if let layout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, transition: .immediate) + } + } + } + + if !strongSelf.didSetIsReady { + strongSelf.didSetIsReady = true + strongSelf.isReady.set(.single(true)) + } + }) + } + + deinit { + if let propertyAnimator = self.propertyAnimator { + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator + propertyAnimator?.stopAnimation(true) + } + } + + self.countsDisposable?.dispose() + } + + func animateIn() { + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + if #available(iOS 10.0, *) { + if let propertyAnimator = self.propertyAnimator { + let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator + propertyAnimator?.stopAnimation(true) + } + self.propertyAnimator = UIViewPropertyAnimator(duration: 0.2 * animationDurationFactor, curve: .easeInOut, animations: { [weak self] in + self?.effectView.effect = makeCustomZoomBlurEffect() + }) + } + + if let _ = self.propertyAnimator { + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor, from: 0.0, to: 1.0, update: { [weak self] value in + (self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value + }, completion: { + }) + } + } else { + UIView.animate(withDuration: 0.2 * animationDurationFactor, animations: { + self.effectView.effect = makeCustomZoomBlurEffect() + }, completion: { _ in + }) + } + + self.contentContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + if let _ = self.validLayout, let sourceNode = self.sourceNodes.first { + let sourceFrame = sourceNode.view.convert(sourceNode.bounds, to: self.view) + self.contentContainerNode.layer.animateFrame(from: sourceFrame, to: self.contentContainerNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + } + + for sourceNode in self.sourceNodes { + if let imageNode = sourceNode as? ASImageNode { + let snapshot = UIImageView() + snapshot.image = imageNode.image + snapshot.frame = sourceNode.view.convert(sourceNode.bounds, to: self.view) + snapshot.isUserInteractionEnabled = false + self.view.addSubview(snapshot) + self.snapshotViews.append(snapshot) + } else if let snapshot = sourceNode.view.snapshotContentTree() { + snapshot.frame = sourceNode.view.convert(sourceNode.bounds, to: self.view) + snapshot.isUserInteractionEnabled = false + self.view.addSubview(snapshot) + self.snapshotViews.append(snapshot) + } + sourceNode.alpha = 0.0 + } + } + + func animateOut(sourceNodes: [ASDisplayNode], fadeOutIcon: Bool, completion: @escaping () -> Void) { + self.isUserInteractionEnabled = false + + var completedEffect = false + var completedSourceNodes = false + + let intermediateCompletion: () -> Void = { + if completedEffect && completedSourceNodes { + completion() + } + } + + if #available(iOS 10.0, *) { + if let propertyAnimator = self.propertyAnimator { + let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator + propertyAnimator?.stopAnimation(true) + } + self.propertyAnimator = UIViewPropertyAnimator(duration: 0.2, curve: .easeInOut, animations: { [weak self] in + self?.effectView.effect = nil + }) + } + + if let _ = self.propertyAnimator { + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor, from: 0.0, to: 0.999, update: { [weak self] value in + (self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value + }, completion: { [weak self] in + if let strongSelf = self { + for sourceNode in strongSelf.sourceNodes { + sourceNode.alpha = 1.0 + } + } + + completedEffect = true + intermediateCompletion() + }) + } + self.effectView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.05 * animationDurationFactor, delay: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) + } else { + UIView.animate(withDuration: 0.21 * animationDurationFactor, animations: { + if #available(iOS 9.0, *) { + self.effectView.effect = nil + } else { + self.effectView.alpha = 0.0 + } + }, completion: { [weak self] _ in + if let strongSelf = self { + for sourceNode in strongSelf.sourceNodes { + sourceNode.alpha = 1.0 + } + } + + completedEffect = true + intermediateCompletion() + }) + } + + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { _ in + }) + if let _ = self.validLayout, let sourceNode = self.sourceNodes.first { + let sourceFrame = sourceNode.view.convert(sourceNode.bounds, to: self.view) + self.contentContainerNode.layer.animateFrame(from: self.contentContainerNode.frame, to: sourceFrame, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false) + } + if fadeOutIcon { + for snapshotView in self.snapshotViews { + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + completedSourceNodes = true + } else { + completedSourceNodes = true + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + + transition.updateFrame(view: self.effectView, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + let sideInset: CGFloat = 18.0 + + var contentSize = CGSize() + contentSize.width = min(layout.size.width - 40.0, 260.0) + var applyNodes: [(ASDisplayNode, CGFloat, (CGFloat) -> Void)] = [] + for itemNode in self.contentNodes { + let (width, height, apply) = itemNode.updateLayout(maxWidth: contentSize.width - sideInset * 2.0) + applyNodes.append((itemNode, height, apply)) + contentSize.width = max(contentSize.width, width) + contentSize.height += height + } + + let insets = layout.insets(options: .input) + + let contentOrigin: CGPoint + if let sourceNode = self.sourceNodes.first, let screenFrame = sourceNode.supernode?.convert(sourceNode.frame, to: nil) { + contentOrigin = CGPoint(x: max(16.0, screenFrame.maxX - contentSize.width + 8.0), y: layout.size.height - 66.0 - insets.bottom - contentSize.height) + } else { + contentOrigin = CGPoint(x: max(16.0, layout.size.width - sideInset - contentSize.width), y: layout.size.height - 66.0 - layout.intrinsicInsets.bottom - contentSize.height) + } + + transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: contentOrigin, size: contentSize)) + var nextY: CGFloat = 0.0 + for (itemNode, height, apply) in applyNodes { + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: nextY), size: CGSize(width: contentSize.width, height: height))) + apply(contentSize.width) + nextY += height + } + } + + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancel() + } + } +} + +private func setAnchorPoint(anchorPoint: CGPoint, forView view: UIView) { + var newPoint = CGPoint(x: view.bounds.size.width * anchorPoint.x, + y: view.bounds.size.height * anchorPoint.y) + + + var oldPoint = CGPoint(x: view.bounds.size.width * view.layer.anchorPoint.x, + y: view.bounds.size.height * view.layer.anchorPoint.y) + + newPoint = newPoint.applying(view.transform) + oldPoint = oldPoint.applying(view.transform) + + var position = view.layer.position + position.x -= oldPoint.x + position.x += newPoint.x + + position.y -= oldPoint.y + position.y += newPoint.y + + view.layer.position = position + view.layer.anchorPoint = anchorPoint +} diff --git a/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityContentNode.swift b/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityContentNode.swift index 27c2014190..3578b12212 100644 --- a/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityContentNode.swift +++ b/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityContentNode.swift @@ -95,6 +95,15 @@ public class ChatTitleActivityContentNode: ASDisplayNode { self.textNode.attributedText = text } + func makeCopy() -> ASDisplayNode { + let node = ASDisplayNode() + let textNode = self.textNode.makeCopy() + textNode.frame = self.textNode.frame + node.addSubnode(textNode) + node.frame = self.frame + return node + } + public func animateOut(to: ChatTitleActivityNodeState, style: ChatTitleActivityAnimationStyle, completion: @escaping () -> Void) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration, removeOnCompletion: false, completion: { _ in completion() diff --git a/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityNode.swift b/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityNode.swift index c345f428bb..2ccf9a29f5 100644 --- a/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityNode.swift +++ b/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityNode.swift @@ -61,6 +61,15 @@ public class ChatTitleActivityNode: ASDisplayNode { super.init() } + public func makeCopy() -> ASDisplayNode { + let node = ASDisplayNode() + if let contentNode = self.contentNode { + node.addSubnode(contentNode.makeCopy()) + } + node.frame = self.frame + return node + } + public func transitionToState(_ state: ChatTitleActivityNodeState, animation: ChatTitleActivityAnimationStyle = .crossfade, completion: @escaping () -> Void = {}) -> Bool { if self.state != state { let currentState = self.state diff --git a/submodules/ComposePollUI/Sources/CreatePollController.swift b/submodules/ComposePollUI/Sources/CreatePollController.swift index a1b13f6384..8fbcbab1ec 100644 --- a/submodules/ComposePollUI/Sources/CreatePollController.swift +++ b/submodules/ComposePollUI/Sources/CreatePollController.swift @@ -12,8 +12,131 @@ import AccountContext import AlertUI import PresentationDataUtils +private struct OrderedLinkedListItemOrderingId: RawRepresentable, Hashable { + var rawValue: Int +} + +private struct OrderedLinkedListItemOrdering: Comparable { + var id: OrderedLinkedListItemOrderingId + var lowerItemIds: Set + var higherItemIds: Set + + static func <(lhs: OrderedLinkedListItemOrdering, rhs: OrderedLinkedListItemOrdering) -> Bool { + if rhs.lowerItemIds.contains(lhs.id) { + return true + } + if rhs.higherItemIds.contains(lhs.id) { + return false + } + if lhs.lowerItemIds.contains(rhs.id) { + return false + } + if lhs.higherItemIds.contains(rhs.id) { + return true + } + assertionFailure() + return false + } +} + +private struct OrderedLinkedListItem { + var item: T + var ordering: OrderedLinkedListItemOrdering +} + +private struct OrderedLinkedList: Sequence, Equatable { + private var items: [OrderedLinkedListItem] = [] + private var nextId: Int = 0 + + init(items: [T]) { + for i in 0 ..< items.count { + self.insert(items[i], at: i, id: nil) + } + } + + static func ==(lhs: OrderedLinkedList, rhs: OrderedLinkedList) -> Bool { + if lhs.items.count != rhs.items.count { + return false + } + for i in 0 ..< lhs.items.count { + if lhs.items[i].item != rhs.items[i].item { + return false + } + } + return true + } + + func makeIterator() -> AnyIterator> { + var index = 0 + return AnyIterator { () -> OrderedLinkedListItem? in + if index < self.items.count { + let currentIndex = index + index += 1 + return self.items[currentIndex] + } + return nil + } + } + + subscript(index: Int) -> OrderedLinkedListItem { + return self.items[index] + } + + mutating func update(at index: Int, _ f: (inout T) -> Void) { + f(&self.items[index].item) + } + + var count: Int { + return self.items.count + } + + var isEmpty: Bool { + return self.items.isEmpty + } + + var last: OrderedLinkedListItem? { + return self.items.last + } + + mutating func append(_ item: T, id: OrderedLinkedListItemOrderingId?) { + self.insert(item, at: self.items.count, id: id) + } + + mutating func insert(_ item: T, at index: Int, id: OrderedLinkedListItemOrderingId?) { + let previousId = id + let id = previousId ?? OrderedLinkedListItemOrderingId(rawValue: self.nextId) + self.nextId += 1 + + if let previousId = previousId { + for i in 0 ..< self.items.count { + self.items[i].ordering.higherItemIds.remove(previousId) + self.items[i].ordering.lowerItemIds.remove(previousId) + } + } + + var lowerItemIds = Set() + var higherItemIds = Set() + for i in 0 ..< self.items.count { + if i < index { + lowerItemIds.insert(self.items[i].ordering.id) + self.items[i].ordering.higherItemIds.insert(id) + } else { + higherItemIds.insert(self.items[i].ordering.id) + self.items[i].ordering.lowerItemIds.insert(id) + } + } + + self.items.insert(OrderedLinkedListItem(item: item, ordering: OrderedLinkedListItemOrdering(id: id, lowerItemIds: lowerItemIds, higherItemIds: higherItemIds)), at: index) + } + + mutating func remove(at index: Int) { + self.items.remove(at: index) + } +} + private let maxTextLength = 255 private let maxOptionLength = 100 +private let maxOptionCount = 10 private func processPollText(_ text: String) -> String { var text = text.trimmingCharacters(in: .whitespacesAndNewlines) @@ -25,33 +148,56 @@ private func processPollText(_ text: String) -> String { private final class CreatePollControllerArguments { let updatePollText: (String) -> Void - let updateOptionText: (Int, String) -> Void + let updateOptionText: (Int, String, Bool) -> Void let moveToNextOption: (Int) -> Void - let addOption: () -> Void + let moveToPreviousOption: (Int) -> Void let removeOption: (Int, Bool) -> Void - let optionFocused: (Int) -> Void + let optionFocused: (Int, Bool) -> Void let setItemIdWithRevealedOptions: (Int?, Int?) -> Void + let toggleOptionSelected: (Int) -> Void + let updateAnonymous: (Bool) -> Void + let updateMultipleChoice: (Bool) -> Void + let displayMultipleChoiceDisabled: () -> Void + let updateQuiz: (Bool) -> Void - init(updatePollText: @escaping (String) -> Void, updateOptionText: @escaping (Int, String) -> Void, moveToNextOption: @escaping (Int) -> Void, addOption: @escaping () -> Void, removeOption: @escaping (Int, Bool) -> Void, optionFocused: @escaping (Int) -> Void, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void) { + init(updatePollText: @escaping (String) -> Void, updateOptionText: @escaping (Int, String, Bool) -> Void, moveToNextOption: @escaping (Int) -> Void, moveToPreviousOption: @escaping (Int) -> Void, removeOption: @escaping (Int, Bool) -> Void, optionFocused: @escaping (Int, Bool) -> Void, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, toggleOptionSelected: @escaping (Int) -> Void, updateAnonymous: @escaping (Bool) -> Void, updateMultipleChoice: @escaping (Bool) -> Void, displayMultipleChoiceDisabled: @escaping () -> Void, updateQuiz: @escaping (Bool) -> Void) { self.updatePollText = updatePollText self.updateOptionText = updateOptionText self.moveToNextOption = moveToNextOption - self.addOption = addOption + self.moveToPreviousOption = moveToPreviousOption self.removeOption = removeOption self.optionFocused = optionFocused self.setItemIdWithRevealedOptions = setItemIdWithRevealedOptions + self.toggleOptionSelected = toggleOptionSelected + self.updateAnonymous = updateAnonymous + self.updateMultipleChoice = updateMultipleChoice + self.displayMultipleChoiceDisabled = displayMultipleChoiceDisabled + self.updateQuiz = updateQuiz } } private enum CreatePollSection: Int32 { case text case options + case settings +} + +private enum CreatePollEntryId: Hashable { + case textHeader + case text + case optionsHeader + case option(Int) + case optionsInfo + case anonymousVotes + case multipleChoice + case quiz + case quizInfo } private enum CreatePollEntryTag: Equatable, ItemListItemTag { case text case option(Int) - case addOption(Int) + case optionsInfo func isEqual(to other: ItemListItemTag) -> Bool { if let other = other as? CreatePollEntryTag { @@ -63,103 +209,149 @@ private enum CreatePollEntryTag: Equatable, ItemListItemTag { } private enum CreatePollEntry: ItemListNodeEntry { - case textHeader(PresentationTheme, String, ItemListSectionHeaderAccessoryText) - case text(PresentationTheme, String, String, Int) - case optionsHeader(PresentationTheme, String) - case option(PresentationTheme, PresentationStrings, Int, Int, String, String, Bool, Bool) - case addOption(PresentationTheme, String, Bool, Int) - case optionsInfo(PresentationTheme, String) + case textHeader(String, ItemListSectionHeaderAccessoryText) + case text(String, String, Int) + case optionsHeader(String) + case option(id: Int, ordering: OrderedLinkedListItemOrdering, placeholder: String, text: String, revealed: Bool, hasNext: Bool, isLast: Bool, canMove: Bool, isSelected: Bool?) + case optionsInfo(String) + case anonymousVotes(String, Bool) + case multipleChoice(String, Bool, Bool) + case quiz(String, Bool) + case quizInfo(String) var section: ItemListSectionId { switch self { - case .textHeader, .text: - return CreatePollSection.text.rawValue - case .optionsHeader, .option, .addOption, .optionsInfo: - return CreatePollSection.options.rawValue + case .textHeader, .text: + return CreatePollSection.text.rawValue + case .optionsHeader, .option, .optionsInfo: + return CreatePollSection.options.rawValue + case .anonymousVotes, .multipleChoice, .quiz, .quizInfo: + return CreatePollSection.settings.rawValue } } var tag: ItemListItemTag? { switch self { - case .text: - return CreatePollEntryTag.text - case let .option(_, _, id, _, _, _, _, _): - return CreatePollEntryTag.option(id) - case let .addOption(_, _, _, id): - return CreatePollEntryTag.addOption(id) - default: - break + case .text: + return CreatePollEntryTag.text + case let .option(option): + return CreatePollEntryTag.option(option.id) + default: + break } return nil } - var stableId: Int { + var stableId: CreatePollEntryId { switch self { - case .textHeader: - return 0 - case .text: - return 1 - case .optionsHeader: - return 2 - case let .option(_, _, id, _, _, _, _, _): - return 3 + id - case .addOption: - return 1000 - case .optionsInfo: - return 1001 + case .textHeader: + return .textHeader + case .text: + return .text + case .optionsHeader: + return .optionsHeader + case let .option(option): + return .option(option.id) + case .optionsInfo: + return .optionsInfo + case .anonymousVotes: + return .anonymousVotes + case .multipleChoice: + return .multipleChoice + case .quiz: + return .quiz + case .quizInfo: + return .quizInfo } } private var sortId: Int { switch self { - case .textHeader: - return 0 - case .text: - return 1 - case .optionsHeader: - return 2 - case let .option(_, _, _, index, _, _, _, _): - return 3 + index - case .addOption: - return 1000 - case .optionsInfo: - return 1001 + case .textHeader: + return 0 + case .text: + return 1 + case .optionsHeader: + return 2 + case let .option(option): + return 3 + case .optionsInfo: + return 1001 + case .anonymousVotes: + return 1002 + case .multipleChoice: + return 1003 + case .quiz: + return 1004 + case .quizInfo: + return 1005 } } static func <(lhs: CreatePollEntry, rhs: CreatePollEntry) -> Bool { + switch lhs { + case let .option(lhsOption): + switch rhs { + case let .option(rhsOption): + return lhsOption.ordering < rhsOption.ordering + default: + break + } + default: + break + } return lhs.sortId < rhs.sortId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! CreatePollControllerArguments switch self { - case let .textHeader(theme, text, accessoryText): - return ItemListSectionHeaderItem(theme: theme, text: text, accessoryText: accessoryText, sectionId: self.section) - case let .text(theme, placeholder, text, maxLength): - return ItemListMultilineInputItem(theme: theme, text: text, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: maxLength, display: false), sectionId: self.section, style: .blocks, textUpdated: { value in - arguments.updatePollText(value) - }, tag: CreatePollEntryTag.text) - case let .optionsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) - case let .option(theme, strings, id, _, placeholder, text, revealed, hasNext): - return CreatePollOptionItem(theme: theme, strings: strings, id: id, placeholder: placeholder, value: text, maxLength: maxOptionLength, editing: CreatePollOptionItemEditing(editable: true, hasActiveRevealControls: revealed), sectionId: self.section, setItemIdWithRevealedOptions: { id, fromId in - arguments.setItemIdWithRevealedOptions(id, fromId) - }, updated: { value in - arguments.updateOptionText(id, value) - }, next: hasNext ? { - arguments.moveToNextOption(id) - } : nil, delete: { focused in + case let .textHeader(text, accessoryText): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: accessoryText, sectionId: self.section) + case let .text(placeholder, text, maxLength): + return ItemListMultilineInputItem(presentationData: presentationData, text: text, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: maxLength, display: false), sectionId: self.section, style: .blocks, textUpdated: { value in + arguments.updatePollText(value) + }, tag: CreatePollEntryTag.text) + case let .optionsHeader(text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .option(id, _, placeholder, text, revealed, hasNext, isLast, canMove, isSelected): + return CreatePollOptionItem(presentationData: presentationData, id: id, placeholder: placeholder, value: text, isSelected: isSelected, maxLength: maxOptionLength, editing: CreatePollOptionItemEditing(editable: true, hasActiveRevealControls: revealed), sectionId: self.section, setItemIdWithRevealedOptions: { id, fromId in + arguments.setItemIdWithRevealedOptions(id, fromId) + }, updated: { value, isFocused in + arguments.updateOptionText(id, value, isFocused) + }, next: hasNext ? { + arguments.moveToNextOption(id) + } : nil, delete: { focused in + if !isLast { arguments.removeOption(id, focused) - }, focused: { - arguments.optionFocused(id) - }, tag: CreatePollEntryTag.option(id)) - case let .addOption(theme, title, enabled, id): - return CreatePollOptionActionItem(theme: theme, title: title, enabled: enabled, tag: CreatePollEntryTag.addOption(id), sectionId: self.section, action: { - arguments.addOption() - }) - case let .optionsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + } else { + arguments.moveToPreviousOption(id) + } + }, canDelete: !isLast, + canMove: canMove, + focused: { isFocused in + arguments.optionFocused(id, isFocused) + }, toggleSelected: { + arguments.toggleOptionSelected(id) + }, tag: CreatePollEntryTag.option(id)) + case let .optionsInfo(text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section, tag: CreatePollEntryTag.optionsInfo) + case let .anonymousVotes(text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateAnonymous(value) + }) + case let .multipleChoice(text, value, enabled): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateMultipleChoice(value) + }, activatedWhileDisabled: { + arguments.displayMultipleChoiceDisabled() + }) + case let .quiz(text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateQuiz(value) + }) + case let .quizInfo(text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -167,17 +359,21 @@ private enum CreatePollEntry: ItemListNodeEntry { private struct CreatePollControllerOption: Equatable { var text: String let id: Int + var isSelected: Bool } private struct CreatePollControllerState: Equatable { var text: String = "" - var options: [CreatePollControllerOption] = [CreatePollControllerOption(text: "", id: 0), CreatePollControllerOption(text: "", id: 1)] + var options = OrderedLinkedList(items: [CreatePollControllerOption(text: "", id: 0, isSelected: false), CreatePollControllerOption(text: "", id: 1, isSelected: false)]) var nextOptionId: Int = 2 var focusOptionId: Int? var optionIdWithRevealControls: Int? + var isAnonymous: Bool = true + var isMultipleChoice: Bool = false + var isQuiz: Bool = false } -private func createPollControllerEntries(presentationData: PresentationData, state: CreatePollControllerState, limitsConfiguration: LimitsConfiguration) -> [CreatePollEntry] { +private func createPollControllerEntries(presentationData: PresentationData, peer: Peer, state: CreatePollControllerState, limitsConfiguration: LimitsConfiguration, defaultIsQuiz: Bool?) -> [CreatePollEntry] { var entries: [CreatePollEntry] = [] var textLimitText = ItemListSectionHeaderAccessoryText(value: "", color: .generic) @@ -185,25 +381,55 @@ private func createPollControllerEntries(presentationData: PresentationData, sta let remainingCount = Int(maxTextLength) - state.text.count textLimitText = ItemListSectionHeaderAccessoryText(value: "\(remainingCount)", color: remainingCount < 0 ? .destructive : .generic) } - entries.append(.textHeader(presentationData.theme, presentationData.strings.CreatePoll_TextHeader, textLimitText)) - entries.append(.text(presentationData.theme, presentationData.strings.CreatePoll_TextPlaceholder, state.text, Int(limitsConfiguration.maxMediaCaptionLength))) - entries.append(.optionsHeader(presentationData.theme, presentationData.strings.CreatePoll_OptionsHeader)) - for i in 0 ..< state.options.count { - entries.append(.option(presentationData.theme, presentationData.strings, state.options[i].id, i, presentationData.strings.CreatePoll_OptionPlaceholder, state.options[i].text, state.optionIdWithRevealControls == state.options[i].id, i != 9)) - } - if state.options.count < 10 { - entries.append(.addOption(presentationData.theme, presentationData.strings.CreatePoll_AddOption, true, state.options.last?.id ?? -1)) - entries.append(.optionsInfo(presentationData.theme, presentationData.strings.CreatePoll_AddMoreOptions(Int32(10 - state.options.count)))) + entries.append(.textHeader(presentationData.strings.CreatePoll_TextHeader, textLimitText)) + entries.append(.text(presentationData.strings.CreatePoll_TextPlaceholder, state.text, Int(limitsConfiguration.maxMediaCaptionLength))) + let optionsHeaderTitle: String + if let defaultIsQuiz = defaultIsQuiz, defaultIsQuiz { + optionsHeaderTitle = presentationData.strings.CreatePoll_QuizOptionsHeader } else { - entries.append(.optionsInfo(presentationData.theme, presentationData.strings.CreatePoll_AllOptionsAdded)) + optionsHeaderTitle = presentationData.strings.CreatePoll_OptionsHeader + } + entries.append(.optionsHeader(optionsHeaderTitle)) + for i in 0 ..< state.options.count { + let isSecondLast = state.options.count == 2 && i == 0 + let isLast = i == state.options.count - 1 + let option = state.options[i].item + entries.append(.option(id: option.id, ordering: state.options[i].ordering, placeholder: isLast ? presentationData.strings.CreatePoll_AddOption : presentationData.strings.CreatePoll_OptionPlaceholder, text: option.text, revealed: state.optionIdWithRevealControls == option.id, hasNext: i != 9, isLast: isLast || isSecondLast, canMove: !isLast || state.options.count == 10, isSelected: state.isQuiz ? option.isSelected : nil)) + } + if state.options.count < maxOptionCount { + entries.append(.optionsInfo(presentationData.strings.CreatePoll_AddMoreOptions(Int32(maxOptionCount - state.options.count)))) + } else { + entries.append(.optionsInfo(presentationData.strings.CreatePoll_AllOptionsAdded)) + } + + var canBePublic = true + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + canBePublic = false + } + + if canBePublic { + entries.append(.anonymousVotes(presentationData.strings.CreatePoll_Anonymous, state.isAnonymous)) + } + if let defaultIsQuiz = defaultIsQuiz { + if !defaultIsQuiz { + entries.append(.multipleChoice(presentationData.strings.CreatePoll_MultipleChoice, state.isMultipleChoice && !state.isQuiz, !state.isQuiz)) + } + } else { + entries.append(.multipleChoice(presentationData.strings.CreatePoll_MultipleChoice, state.isMultipleChoice && !state.isQuiz, !state.isQuiz)) + entries.append(.quiz(presentationData.strings.CreatePoll_Quiz, state.isQuiz)) + entries.append(.quizInfo(presentationData.strings.CreatePoll_QuizInfo)) } return entries } -public func createPollController(context: AccountContext, peerId: PeerId, completion: @escaping (EnqueueMessage) -> Void) -> ViewController { - let statePromise = ValuePromise(CreatePollControllerState(), ignoreRepeated: true) - let stateValue = Atomic(value: CreatePollControllerState()) +public func createPollController(context: AccountContext, peer: Peer, isQuiz: Bool? = nil, completion: @escaping (EnqueueMessage) -> Void) -> ViewController { + var initialState = CreatePollControllerState() + if let isQuiz = isQuiz { + initialState.isQuiz = isQuiz + } + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) let updateState: ((CreatePollControllerState) -> CreatePollControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } @@ -212,6 +438,8 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple var dismissImpl: (() -> Void)? var ensureTextVisibleImpl: (() -> Void)? var ensureOptionVisibleImpl: ((Int) -> Void)? + var displayQuizTooltipImpl: ((Bool) -> Void)? + var attemptNavigationImpl: (() -> Bool)? let actionsDisposable = DisposableSet() @@ -224,37 +452,54 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple let arguments = CreatePollControllerArguments(updatePollText: { value in updateState { state in var state = state + state.focusOptionId = nil state.text = value return state } ensureTextVisibleImpl?() - }, updateOptionText: { id, value in + }, updateOptionText: { id, value, isFocused in + var ensureVisibleId = id updateState { state in var state = state for i in 0 ..< state.options.count { - if state.options[i].id == id { - state.options[i].text = value + if state.options[i].item.id == id { + if isFocused { + state.focusOptionId = id + } + state.options.update(at: i, { option in + option.text = value + }) + if !value.isEmpty && i == state.options.count - 1 && state.options.count < maxOptionCount { + state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false), id: nil) + state.nextOptionId += 1 + } + if i != state.options.count - 1 { + ensureVisibleId = state.options[i + 1].item.id + } + break } } return state } - ensureOptionVisibleImpl?(id) + if isFocused { + ensureOptionVisibleImpl?(ensureVisibleId) + } }, moveToNextOption: { id in var resetFocusOptionId: Int? updateState { state in var state = state for i in 0 ..< state.options.count { - if state.options[i].id == id { + if state.options[i].item.id == id { if i == state.options.count - 1 { - state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId)) + /*state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false)) state.focusOptionId = state.nextOptionId - state.nextOptionId += 1 + state.nextOptionId += 1*/ } else { - if state.focusOptionId == state.options[i + 1].id { - resetFocusOptionId = state.options[i + 1].id + if state.focusOptionId == state.options[i + 1].item.id { + resetFocusOptionId = state.options[i + 1].item.id state.focusOptionId = -1 } else { - state.focusOptionId = state.options[i + 1].id + state.focusOptionId = state.options[i + 1].item.id } } break @@ -269,30 +514,78 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple return state } } - }, addOption: { - updateState { state in - var state = state - state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId)) - state.focusOptionId = state.nextOptionId - state.nextOptionId += 1 - return state - } - }, removeOption: { id, focused in + }, moveToPreviousOption: { id in + var resetFocusOptionId: Int? updateState { state in var state = state for i in 0 ..< state.options.count { - if state.options[i].id == id { - state.options.remove(at: i) - if focused && i != 0 { - state.focusOptionId = state.options[i - 1].id + if state.options[i].item.id == id { + if i != 0 { + if state.focusOptionId == state.options[i - 1].item.id { + resetFocusOptionId = state.options[i - 1].item.id + state.focusOptionId = -1 + } else { + state.focusOptionId = state.options[i - 1].item.id + } } break } } return state } - }, optionFocused: { id in - ensureOptionVisibleImpl?(id) + if let resetFocusOptionId = resetFocusOptionId { + updateState { state in + var state = state + state.focusOptionId = resetFocusOptionId + return state + } + } + }, removeOption: { id, focused in + updateState { state in + var state = state + for i in 0 ..< state.options.count { + if state.options[i].item.id == id { + state.options.remove(at: i) + if focused && i != 0 { + state.focusOptionId = state.options[i - 1].item.id + } + break + } + } + let focusOnFirst = state.options.isEmpty + if state.options.count < 2 { + for i in 0 ..< (2 - state.options.count) { + if i == 0 && focusOnFirst { + state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false), id: nil) + state.focusOptionId = state.nextOptionId + state.nextOptionId += 1 + } else { + state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false), id: nil) + state.nextOptionId += 1 + } + } + } + return state + } + }, optionFocused: { id, isFocused in + if isFocused { + ensureOptionVisibleImpl?(id) + } else { + updateState { state in + var state = state + if state.options.count > 2 { + for i in 0 ..< state.options.count { + if state.options[i].item.id == id { + if state.options[i].item.text.isEmpty && i != state.options.count - 1 { + state.options.remove(at: i) + } + break + } + } + } + return state + } + } }, setItemIdWithRevealedOptions: { id, fromId in updateState { state in var state = state @@ -303,6 +596,73 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple return state } } + }, toggleOptionSelected: { id in + updateState { state in + var state = state + for i in 0 ..< state.options.count { + if state.options[i].item.id == id { + state.options.update(at: i, { option in + option.isSelected = !option.isSelected + }) + if state.options[i].item.isSelected && state.isQuiz { + for j in 0 ..< state.options.count { + if i != j { + state.options.update(at: j, { option in + option.isSelected = false + }) + } + } + } + break + } + } + return state + } + }, updateAnonymous: { value in + updateState { state in + var state = state + state.focusOptionId = -1 + state.isAnonymous = value + return state + } + }, updateMultipleChoice: { value in + updateState { state in + var state = state + state.focusOptionId = -1 + state.isMultipleChoice = value + return state + } + }, displayMultipleChoiceDisabled: { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.CreatePoll_MultipleChoiceQuizAlert, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), nil) + }, updateQuiz: { value in + if !value { + displayQuizTooltipImpl?(value) + } + updateState { state in + var state = state + state.focusOptionId = -1 + state.isQuiz = value + if value { + state.isMultipleChoice = false + var foundSelectedOption = false + for i in 0 ..< state.options.count { + if state.options[i].item.isSelected { + if !foundSelectedOption { + foundSelectedOption = true + } else { + state.options.update(at: i, { option in + option.isSelected = false + }) + } + } + } + } + return state + } + if value { + displayQuizTooltipImpl?(value) + } }) let previousOptionIds = Atomic<[Int]?>(value: nil) @@ -320,11 +680,25 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple enabled = false } var nonEmptyOptionCount = 0 + var hasSelectedOptions = false for option in state.options { - if !option.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + if !option.item.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { nonEmptyOptionCount += 1 } - if option.text.count > maxOptionLength { + if option.item.text.count > maxOptionLength { + enabled = false + } + if option.item.isSelected { + hasSelectedOptions = true + } + if state.isQuiz { + if option.item.text.isEmpty && option.item.isSelected { + enabled = false + } + } + } + if state.isQuiz { + if !hasSelectedOptions { enabled = false } } @@ -335,44 +709,48 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.CreatePoll_Create), style: .bold, enabled: enabled, action: { let state = stateValue.with { $0 } var options: [TelegramMediaPollOption] = [] + var correctAnswers: [Data]? for i in 0 ..< state.options.count { - let optionText = state.options[i].text.trimmingCharacters(in: .whitespacesAndNewlines) + let optionText = state.options[i].item.text.trimmingCharacters(in: .whitespacesAndNewlines) if !optionText.isEmpty { - options.append(TelegramMediaPollOption(text: optionText, opaqueIdentifier: "\(i)".data(using: .utf8)!)) + let optionData = "\(i)".data(using: .utf8)! + options.append(TelegramMediaPollOption(text: optionText, opaqueIdentifier: optionData)) + if state.isQuiz && state.options[i].item.isSelected { + correctAnswers = [optionData] + } } } - completion(.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: arc4random64()), text: processPollText(state.text), options: options, results: TelegramMediaPollResults(voters: nil, totalVoters: nil), isClosed: false)), replyToMessageId: nil, localGroupingKey: nil)) + let publicity: TelegramMediaPollPublicity + if state.isAnonymous { + publicity = .anonymous + } else { + publicity = .public + } + let kind: TelegramMediaPollKind + if state.isQuiz { + kind = .quiz + } else { + kind = .poll(multipleAnswers: state.isMultipleChoice) + } dismissImpl?() + completion(.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: arc4random64()), publicity: publicity, kind: kind, text: processPollText(state.text), options: options, correctAnswers: correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: []), isClosed: false)), replyToMessageId: nil, localGroupingKey: nil)) }) let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { - let state = stateValue.with { $0 } - var hasNonEmptyOptions = false - for i in 0 ..< state.options.count { - let optionText = state.options[i].text.trimmingCharacters(in: .whitespacesAndNewlines) - if !optionText.isEmpty { - hasNonEmptyOptions = true - } - } - if hasNonEmptyOptions || !state.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.CreatePoll_CancelConfirmation, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { - dismissImpl?() - })]), nil) - } else { + if let attemptNavigationImpl = attemptNavigationImpl, attemptNavigationImpl() { dismissImpl?() } }) - let optionIds = state.options.map { $0.id } + let optionIds = state.options.map { $0.item.id } let previousIds = previousOptionIds.swap(optionIds) var focusItemTag: ItemListItemTag? var ensureVisibleItemTag: ItemListItemTag? if let focusOptionId = state.focusOptionId { focusItemTag = CreatePollEntryTag.option(focusOptionId) - if focusOptionId == state.options.last?.id { - ensureVisibleItemTag = CreatePollEntryTag.addOption(focusOptionId) + if focusOptionId == state.options.last?.item.id { + ensureVisibleItemTag = nil } else { ensureVisibleItemTag = focusItemTag } @@ -381,8 +759,15 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple ensureVisibleItemTag = focusItemTag } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.CreatePoll_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: createPollControllerEntries(presentationData: presentationData, state: state, limitsConfiguration: limitsConfiguration), style: .blocks, focusItemTag: focusItemTag, ensureVisibleItemTag: ensureVisibleItemTag, animateChanges: previousIds != nil && previousIds != optionIds) + let title: String + if let isQuiz = isQuiz, isQuiz { + title = presentationData.strings.CreatePoll_QuizTitle + } else { + title = presentationData.strings.CreatePoll_Title + } + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createPollControllerEntries(presentationData: presentationData, peer: peer, state: state, limitsConfiguration: limitsConfiguration, defaultIsQuiz: isQuiz), style: .blocks, focusItemTag: focusItemTag, ensureVisibleItemTag: ensureVisibleItemTag, animateChanges: previousIds != nil) return (controllerState, (listState, arguments)) } @@ -390,13 +775,13 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple actionsDisposable.dispose() } + weak var currentTooltipController: TooltipController? let controller = ItemListController(context: context, state: signal) controller.navigationPresentation = .modal presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) } dismissImpl = { [weak controller] in - //controller?.view.endEditing(true) controller?.dismiss() } ensureTextVisibleImpl = { [weak controller] in @@ -428,23 +813,23 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple var resultItemNode: ListViewItemNode? let state = stateValue.with({ $0 }) - if state.options.last?.id == id { - let _ = controller.frameForItemNode({ itemNode in - if let itemNode = itemNode as? ItemListItemNode { - if let tag = itemNode.tag, tag.isEqual(to: CreatePollEntryTag.addOption(id)) { - resultItemNode = itemNode as? ListViewItemNode - return true - } - } - return false - }) + var isLast = false + if state.options.last?.item.id == id { + isLast = true } if resultItemNode == nil { let _ = controller.frameForItemNode({ itemNode in - if let itemNode = itemNode as? ItemListItemNode { - if let tag = itemNode.tag, tag.isEqual(to: CreatePollEntryTag.option(id)) { - resultItemNode = itemNode as? ListViewItemNode - return true + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag { + if isLast { + if tag.isEqual(to: CreatePollEntryTag.optionsInfo) { + resultItemNode = itemNode as? ListViewItemNode + return true + } + } else { + if tag.isEqual(to: CreatePollEntryTag.option(id)) { + resultItemNode = itemNode as? ListViewItemNode + return true + } } } return false @@ -456,19 +841,53 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple } }) } - - controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [CreatePollEntry]) -> Void in - let fromEntry = entries[fromIndex] - guard case let .option(_, _, id, _, _, _, _, _) = fromEntry else { + displayQuizTooltipImpl = { [weak controller] display in + guard let controller = controller else { return } + var resultItemNode: CreatePollOptionItemNode? + let insets = controller.listInsets + let _ = controller.frameForItemNode({ itemNode in + if resultItemNode == nil, let itemNode = itemNode as? CreatePollOptionItemNode { + if itemNode.frame.minY >= insets.top { + resultItemNode = itemNode + return true + } + } + return false + }) + if let resultItemNode = resultItemNode, let localCheckNodeFrame = resultItemNode.checkNodeFrame { + let checkNodeFrame = resultItemNode.view.convert(localCheckNodeFrame, to: controller.view) + if display { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let tooltipController = TooltipController(content: .text(presentationData.strings.CreatePoll_QuizTip), baseFontSize: presentationData.listsFontSize.baseDisplaySize, dismissByTapOutside: true) + controller.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceViewAndRect: { [weak controller] in + if let controller = controller { + return (controller.view, checkNodeFrame.insetBy(dx: 0.0, dy: 0.0)) + } + return nil + })) + tooltipController.displayNode.layer.animatePosition(from: CGPoint(x: -checkNodeFrame.maxX, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + currentTooltipController = tooltipController + } else if let tooltipController = currentTooltipController{ + currentTooltipController = nil + tooltipController.displayNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -checkNodeFrame.maxX, y: 0.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + } + } + } + controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [CreatePollEntry]) -> Signal in + let fromEntry = entries[fromIndex] + guard case let .option(option) = fromEntry else { + return .single(false) + } + let id = option.id var referenceId: Int? var beforeAll = false var afterAll = false if toIndex < entries.count { switch entries[toIndex] { - case let .option(_, _, toId, _, _, _, _, _): - referenceId = toId + case let .option(toOption): + referenceId = toOption.id default: if entries[toIndex] < fromEntry { beforeAll = true @@ -479,13 +898,18 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple } else { afterAll = true } + + var didReorder = false + updateState { state in var state = state var options = state.options - var reorderOption: CreatePollControllerOption? + var reorderOption: OrderedLinkedListItem? + var previousIndex: Int? for i in 0 ..< options.count { - if options[i].id == id { + if options[i].item.id == id { reorderOption = options[i] + previousIndex = i options.remove(at: i) break } @@ -493,33 +917,76 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple if let reorderOption = reorderOption { if let referenceId = referenceId { var inserted = false - for i in 0 ..< options.count { - if options[i].id == referenceId { + for i in 0 ..< options.count - 1 { + if options[i].item.id == referenceId { if fromIndex < toIndex { - options.insert(reorderOption, at: i + 1) + didReorder = previousIndex != i + 1 + options.insert(reorderOption.item, at: i + 1, id: reorderOption.ordering.id) } else { - options.insert(reorderOption, at: i) + didReorder = previousIndex != i + options.insert(reorderOption.item, at: i, id: reorderOption.ordering.id) } inserted = true break } } if !inserted { - options.append(reorderOption) + if options.count >= 2 { + didReorder = previousIndex != options.count - 1 + options.insert(reorderOption.item, at: options.count - 1, id: reorderOption.ordering.id) + } else { + didReorder = previousIndex != options.count + options.append(reorderOption.item, id: reorderOption.ordering.id) + } } } else if beforeAll { - options.insert(reorderOption, at: 0) + didReorder = previousIndex != 0 + options.insert(reorderOption.item, at: 0, id: reorderOption.ordering.id) } else if afterAll { - options.append(reorderOption) + if options.count >= 2 { + didReorder = previousIndex != options.count - 1 + options.insert(reorderOption.item, at: options.count - 1, id: reorderOption.ordering.id) + } else { + didReorder = previousIndex != options.count + options.append(reorderOption.item, id: reorderOption.ordering.id) + } } state.options = options } return state } + + return .single(didReorder) }) + attemptNavigationImpl = { + let state = stateValue.with { $0 } + var hasNonEmptyOptions = false + for i in 0 ..< state.options.count { + let optionText = state.options[i].item.text.trimmingCharacters(in: .whitespacesAndNewlines) + if !optionText.isEmpty { + hasNonEmptyOptions = true + } + } + if hasNonEmptyOptions || !state.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.CreatePoll_CancelConfirmation, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { + dismissImpl?() + })]), nil) + return false + } else { + return true + } + } + controller.attemptNavigation = { _ in + if let attemptNavigationImpl = attemptNavigationImpl, attemptNavigationImpl() { + return true + } + return false + } controller.isOpaqueWhenInOverlay = true controller.blocksBackgroundWhenInOverlay = true controller.experimentalSnapScrollToItem = true + controller.alwaysSynchronous = true return controller } diff --git a/submodules/ComposePollUI/Sources/CreatePollOptionActionItem.swift b/submodules/ComposePollUI/Sources/CreatePollOptionActionItem.swift index ba1dae70be..e7cc231c00 100644 --- a/submodules/ComposePollUI/Sources/CreatePollOptionActionItem.swift +++ b/submodules/ComposePollUI/Sources/CreatePollOptionActionItem.swift @@ -170,7 +170,7 @@ class CreatePollOptionActionItemNode: ListViewItemNode, ItemListItemNode { strongSelf.iconNode.image = updatedIcon } if let image = strongSelf.iconNode.image { - transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0 - 1.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)) + transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0 - 3.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)) } if strongSelf.backgroundNode.supernode == nil { diff --git a/submodules/ComposePollUI/Sources/CreatePollOptionItem.swift b/submodules/ComposePollUI/Sources/CreatePollOptionItem.swift index 6b076495d3..026339a6b6 100644 --- a/submodules/ComposePollUI/Sources/CreatePollOptionItem.swift +++ b/submodules/ComposePollUI/Sources/CreatePollOptionItem.swift @@ -6,6 +6,7 @@ import SwiftSignalKit import TelegramPresentationData import ItemListUI import PresentationDataUtils +import CheckNode struct CreatePollOptionItemEditing { let editable: Bool @@ -13,27 +14,30 @@ struct CreatePollOptionItemEditing { } class CreatePollOptionItem: ListViewItem, ItemListItem { - let theme: PresentationTheme - let strings: PresentationStrings + let presentationData: ItemListPresentationData let id: Int let placeholder: String let value: String + let isSelected: Bool? let maxLength: Int let editing: CreatePollOptionItemEditing let sectionId: ItemListSectionId let setItemIdWithRevealedOptions: (Int?, Int?) -> Void - let updated: (String) -> Void + let updated: (String, Bool) -> Void let next: (() -> Void)? let delete: (Bool) -> Void - let focused: () -> Void + let canDelete: Bool + let canMove: Bool + let focused: (Bool) -> Void + let toggleSelected: () -> Void let tag: ItemListItemTag? - init(theme: PresentationTheme, strings: PresentationStrings, id: Int, placeholder: String, value: String, maxLength: Int, editing: CreatePollOptionItemEditing, sectionId: ItemListSectionId, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, updated: @escaping (String) -> Void, next: (() -> Void)?, delete: @escaping (Bool) -> Void, focused: @escaping () -> Void, tag: ItemListItemTag?) { - self.theme = theme - self.strings = strings + init(presentationData: ItemListPresentationData, id: Int, placeholder: String, value: String, isSelected: Bool?, maxLength: Int, editing: CreatePollOptionItemEditing, sectionId: ItemListSectionId, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, updated: @escaping (String, Bool) -> Void, next: (() -> Void)?, delete: @escaping (Bool) -> Void, canDelete: Bool, canMove: Bool, focused: @escaping (Bool) -> Void, toggleSelected: @escaping () -> Void, tag: ItemListItemTag?) { + self.presentationData = presentationData self.id = id self.placeholder = placeholder self.value = value + self.isSelected = isSelected self.maxLength = maxLength self.editing = editing self.sectionId = sectionId @@ -41,7 +45,10 @@ class CreatePollOptionItem: ListViewItem, ItemListItem { self.updated = updated self.next = next self.delete = delete + self.canDelete = canDelete + self.canMove = canMove self.focused = focused + self.toggleSelected = toggleSelected self.tag = tag } @@ -55,7 +62,7 @@ class CreatePollOptionItem: ListViewItem, ItemListItem { Queue.mainQueue().async { completion(node, { - return (nil, { _ in apply() }) + return (nil, { _ in apply(.None) }) }) } } @@ -70,7 +77,7 @@ class CreatePollOptionItem: ListViewItem, ItemListItem { let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { _ in - apply() + apply(animation) }) } } @@ -84,17 +91,19 @@ class CreatePollOptionItem: ListViewItem, ItemListItem { private let titleFont = Font.regular(15.0) class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, ItemListItemFocusableNode, ASEditableTextNodeDelegate { + private let containerNode: ASDisplayNode private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let maskNode: ASImageNode + private var checkNode: CheckNode? + private let textClippingNode: ASDisplayNode private let textNode: EditableTextNode private let measureTextNode: TextNode private let textLimitNode: TextNode - private let editableControlNode: ItemListEditableControlNode private let reorderControlNode: ItemListEditableReorderControlNode private var item: CreatePollOptionItem? @@ -104,7 +113,21 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, return self.item?.tag } + override var controlsContainer: ASDisplayNode { + return self.containerNode + } + + var checkNodeFrame: CGRect? { + guard let _ = self.layoutParams, let checkNode = self.checkNode else { + return nil + } + return checkNode.frame + } + init() { + self.containerNode = ASDisplayNode() + self.containerNode.clipsToBounds = true + self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.backgroundColor = .white @@ -117,7 +140,6 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, self.maskNode = ASImageNode() - self.editableControlNode = ItemListEditableControlNode() self.reorderControlNode = ItemListEditableReorderControlNode() self.textClippingNode = ASDisplayNode() @@ -131,21 +153,17 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) - self.clipsToBounds = true + self.addSubnode(self.containerNode) self.textClippingNode.addSubnode(self.textNode) - self.addSubnode(self.textClippingNode) + self.containerNode.addSubnode(self.textClippingNode) - self.addSubnode(self.editableControlNode) - self.addSubnode(self.reorderControlNode) - self.addSubnode(self.textLimitNode) - - self.editableControlNode.tapped = { [weak self] in - if let strongSelf = self { - strongSelf.setRevealOptionsOpened(true, animated: true) - strongSelf.revealOptionsInteractivelyOpened() - } - } + self.containerNode.addSubnode(self.reorderControlNode) + self.containerNode.addSubnode(self.textLimitNode) + } + + @objc private func checkNodePressed() { + self.item?.toggleSelected() } override func didLoad() { @@ -153,7 +171,7 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, var textColor: UIColor = .black if let item = self.item { - textColor = item.theme.list.itemPrimaryTextColor + textColor = item.presentationData.theme.list.itemPrimaryTextColor } self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: textColor] self.textNode.clipsToBounds = true @@ -162,7 +180,12 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, } func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { - self.item?.focused() + self.item?.focused(true) + } + + func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { + self.internalEditableTextNodeDidUpdateText(editableTextNode, isLosingFocus: true) + self.item?.focused(false) } func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { @@ -177,7 +200,7 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, if updatedText.count == 1 { updatedText = "" } - let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor) + let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor) self.textNode.attributedText = updatedAttributedText self.editableTextNodeDidUpdateText(editableTextNode) } @@ -192,6 +215,10 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, } func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + self.internalEditableTextNodeDidUpdateText(editableTextNode, isLosingFocus: false) + } + + private func internalEditableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode, isLosingFocus: Bool) { if let item = self.item { let text = self.textNode.attributedText ?? NSAttributedString() @@ -201,15 +228,15 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, hadReturn = true updatedText = updatedText.replacingOccurrences(of: "\n", with: " ") } - let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor) + let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor) if text.string != updatedAttributedText.string { self.textNode.attributedText = updatedAttributedText } - item.updated(updatedText) + item.updated(updatedText, !isLosingFocus && editableTextNode.isFirstResponder()) if hadReturn { if let next = item.next { next() - } else { + } else if !isLosingFocus { editableTextNode.resignFirstResponder() } } @@ -220,8 +247,7 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, self.item?.delete(editableTextNode.isFirstResponder()) } - func asyncLayout() -> (_ item: CreatePollOptionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { - let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) + func asyncLayout() -> (_ item: CreatePollOptionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) let makeTextLayout = TextNode.asyncLayout(self.measureTextNode) let makeTextLimitLayout = TextNode.asyncLayout(self.textLimitNode) @@ -231,32 +257,31 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, return { item, params, neighbors in var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } - let controlSizeAndApply = editableControlLayout(44.0, item.theme, false) - let reorderSizeAndApply = reorderControlLayout(44.0, item.theme) + let reorderSizeAndApply = reorderControlLayout(item.presentationData.theme) let separatorHeight = UIScreenPixel let insets = itemListNeighborsGroupedInsets(neighbors) - let leftInset: CGFloat = 60.0 + params.leftInset + let leftInset: CGFloat = params.leftInset + (item.isSelected != nil ? 60.0 : 16.0) let rightInset: CGFloat = 44.0 + params.rightInset let textLength = item.value.count let displayTextLimit = textLength > item.maxLength * 70 / 100 let remainingCount = item.maxLength - textLength - let (textLimitLayout, textLimitApply) = makeTextLimitLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(remainingCount)", font: Font.regular(13.0), textColor: remainingCount < 0 ? item.theme.list.itemDestructiveColor : item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: .greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + let (textLimitLayout, textLimitApply) = makeTextLimitLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(remainingCount)", font: Font.regular(13.0), textColor: remainingCount < 0 ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: .greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) var measureText = item.value if measureText.hasSuffix("\n") || measureText.isEmpty { measureText += "|" } let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), textColor: .black) - let attributedText = NSAttributedString(string: item.value, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor) + let attributedText = NSAttributedString(string: item.value, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedMeasureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, lineSpacing: 0.05, cutout: nil, insets: UIEdgeInsets())) let textTopInset: CGFloat = 11.0 @@ -265,21 +290,29 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, let contentSize = CGSize(width: params.width, height: textLayout.size.height + textTopInset + textBottomInset) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor) + let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPlaceholderTextColor) - return (layout, { [weak self] in + return (layout, { [weak self] animation in if let strongSelf = self { + let transition: ContainedViewLayoutTransition + switch animation { + case .System: + transition = .animated(duration: 0.3, curve: .spring) + default: + transition = .immediate + } + strongSelf.item = item strongSelf.layoutParams = params if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor if strongSelf.isNodeLoaded { - strongSelf.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: item.theme.list.itemPrimaryTextColor] - strongSelf.textNode.tintColor = item.theme.list.itemAccentColor + strongSelf.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: item.presentationData.theme.list.itemPrimaryTextColor] + strongSelf.textNode.tintColor = item.presentationData.theme.list.itemAccentColor } } @@ -295,7 +328,7 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, let _ = textApply() if let currentText = strongSelf.textNode.attributedText { - if currentText.string != attributedText.string { + if currentText.string != attributedText.string || updatedTheme != nil { strongSelf.textNode.attributedText = attributedText } } else { @@ -325,58 +358,93 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, strongSelf.textNode.attributedPlaceholderText = attributedPlaceholderText } - strongSelf.textNode.keyboardAppearance = item.theme.rootController.keyboardColor.keyboardAppearance + strongSelf.textNode.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance - strongSelf.textClippingNode.frame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - params.rightInset, height: textLayout.size.height)) - strongSelf.textNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width - leftInset - rightInset, height: textLayout.size.height + 1.0)) + let checkSize = CGSize(width: 32.0, height: 32.0) + let checkFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset + 11.0, y: floor((layout.contentSize.height - checkSize.height) / 2.0)), size: checkSize) + if let isSelected = item.isSelected { + if let checkNode = strongSelf.checkNode { + transition.updateFrame(node: checkNode, frame: checkFrame) + checkNode.setIsChecked(isSelected, animated: true) + } else { + let checkNode = CheckNode(strokeColor: item.presentationData.theme.list.itemCheckColors.strokeColor, fillColor: item.presentationData.theme.list.itemSwitchColors.positiveColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, style: .plain) + checkNode.addTarget(target: strongSelf, action: #selector(strongSelf.checkNodePressed)) + checkNode.setIsChecked(isSelected, animated: false) + strongSelf.checkNode = checkNode + strongSelf.containerNode.addSubnode(checkNode) + checkNode.frame = checkFrame + transition.animatePositionAdditive(node: checkNode, offset: CGPoint(x: -checkFrame.maxX, y: 0.0)) + } + } else if let checkNode = strongSelf.checkNode { + strongSelf.checkNode = nil + transition.updateFrame(node: checkNode, frame: checkFrame.offsetBy(dx: -checkFrame.maxX, dy: 0.0), completion: { [weak checkNode] _ in + checkNode?.removeFromSupernode() + }) + } + + transition.updateFrame(node: strongSelf.textClippingNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - params.rightInset, height: textLayout.size.height))) + transition.updateFrame(node: strongSelf.textNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: params.width - leftInset - rightInset, height: textLayout.size.height + 1.0))) if strongSelf.backgroundNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + strongSelf.containerNode.insertSubnode(strongSelf.backgroundNode, at: 0) } if strongSelf.topStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + strongSelf.containerNode.insertSubnode(strongSelf.topStripeNode, at: 1) } if strongSelf.bottomStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + strongSelf.containerNode.insertSubnode(strongSelf.bottomStripeNode, at: 2) } if strongSelf.maskNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + strongSelf.containerNode.insertSubnode(strongSelf.maskNode, at: 3) } + let bottomStripeWasHidden = strongSelf.bottomStripeNode.isHidden + let hasCorners = itemListHasRoundedBlockLayout(params) var hasTopCorners = false var hasBottomCorners = false switch neighbors.top { - case .sameSection(false): - strongSelf.topStripeNode.isHidden = true - default: - hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners } let bottomStripeInset: CGFloat switch neighbors.bottom { - case .sameSection(false): - bottomStripeInset = leftInset - default: - bottomStripeInset = 0.0 - hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners + case .sameSection(false): + bottomStripeInset = leftInset + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layout.contentSize.width, height: separatorHeight)) - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layout.contentSize.width - bottomStripeInset, height: separatorHeight)) + if strongSelf.animationForKey("apparentHeight") == nil { + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + let previousX = strongSelf.bottomStripeNode.frame.minX + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layout.contentSize.width, height: separatorHeight)) + if !bottomStripeWasHidden { + transition.animatePositionAdditive(node: strongSelf.bottomStripeNode, offset: CGPoint(x: previousX - strongSelf.bottomStripeNode.frame.minX, y: 0.0)) + } + } else { + let previousX = strongSelf.bottomStripeNode.frame.minX + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: strongSelf.bottomStripeNode.frame.minY), size: CGSize(width: layout.contentSize.width, height: separatorHeight)) + if !bottomStripeWasHidden { + transition.animatePositionAdditive(node: strongSelf.bottomStripeNode, offset: CGPoint(x: previousX - strongSelf.bottomStripeNode.frame.minX, y: 0.0)) + } + } - let _ = controlSizeAndApply.1() - let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + 6.0 + revealOffset, y: 0.0), size: controlSizeAndApply.0) - strongSelf.editableControlNode.frame = editableControlFrame - - let _ = reorderSizeAndApply.1(displayTextLimit && layout.contentSize.height <= 44.0) - let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderSizeAndApply.0.width, y: 0.0), size: reorderSizeAndApply.0) + let _ = reorderSizeAndApply.1(layout.contentSize.height, displayTextLimit, transition) + let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderSizeAndApply.0, y: 0.0), size: CGSize(width: reorderSizeAndApply.0, height: layout.contentSize.height)) strongSelf.reorderControlNode.frame = reorderControlFrame + strongSelf.reorderControlNode.isHidden = !item.canMove let _ = textLimitApply() strongSelf.textLimitNode.frame = CGRect(origin: CGPoint(x: reorderControlFrame.minX + floor((reorderControlFrame.width - textLimitLayout.size.width) / 2.0) - 4.0 - UIScreenPixel, y: max(floor(reorderControlFrame.midY + 2.0), layout.contentSize.height - 15.0 - textLimitLayout.size.height)), size: textLimitLayout.size) @@ -384,7 +452,7 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + strongSelf.setRevealOptions((left: [], right: item.canDelete ? [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] : [])) } }) } @@ -393,18 +461,20 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) - guard let params = self.layoutParams else { + guard let params = self.layoutParams, let item = self.item else { return } let revealOffset = offset let leftInset: CGFloat - leftInset = 60.0 + params.leftInset + leftInset = params.leftInset + (item.isSelected != nil ? 60.0 : 16.0) - var controlFrame = self.editableControlNode.frame - controlFrame.origin.x = params.leftInset + 6.0 + revealOffset - transition.updateFrame(node: self.editableControlNode, frame: controlFrame) + if let checkNode = self.checkNode { + var checkNodeFrame = checkNode.frame + checkNodeFrame.origin.x = params.leftInset + 11.0 + revealOffset + transition.updateFrame(node: checkNode, frame: checkNodeFrame) + } var reorderFrame = self.reorderControlNode.frame reorderFrame.origin.x = params.width + revealOffset - params.rightInset - reorderFrame.width @@ -436,7 +506,7 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, } override func isReorderable(at point: CGPoint) -> Bool { - if self.reorderControlNode.frame.contains(point), !self.isDisplayingRevealedOptions { + if self.reorderControlNode.frame.contains(point), !self.reorderControlNode.isHidden, !self.isDisplayingRevealedOptions { return true } return false @@ -448,5 +518,16 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, var separatorFrame = self.bottomStripeNode.frame separatorFrame.origin.y = currentValue - UIScreenPixel self.bottomStripeNode.frame = separatorFrame + + self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.containerNode.bounds.width, height: currentValue)) + + let insets = self.insets + let separatorHeight = UIScreenPixel + guard let params = self.layoutParams else { + return + } + + self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: self.containerNode.bounds.width, height: currentValue + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + self.maskNode.frame = self.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) } } diff --git a/submodules/ContactListUI/BUCK b/submodules/ContactListUI/BUCK index d16c21b35c..6a6691d744 100644 --- a/submodules/ContactListUI/BUCK +++ b/submodules/ContactListUI/BUCK @@ -29,6 +29,7 @@ static_library( "//submodules/AppBundle:AppBundle", "//submodules/OverlayStatusController:OverlayStatusController", "//submodules/PhoneNumberFormat:PhoneNumberFormat", + "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/ContactListUI/Sources/ContactListActionItem.swift b/submodules/ContactListUI/Sources/ContactListActionItem.swift index 27e30043b4..34e838e162 100644 --- a/submodules/ContactListUI/Sources/ContactListActionItem.swift +++ b/submodules/ContactListUI/Sources/ContactListActionItem.swift @@ -15,7 +15,7 @@ public enum ContactListActionItemHighlight { } class ContactListActionItem: ListViewItem, ListViewItemWithHeader { - let theme: PresentationTheme + let presentationData: ItemListPresentationData let title: String let icon: ContactListActionItemIcon let highlight: ContactListActionItemHighlight @@ -23,8 +23,8 @@ class ContactListActionItem: ListViewItem, ListViewItemWithHeader { let action: () -> Void let header: ListViewItemHeader? - init(theme: PresentationTheme, title: String, icon: ContactListActionItemIcon, highlight: ContactListActionItemHighlight = .cell, clearHighlightAutomatically: Bool = true, header: ListViewItemHeader?, action: @escaping () -> Void) { - self.theme = theme + init(presentationData: ItemListPresentationData, title: String, icon: ContactListActionItemIcon, highlight: ContactListActionItemHighlight = .cell, clearHighlightAutomatically: Bool = true, header: ListViewItemHeader?, action: @escaping () -> Void) { + self.presentationData = presentationData self.title = title self.icon = icon self.highlight = highlight @@ -110,8 +110,6 @@ class ContactListActionItem: ListViewItem, ListViewItemWithHeader { } } -private let titleFont = Font.regular(17.0) - class ContactListActionItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -123,8 +121,6 @@ class ContactListActionItemNode: ListViewItemNode { private let activateArea: AccessibilityAreaNode - private var theme: PresentationTheme? - private var item: ContactListActionItem? init() { @@ -167,23 +163,27 @@ class ContactListActionItemNode: ListViewItemNode { func asyncLayout() -> (_ item: ContactListActionItem, _ params: ListViewItemLayoutParams, _ firstWithHeader: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - let currentTheme = self.theme + let currentItem = self.item return { item, params, firstWithHeader, last in var updatedTheme: PresentationTheme? - if currentTheme !== item.theme { - updatedTheme = item.theme + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + var leftInset: CGFloat = 16.0 + params.leftInset if case .generic = item.icon { leftInset += 49.0 } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - 10.0 - leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - 10.0 - leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let contentSize = CGSize(width: params.width, height: 50.0) + let contentHeight: CGFloat = item.highlight == .alpha ? 50.0 : 12.0 * 2.0 + titleLayout.size.height + + let contentSize = CGSize(width: params.width, height: contentHeight) let insets = UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0) let separatorHeight = UIScreenPixel @@ -192,18 +192,17 @@ class ContactListActionItemNode: ListViewItemNode { return (layout, { [weak self] in if let strongSelf = self { strongSelf.item = item - strongSelf.theme = item.theme strongSelf.activateArea.accessibilityLabel = item.title strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: layout.contentSize.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor - strongSelf.iconNode.image = generateTintedImage(image: item.icon.image, color: item.theme.list.itemAccentColor) + strongSelf.iconNode.image = generateTintedImage(image: item.icon.image, color: item.presentationData.theme.list.itemAccentColor) } let _ = titleApply() @@ -218,12 +217,12 @@ class ContactListActionItemNode: ListViewItemNode { let iconSpacing: CGFloat = 4.0 let totalWidth: CGFloat = titleLayout.size.width + image.size.width + iconSpacing switch position { - case .left: - iconFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((contentSize.width - params.leftInset - params.rightInset - totalWidth) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) - titleOffset = iconFrame.minX + iconSpacing - case .right: - iconFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((contentSize.width - params.leftInset - params.rightInset - totalWidth) / 2.0) + totalWidth - image.size.width, y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) - titleOffset = iconFrame.maxX - totalWidth + case .left: + iconFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((contentSize.width - params.leftInset - params.rightInset - totalWidth) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) + titleOffset = iconFrame.minX + iconSpacing + case .right: + iconFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((contentSize.width - params.leftInset - params.rightInset - totalWidth) / 2.0) + totalWidth - image.size.width, y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) + titleOffset = iconFrame.maxX - totalWidth } default: iconFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0) + 3.0, y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) @@ -250,9 +249,7 @@ class ContactListActionItemNode: ListViewItemNode { strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: titleOffset, y: floor((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 50.0 + UIScreenPixel + UIScreenPixel)) - - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 50.0 + UIScreenPixel + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel)) } }) } diff --git a/submodules/ContactListUI/Sources/ContactListNameIndexHeader.swift b/submodules/ContactListUI/Sources/ContactListNameIndexHeader.swift index 69da8ed11c..788c81b52d 100644 --- a/submodules/ContactListUI/Sources/ContactListNameIndexHeader.swift +++ b/submodules/ContactListUI/Sources/ContactListNameIndexHeader.swift @@ -22,6 +22,10 @@ final class ContactListNameIndexHeader: Equatable, ListViewItemHeader { return ContactListNameIndexHeaderNode(theme: self.theme, letter: self.letter) } + func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) { + + } + static func ==(lhs: ContactListNameIndexHeader, rhs: ContactListNameIndexHeader) -> Bool { return lhs.id == rhs.id } diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index fd789bc2dd..30be08fba4 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -152,9 +152,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } } - - - func item(account: Account, interaction: ContactListNodeInteraction) -> ListViewItem { + func item(context: AccountContext, presentationData: PresentationData, interaction: ContactListNodeInteraction) -> ListViewItem { switch self { case let .search(theme, strings): return ChatListSearchItem(theme: theme, placeholder: strings.Contacts_SearchLabel, activate: { @@ -165,19 +163,19 @@ private enum ContactListNodeEntry: Comparable, Identifiable { if case .presence = sortOrder { text = strings.Contacts_SortedByPresence } - return ContactListActionItem(theme: theme, title: text, icon: .inline(dropDownIcon, .right), highlight: .alpha, header: nil, action: { + return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .inline(dropDownIcon, .right), highlight: .alpha, header: nil, action: { interaction.openSortMenu() }) case let .permissionInfo(theme, title, text, suppressed): - return InfoListItem(theme: theme, title: title, text: .plain(text), style: .plain, closeAction: suppressed ? nil : { + return InfoListItem(presentationData: ItemListPresentationData(presentationData), title: title, text: .plain(text), style: .plain, closeAction: suppressed ? nil : { interaction.suppressWarning() }) case let .permissionEnable(theme, text): - return ContactListActionItem(theme: theme, title: text, icon: .none, header: nil, action: { + return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .none, header: nil, action: { interaction.authorize() }) case let .option(_, option, header, theme, _): - return ContactListActionItem(theme: theme, title: option.title, icon: option.icon, clearHighlightAutomatically: false, header: header, action: option.action) + return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, clearHighlightAutomatically: false, header: header, action: option.action) case let .peer(_, peer, presence, header, selection, theme, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, enabled): let status: ContactsPeerItemStatus let itemPeer: ContactsPeerItemPeer @@ -221,7 +219,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } } } - return ContactsPeerItem(theme: theme, strings: strings, sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, account: account, peerMode: .peer, peer: itemPeer, status: status, enabled: enabled, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: itemPeer, status: status, enabled: enabled, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in interaction.openPeer(peer) }, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction) } @@ -602,12 +600,12 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer] return entries } -private func preparedContactListNodeTransition(account: Account, from fromEntries: [ContactListNodeEntry], to toEntries: [ContactListNodeEntry], interaction: ContactListNodeInteraction, firstTime: Bool, isEmpty: Bool, generateIndexSections: Bool, animation: ContactListAnimation) -> ContactsListNodeTransition { +private func preparedContactListNodeTransition(context: AccountContext, presentationData: PresentationData, from fromEntries: [ContactListNodeEntry], to toEntries: [ContactListNodeEntry], interaction: ContactListNodeInteraction, firstTime: Bool, isEmpty: Bool, generateIndexSections: Bool, animation: ContactListAnimation) -> ContactsListNodeTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } var shouldFixScroll = false var indexSections: [String] = [] @@ -772,7 +770,7 @@ public final class ContactListNode: ASDisplayNode { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool)> + private let presentationDataPromise: Promise private var authorizationNode: PermissionContentNode private let displayPermissionPlaceholder: Bool @@ -790,7 +788,7 @@ public final class ContactListNode: ASDisplayNode { self.indexNode = CollectionIndexNode() - self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings, self.presentationData.dateTimeFormat, self.presentationData.nameSortOrder, self.presentationData.nameDisplayOrder, self.presentationData.disableAnimations)) + self.presentationDataPromise = Promise(self.presentationData) let contactsAuthorization = Promise() contactsAuthorization.set(.single(.allowed) @@ -900,7 +898,7 @@ public final class ContactListNode: ASDisplayNode { var firstTime: Int32 = 1 let selectionStateSignal = self.selectionStatePromise.get() let transition: Signal - let themeAndStringsPromise = self.themeAndStringsPromise + let presentationDataPromise = self.presentationDataPromise transition = presentation |> mapToSignal { presentation in @@ -980,8 +978,8 @@ public final class ContactListNode: ASDisplayNode { foundDeviceContacts = .single([:]) } - return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, selectionStateSignal, themeAndStringsPromise.get()) - |> mapToQueue { localPeersAndStatuses, remotePeers, deviceContacts, selectionState, themeAndStrings -> Signal in + return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, selectionStateSignal, presentationDataPromise.get()) + |> mapToQueue { localPeersAndStatuses, remotePeers, deviceContacts, selectionState, presentationData -> Signal in let signal = deferred { () -> Signal in var existingPeerIds = Set() var disabledPeerIds = Set() @@ -1084,9 +1082,9 @@ public final class ContactListNode: ASDisplayNode { peers.append(.deviceContact(stableId, contact.0)) } - let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1, dateTimeFormat: themeAndStrings.2, sortOrder: themeAndStrings.3, displayOrder: themeAndStrings.4, disabledPeerIds: disabledPeerIds, authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false) + let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false) let previous = previousEntries.swap(entries) - return .single(preparedContactListNodeTransition(account: context.account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: false, generateIndexSections: generateSections, animation: .none)) + return .single(preparedContactListNodeTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: false, generateIndexSections: generateSections, animation: .none)) } if OSAtomicCompareAndSwap32(1, 0, &firstTime) { @@ -1129,8 +1127,8 @@ public final class ContactListNode: ASDisplayNode { chatListSignal = .single([]) } - return (combineLatest(self.contactPeersViewPromise.get(), chatListSignal, selectionStateSignal, themeAndStringsPromise.get(), contactsAuthorization.get(), contactsWarningSuppressed.get()) - |> mapToQueue { view, chatListPeers, selectionState, themeAndStrings, authorizationStatus, warningSuppressed -> Signal in + return (combineLatest(self.contactPeersViewPromise.get(), chatListSignal, selectionStateSignal, presentationDataPromise.get(), contactsAuthorization.get(), contactsWarningSuppressed.get()) + |> mapToQueue { view, chatListPeers, selectionState, presentationData, authorizationStatus, warningSuppressed -> Signal in let signal = deferred { () -> Signal in var peers = view.peers.map({ ContactListPeer.peer(peer: $0, isGlobal: false, participantCount: nil) }) for (peer, memberCount) in chatListPeers { @@ -1162,7 +1160,7 @@ public final class ContactListNode: ASDisplayNode { if (authorizationStatus == .notDetermined || authorizationStatus == .denied) && peers.isEmpty { isEmpty = true } - let entries = contactListNodeEntries(accountPeer: view.accountPeer, peers: peers, presences: view.peerPresences, presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1, dateTimeFormat: themeAndStrings.2, sortOrder: themeAndStrings.3, displayOrder: themeAndStrings.4, disabledPeerIds: disabledPeerIds, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions) + let entries = contactListNodeEntries(accountPeer: view.accountPeer, peers: peers, presences: view.peerPresences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions) let previous = previousEntries.swap(entries) var hadPermissionInfo = false @@ -1186,13 +1184,13 @@ public final class ContactListNode: ASDisplayNode { if hadPermissionInfo != hasPermissionInfo { animation = .insertion } - else if let previous = previous, !themeAndStrings.5, (entries.count - previous.count) < 20 { + else if let previous = previous, !presentationData.disableAnimations, (entries.count - previous.count) < 20 { animation = .default } else { animation = .none } - return .single(preparedContactListNodeTransition(account: context.account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: isEmpty, generateIndexSections: generateSections, animation: animation)) + return .single(preparedContactListNodeTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: isEmpty, generateIndexSections: generateSections, animation: animation)) } if OSAtomicCompareAndSwap32(1, 0, &firstTime) { @@ -1221,7 +1219,7 @@ public final class ContactListNode: ASDisplayNode { if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings || previousDisableAnimations != presentationData.disableAnimations { strongSelf.backgroundColor = presentationData.theme.chatList.backgroundColor strongSelf.listNode.verticalScrollIndicatorColor = presentationData.theme.list.scrollIndicatorColor - strongSelf.themeAndStringsPromise.set(.single((presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameSortOrder, presentationData.nameDisplayOrder, presentationData.disableAnimations))) + strongSelf.presentationDataPromise.set(.single(presentationData)) let authorizationPreviousHidden = strongSelf.authorizationNode.isHidden strongSelf.authorizationNode.removeFromSupernode() @@ -1327,32 +1325,10 @@ public final class ContactListNode: ASDisplayNode { headerInsets.top -= navigationBarSearchContentHeight } - self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) - self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + transition.updateFrame(node: self.listNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: listViewCurve) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: curve) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if let indexSections = self.indexSections { @@ -1436,6 +1412,6 @@ public final class ContactListNode: ASDisplayNode { } public func scrollToTop() { - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(-50.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } } diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 4d8f4696e5..c1151b630f 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -105,7 +105,12 @@ public class ContactsController: ViewController { self.title = self.presentationData.strings.Contacts_Title self.tabBarItem.title = self.presentationData.strings.Contacts_Title - let icon = UIImage(bundleImageName: "Chat List/Tabs/IconContacts") + let icon: UIImage? + if useSpecialTabBarIcons() { + icon = UIImage(bundleImageName: "Chat List/Tabs/Holiday/IconContacts") + } else { + icon = UIImage(bundleImageName: "Chat List/Tabs/IconContacts") + } self.tabBarItem.image = icon self.tabBarItem.selectedImage = icon @@ -495,7 +500,7 @@ public class ContactsController: ViewController { } } - let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: self.presentationData.strings.Contacts_SortBy)) items.append(ActionSheetButtonItem(title: self.presentationData.strings.Contacts_SortByName, color: .accent, action: { [weak actionSheet] in @@ -507,7 +512,7 @@ public class ContactsController: ViewController { updateSortOrder(.presence) })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -524,13 +529,13 @@ public class ContactsController: ViewController { switch status { case .allowed: - let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: "", lastName: "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: "+")]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: []) + let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: "", lastName: "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: "+")]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") (strongSelf.navigationController as? NavigationController)?.pushViewController(strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .create(peer: nil, contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in guard let strongSelf = self else { return } if let peer = peer { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) } } else { diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index ee7fcb6e3f..d994543130 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -193,7 +193,7 @@ final class ContactsControllerNode: ASDisplayNode { } let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) - let contextController = ContextController(account: self.context.account, theme: self.presentationData.theme, strings: self.presentationData.strings, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: contactContextMenuItems(context: self.context, peerId: peer.id, contactsController: contactsController), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: contactContextMenuItems(context: self.context, peerId: peer.id, contactsController: contactsController), reactionItems: [], gesture: gesture) contactsController.presentInGlobalOverlay(contextController) } diff --git a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift index 8a23d9bdaf..7371480558 100644 --- a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift @@ -15,6 +15,7 @@ import ChatListSearchItemHeader import ContactsPeerItem import ContextUI import PhoneNumberFormat +import ItemListUI private enum ContactListSearchGroup { case contacts @@ -68,7 +69,7 @@ private struct ContactListSearchEntry: Identifiable, Comparable { return lhs.index < rhs.index } - func item(account: Account, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, openPeer: @escaping (ContactListPeer) -> Void, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem { + func item(context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, openPeer: @escaping (ContactListPeer) -> Void, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem { let header: ListViewItemHeader let status: ContactsPeerItemStatus switch self.group { @@ -100,7 +101,7 @@ private struct ContactListSearchEntry: Identifiable, Comparable { case let .deviceContact(stableId, contact): peerItem = .deviceContact(stableId: stableId, contact: contact) } - return ContactsPeerItem(theme: self.theme, strings: self.strings, sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, account: account, peerMode: .peer, peer: peerItem, status: status, enabled: self.enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: peerItem, status: status, enabled: self.enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in openPeer(peer) }, contextAction: contextAction.flatMap { contextAction in return nativePeer.flatMap { nativePeer in @@ -119,12 +120,12 @@ struct ContactListSearchContainerTransition { let isSearching: Bool } -private func contactListSearchContainerPreparedRecentTransition(from fromEntries: [ContactListSearchEntry], to toEntries: [ContactListSearchEntry], isSearching: Bool, account: Account, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, openPeer: @escaping (ContactListPeer) -> Void, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?) -> ContactListSearchContainerTransition { +private func contactListSearchContainerPreparedRecentTransition(from fromEntries: [ContactListSearchEntry], to toEntries: [ContactListSearchEntry], isSearching: Bool, context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, openPeer: @escaping (ContactListPeer) -> Void, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?) -> ContactListSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, timeFormat: timeFormat, openPeer: openPeer, contextAction: contextAction), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, timeFormat: timeFormat, openPeer: openPeer, contextAction: contextAction), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, timeFormat: timeFormat, openPeer: openPeer, contextAction: contextAction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, timeFormat: timeFormat, openPeer: openPeer, contextAction: contextAction), directionHint: nil) } return ContactListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) } @@ -323,7 +324,7 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo if let strongSelf = self { let previousItems = previousSearchItems.swap(items ?? []) - let transition = contactListSearchContainerPreparedRecentTransition(from: previousItems, to: items ?? [], isSearching: items != nil, account: context.account, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, timeFormat: strongSelf.presentationData.dateTimeFormat, openPeer: { peer in self?.listNode.clearHighlightAnimated(true) + let transition = contactListSearchContainerPreparedRecentTransition(from: previousItems, to: items ?? [], isSearching: items != nil, context: context, presentationData: strongSelf.presentationData, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, timeFormat: strongSelf.presentationData.dateTimeFormat, openPeer: { peer in self?.listNode.clearHighlightAnimated(true) self?.openPeer(peer) }, contextAction: strongSelf.contextAction) diff --git a/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift b/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift index 59d791b2cb..4b0c111e6b 100644 --- a/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift @@ -17,6 +17,7 @@ import ContactsPeerItem import ChatListSearchItemHeader import AppBundle import PhoneNumberFormat +import ItemListUI private enum InviteContactsEntryId: Hashable { case option(index: Int) @@ -46,10 +47,10 @@ private enum InviteContactsEntry: Comparable, Identifiable { } } - func item(account: Account, interaction: InviteContactsInteraction) -> ListViewItem { + func item(context: AccountContext, presentationData: PresentationData, interaction: InviteContactsInteraction) -> ListViewItem { switch self { case let .option(_, option, theme, _): - return ContactListActionItem(theme: theme, title: option.title, icon: option.icon, header: nil, action: option.action) + return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, header: nil, action: option.action) case let .peer(_, id, contact, count, selection, theme, strings, nameSortOrder, nameDisplayOrder): let status: ContactsPeerItemStatus if count != 0 { @@ -58,7 +59,7 @@ private enum InviteContactsEntry: Comparable, Identifiable { status = .none } let peer = TelegramUser(id: PeerId(namespace: -1, id: 0), accessHash: nil, firstName: contact.firstName, lastName: contact.lastName, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) - return ContactsPeerItem(theme: theme, strings: strings, sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, account: account, peerMode: .peer, peer: .peer(peer: peer, chatPeer: peer), status: status, enabled: true, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: peer, chatPeer: peer), status: status, enabled: true, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in interaction.toggleContact(id) }) } @@ -201,12 +202,12 @@ private func inviteContactsEntries(accountPeer: Peer?, sortedContacts: [(DeviceC return entries } -private func preparedInviteContactsTransition(account: Account, from fromEntries: [InviteContactsEntry], to toEntries: [InviteContactsEntry], sortedContacts: [(DeviceContactStableId, DeviceContactBasicData, Int32)]?, interaction: InviteContactsInteraction, isLoading: Bool, firstTime: Bool, crossfade: Bool) -> InviteContactsTransition { +private func preparedInviteContactsTransition(context: AccountContext, presentationData: PresentationData, from fromEntries: [InviteContactsEntry], to toEntries: [InviteContactsEntry], sortedContacts: [(DeviceContactStableId, DeviceContactBasicData, Int32)]?, interaction: InviteContactsInteraction, isLoading: Bool, firstTime: Bool, crossfade: Bool) -> InviteContactsTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } return InviteContactsTransition(deletions: deletions, insertions: insertions, updates: updates, sortedContacts: sortedContacts, isLoading: isLoading, firstTime: firstTime, crossfade: crossfade) } @@ -246,7 +247,7 @@ final class InviteContactsControllerNode: ASDisplayNode { didSet { if self.selectionState != oldValue { self.selectionStatePromise.set(.single(self.selectionState)) - self.countPanelNode.badge = "\(self.selectionState.selectedContactIndices.count)" + self.countPanelNode.count = self.selectionState.selectedContactIndices.count if oldValue.selectedContactIndices.isEmpty != self.selectionState.selectedContactIndices.isEmpty { if let (layout, navigationHeight, actualNavigationHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, actualNavigationBarHeight: actualNavigationHeight, transition: .animated(duration: 0.3, curve: .spring)) @@ -263,7 +264,7 @@ final class InviteContactsControllerNode: ASDisplayNode { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder)> + private let presentationDataPromise: Promise private let _ready = Promise() private var readyValue = false { @@ -288,7 +289,7 @@ final class InviteContactsControllerNode: ASDisplayNode { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings, self.presentationData.nameSortOrder, self.presentationData.nameDisplayOrder)) + self.presentationDataPromise = Promise(self.presentationData) self.listNode = ListView() @@ -316,7 +317,7 @@ final class InviteContactsControllerNode: ASDisplayNode { let previousStrings = strongSelf.presentationData.strings strongSelf.presentationData = presentationData - strongSelf.themeAndStringsPromise.set(.single((presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder))) + strongSelf.presentationDataPromise.set(.single(presentationData)) if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { strongSelf.updateThemeAndStrings() @@ -327,7 +328,7 @@ final class InviteContactsControllerNode: ASDisplayNode { let account = self.context.account let selectionStateSignal = self.selectionStatePromise.get() let transition: Signal - let themeAndStringsPromise = self.themeAndStringsPromise + let presentationDataPromise = self.presentationDataPromise let previousEntries = Atomic<[InviteContactsEntry]?>(value: nil) let interaction = InviteContactsInteraction(toggleContact: { [weak self] id in @@ -403,19 +404,19 @@ final class InviteContactsControllerNode: ASDisplayNode { } let processingQueue = Queue() - transition = (combineLatest(.single(nil) |> then(sortedContacts), selectionStateSignal, themeAndStringsPromise.get(), .single(true) |> delay(0.2, queue: Queue.mainQueue())) - |> mapToQueue { sortedContacts, selectionState, themeAndStrings, ready -> Signal in + transition = (combineLatest(.single(nil) |> then(sortedContacts), selectionStateSignal, presentationDataPromise.get(), .single(true) |> delay(0.2, queue: Queue.mainQueue())) + |> mapToQueue { sortedContacts, selectionState, presentationData, ready -> Signal in guard sortedContacts != nil || ready else { return .never() } let signal = deferred { () -> Signal in - let entries = inviteContactsEntries(accountPeer: nil, sortedContacts: sortedContacts, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1, nameSortOrder: themeAndStrings.2, nameDisplayOrder: themeAndStrings.3, interaction: interaction) + let entries = inviteContactsEntries(accountPeer: nil, sortedContacts: sortedContacts, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, interaction: interaction) let previous = previousEntries.swap(entries) let previousContacts = currentSortedContacts.with { $0 } let crossfade = previous != nil && previousContacts == nil - return .single(preparedInviteContactsTransition(account: context.account, from: previous ?? [], to: entries, sortedContacts: sortedContacts, interaction: interaction, isLoading: sortedContacts == nil, firstTime: previous == nil, crossfade: crossfade)) + return .single(preparedInviteContactsTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, sortedContacts: sortedContacts, interaction: interaction, isLoading: sortedContacts == nil, firstTime: previous == nil, crossfade: crossfade)) } return signal |> runOn(processingQueue) @@ -484,29 +485,8 @@ final class InviteContactsControllerNode: ASDisplayNode { self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: nil) - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: listViewCurve) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: curve) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) diff --git a/submodules/ContactListUI/Sources/InviteContactsCountPanelNode.swift b/submodules/ContactListUI/Sources/InviteContactsCountPanelNode.swift index be9ee6247c..0e74287d99 100644 --- a/submodules/ContactListUI/Sources/InviteContactsCountPanelNode.swift +++ b/submodules/ContactListUI/Sources/InviteContactsCountPanelNode.swift @@ -3,25 +3,21 @@ import UIKit import AsyncDisplayKit import Display import TelegramPresentationData +import SolidRoundedButtonNode final class InviteContactsCountPanelNode: ASDisplayNode { private let theme: PresentationTheme - private let action: () -> Void + private let strings: PresentationStrings private let separatorNode: ASDisplayNode - private let labelNode: ImmediateTextNode - private let badgeLabel: ImmediateTextNode - private let badgeBackground: ASImageNode - private let buttonNode: HighlightableButtonNode + private let button: SolidRoundedButtonNode private var validLayout: (CGFloat, CGFloat)? - var badge: String? { + var count: Int = 0 { didSet { - if self.badge != oldValue { - if let badge = self.badge { - self.badgeLabel.attributedText = NSAttributedString(string: badge, font: Font.regular(14.0), textColor: self.theme.rootController.navigationBar.badgeTextColor, paragraphAlignment: .center) - } + if self.count != oldValue && self.count > 0 { + self.button.title = self.strings.Contacts_InviteContacts(Int32(self.count)) if let (width, bottomInset) = self.validLayout { let _ = self.updateLayout(width: width, bottomInset: bottomInset, transition: .immediate) @@ -32,74 +28,38 @@ final class InviteContactsCountPanelNode: ASDisplayNode { init(theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void) { self.theme = theme - self.action = action - + self.strings = strings + self.separatorNode = ASDisplayNode() self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor - self.labelNode = ImmediateTextNode() - self.badgeLabel = ImmediateTextNode() - - self.badgeBackground = ASImageNode() - self.badgeBackground.isLayerBacked = true - self.badgeBackground.displaysAsynchronously = false - self.badgeBackground.displayWithoutProcessing = true - - self.badgeBackground.image = generateStretchableFilledCircleImage(diameter: 22.0, color: theme.rootController.navigationBar.accentTextColor) - - self.buttonNode = HighlightableButtonNode() + self.button = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), height: 48.0, cornerRadius: 10.0) super.init() self.backgroundColor = theme.rootController.navigationBar.backgroundColor - self.addSubnode(self.labelNode) - self.labelNode.attributedText = NSAttributedString(string: strings.Contacts_InviteToTelegram, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) - - self.addSubnode(self.badgeBackground) - self.addSubnode(self.badgeLabel) + self.addSubnode(self.button) self.addSubnode(self.separatorNode) - self.addSubnode(self.buttonNode) - self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) - self.buttonNode.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.labelNode.layer.removeAnimation(forKey: "opacity") - strongSelf.labelNode.alpha = 0.4 - } else { - strongSelf.labelNode.alpha = 1.0 - strongSelf.labelNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } + self.button.pressed = { + action() } } func updateLayout(width: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { self.validLayout = (width, bottomInset) + let topInset: CGFloat = 9.0 + var bottomInset = bottomInset + bottomInset += topInset - (bottomInset.isZero ? 0.0 : 4.0) - let panelHeight: CGFloat = bottomInset + 44.0 - - let titleSize = self.labelNode.updateLayout(CGSize(width: width, height: 100.0)) - let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: floor((44.0 - titleSize.height) / 2.0)), size: titleSize) - transition.updateFrame(node: self.labelNode, frame: titleFrame) - - let badgeSize = self.badgeLabel.updateLayout(CGSize(width: 100.0, height: 100.0)) - - let backgroundSize = CGSize(width: max(22.0, badgeSize.width + 10.0 + 1.0), height: 22.0) - let backgroundFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 6.0, y: 11.0), size: backgroundSize) - - self.badgeBackground.frame = backgroundFrame - self.badgeLabel.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeSize.width / 2.0), y: backgroundFrame.minY + 3.0), size: badgeSize) + let buttonInset: CGFloat = 16.0 + let buttonWidth = width - buttonInset * 2.0 + let buttonHeight = self.button.updateLayout(width: buttonWidth, transition: transition) + transition.updateFrame(node: self.button, frame: CGRect(x: buttonInset, y: topInset, width: buttonWidth, height: buttonHeight)) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) - self.buttonNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 44.0)) - - return panelHeight - } - - @objc func buttonPressed() { - self.action() + return topInset + buttonHeight + bottomInset } } diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 98c6650255..183287730b 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -17,6 +17,7 @@ import AccountContext import PeerPresenceStatusManager import ItemListPeerItem import ContextUI +import AccountContext public final class ContactItemHighlighting { public var chatLocation: ChatLocation? @@ -27,11 +28,6 @@ public final class ContactItemHighlighting { } } -private let titleFont = Font.regular(17.0) -private let titleBoldFont = Font.medium(17.0) -private let statusFont = Font.regular(13.0) -private let badgeFont = Font.regular(14.0) - public enum ContactsPeerItemStatus { case none case presence(PeerPresence, PresentationDateTimeFormat) @@ -109,12 +105,13 @@ public enum ContactsPeerItemPeer: Equatable { } } -public class ContactsPeerItem: ListViewItem, ListViewItemWithHeader { - let theme: PresentationTheme - let strings: PresentationStrings +public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { + let presentationData: ItemListPresentationData + let style: ItemListStyle + public let sectionId: ItemListSectionId let sortOrder: PresentationPersonNameOrder let displayOrder: PresentationPersonNameOrder - let account: Account + let context: AccountContext let peerMode: ContactsPeerItemPeerMode public let peer: ContactsPeerItemPeer let status: ContactsPeerItemStatus @@ -125,6 +122,7 @@ public class ContactsPeerItem: ListViewItem, ListViewItemWithHeader { let options: [ItemListPeerItemRevealOption] let actionIcon: ContactsPeerItemActionIcon let action: (ContactsPeerItemPeer) -> Void + let disabledAction: ((ContactsPeerItemPeer) -> Void)? let setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? let deletePeer: ((PeerId) -> Void)? let itemHighlighting: ContactItemHighlighting? @@ -136,12 +134,13 @@ public class ContactsPeerItem: ListViewItem, ListViewItemWithHeader { public let header: ListViewItemHeader? - public init(theme: PresentationTheme, strings: PresentationStrings, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, account: Account, peerMode: ContactsPeerItemPeerMode, peer: ContactsPeerItemPeer, status: ContactsPeerItemStatus, badge: ContactsPeerItemBadge? = nil, enabled: Bool, selection: ContactsPeerItemSelection, editing: ContactsPeerItemEditing, options: [ItemListPeerItemRevealOption] = [], actionIcon: ContactsPeerItemActionIcon = .none, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (ContactsPeerItemPeer) -> Void, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil, itemHighlighting: ContactItemHighlighting? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil) { - self.theme = theme - self.strings = strings + public init(presentationData: ItemListPresentationData, style: ItemListStyle = .plain, sectionId: ItemListSectionId = 0, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, context: AccountContext, peerMode: ContactsPeerItemPeerMode, peer: ContactsPeerItemPeer, status: ContactsPeerItemStatus, badge: ContactsPeerItemBadge? = nil, enabled: Bool, selection: ContactsPeerItemSelection, editing: ContactsPeerItemEditing, options: [ItemListPeerItemRevealOption] = [], actionIcon: ContactsPeerItemActionIcon = .none, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (ContactsPeerItemPeer) -> Void, disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil, itemHighlighting: ContactItemHighlighting? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil) { + self.presentationData = presentationData + self.style = style + self.sectionId = sectionId self.sortOrder = sortOrder self.displayOrder = displayOrder - self.account = account + self.context = context self.peerMode = peerMode self.peer = peer self.status = status @@ -152,11 +151,12 @@ public class ContactsPeerItem: ListViewItem, ListViewItemWithHeader { self.options = options self.actionIcon = actionIcon self.action = action + self.disabledAction = disabledAction self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.deletePeer = deletePeer self.header = header self.itemHighlighting = itemHighlighting - self.selectable = enabled + self.selectable = enabled || disabledAction != nil self.contextAction = contextAction if let index = index { @@ -203,7 +203,7 @@ public class ContactsPeerItem: ListViewItem, ListViewItemWithHeader { } } } - self.headerAccessoryItem = ContactsSectionHeaderAccessoryItem(sectionHeader: .letter(letter), theme: theme) + self.headerAccessoryItem = ContactsSectionHeaderAccessoryItem(sectionHeader: .letter(letter), theme: presentationData.theme) } else { self.headerAccessoryItem = nil } @@ -214,7 +214,7 @@ public class ContactsPeerItem: ListViewItem, ListViewItemWithHeader { let node = ContactsPeerItemNode() let makeLayout = node.asyncLayout() let (first, last, firstWithHeader) = ContactsPeerItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem) - let (nodeLayout, nodeApply) = makeLayout(self, params, first, last, firstWithHeader) + let (nodeLayout, nodeApply) = makeLayout(self, params, first, last, firstWithHeader, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = nodeLayout.contentSize node.insets = nodeLayout.insets @@ -235,7 +235,7 @@ public class ContactsPeerItem: ListViewItem, ListViewItemWithHeader { let layout = nodeValue.asyncLayout() async { let (first, last, firstWithHeader) = ContactsPeerItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem) - let (nodeLayout, apply) = layout(self, params, first, last, firstWithHeader) + let (nodeLayout, apply) = layout(self, params, first, last, firstWithHeader, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(nodeLayout, { _ in apply().1(animation.isAnimated, false) @@ -247,7 +247,12 @@ public class ContactsPeerItem: ListViewItem, ListViewItemWithHeader { } public func selected(listView: ListView) { - self.action(self.peer) + if self.enabled { + self.action(self.peer) + } else { + listView.clearHighlightAnimated(true) + self.disabledAction?(self.peer) + } } static func mergeType(item: ContactsPeerItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool) { @@ -285,6 +290,7 @@ private let avatarFont = avatarPlaceholderFont(size: 16.0) public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private let backgroundNode: ASDisplayNode + private let topSeparatorNode: ASDisplayNode private let separatorNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode @@ -299,12 +305,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private var selectionNode: CheckNode? private var actionIconNode: ASImageNode? - private var avatarState: (Account, Peer?)? - private var isHighlighted: Bool = false private var peerPresenceManager: PeerPresenceStatusManager? - private var layoutParams: (ContactsPeerItem, ListViewItemLayoutParams, Bool, Bool, Bool)? + private var layoutParams: (ContactsPeerItem, ListViewItemLayoutParams, Bool, Bool, Bool, ItemListNeighbors)? public var chatPeer: Peer? { if let peer = self.layoutParams?.0.peer { switch peer { @@ -326,6 +330,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true + self.topSeparatorNode = ASDisplayNode() + self.topSeparatorNode.isLayerBacked = true + self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true @@ -345,6 +352,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { self.isAccessibilityElement = true self.addSubnode(self.backgroundNode) + self.addSubnode(self.topSeparatorNode) self.addSubnode(self.separatorNode) self.addSubnode(self.containerNode) self.containerNode.addSubnode(self.avatarNode) @@ -353,7 +361,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in if let strongSelf = self, let layoutParams = strongSelf.layoutParams { - let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.1, layoutParams.2, layoutParams.3, layoutParams.4) + let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.1, layoutParams.2, layoutParams.3, layoutParams.4, layoutParams.5) let _ = apply() } }) @@ -368,11 +376,11 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { - if let (item, _, _, _, _) = self.layoutParams { + if let (item, _, _, _, _, _) = self.layoutParams { let (first, last, firstWithHeader) = ContactsPeerItem.mergeType(item: item, previousItem: previousItem, nextItem: nextItem) - self.layoutParams = (item, params, first, last, firstWithHeader) + self.layoutParams = (item, params, first, last, firstWithHeader, itemListNeighbors(item: item, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) let makeLayout = self.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(item, params, first, last, firstWithHeader) + let (nodeLayout, nodeApply) = makeLayout(item, params, first, last, firstWithHeader, itemListNeighbors(item: item, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) self.contentSize = nodeLayout.contentSize self.insets = nodeLayout.insets let _ = nodeApply() @@ -427,7 +435,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } } - public func asyncLayout() -> (_ item: ContactsPeerItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, (Bool, Bool) -> Void)) { + public func asyncLayout() -> (_ item: ContactsPeerItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> (Signal?, (Bool, Bool) -> Void)) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) let currentSelectionNode = self.selectionNode @@ -436,11 +444,17 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let currentItem = self.layoutParams?.0 - return { [weak self] item, params, first, last, firstWithHeader in + return { [weak self] item, params, first, last, firstWithHeader, neighbors in var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + let titleBoldFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize) + let statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0)) + let badgeFont = Font.regular(14.0) + let avatarDiameter = min(40.0, floor(item.presentationData.fontSize.itemListBaseFontSize * 40.0 / 17.0)) + + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } var leftInset: CGFloat = 65.0 + params.leftInset let rightInset: CGFloat = 10.0 + params.rightInset @@ -448,39 +462,39 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let updatedSelectionNode: CheckNode? var isSelected = false switch item.selection { - case .none: - updatedSelectionNode = nil - case let .selectable(selected): - leftInset += 28.0 - isSelected = selected - - let selectionNode: CheckNode - if let current = currentSelectionNode { - selectionNode = current - updatedSelectionNode = selectionNode - } else { - selectionNode = CheckNode(strokeColor: item.theme.list.itemCheckColors.strokeColor, fillColor: item.theme.list.itemCheckColors.fillColor, foregroundColor: item.theme.list.itemCheckColors.foregroundColor, style: .plain) - selectionNode.isUserInteractionEnabled = false - updatedSelectionNode = selectionNode - } + case .none: + updatedSelectionNode = nil + case let .selectable(selected): + leftInset += 28.0 + isSelected = selected + + let selectionNode: CheckNode + if let current = currentSelectionNode { + selectionNode = current + updatedSelectionNode = selectionNode + } else { + selectionNode = CheckNode(strokeColor: item.presentationData.theme.list.itemCheckColors.strokeColor, fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, style: .plain) + selectionNode.isUserInteractionEnabled = false + updatedSelectionNode = selectionNode + } } var verificationIconImage: UIImage? switch item.peer { - case let .peer(peer, _): - if let peer = peer, peer.isVerified { - verificationIconImage = PresentationResourcesChatList.verifiedIcon(item.theme) - } - case .deviceContact: - break + case let .peer(peer, _): + if let peer = peer, peer.isVerified { + verificationIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme) + } + case .deviceContact: + break } let actionIconImage: UIImage? switch item.actionIcon { - case .none: - actionIconImage = nil - case .add: - actionIconImage = PresentationResourcesItemList.plusIconImage(item.theme) + case .none: + actionIconImage = nil + case .add: + actionIconImage = PresentationResourcesItemList.plusIconImage(item.presentationData.theme) } var titleAttributedString: NSAttributedString? @@ -488,94 +502,94 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { var userPresence: TelegramUserPresence? switch item.peer { - case let .peer(peer, chatPeer): - if let peer = peer { - let textColor: UIColor - if let _ = chatPeer as? TelegramSecretChat { - textColor = item.theme.chatList.secretTitleColor - } else { - textColor = item.theme.list.itemPrimaryTextColor - } - if let user = peer as? TelegramUser { - if peer.id == item.account.peerId, case .generalSearch = item.peerMode { - titleAttributedString = NSAttributedString(string: item.strings.DialogList_SavedMessages, font: titleBoldFont, textColor: textColor) - } else if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { - let string = NSMutableAttributedString() - switch item.displayOrder { - case .firstLast: - string.append(NSAttributedString(string: firstName, font: item.sortOrder == .firstLast ? titleBoldFont : titleFont, textColor: textColor)) - string.append(NSAttributedString(string: " ", font: titleFont, textColor: textColor)) - string.append(NSAttributedString(string: lastName, font: item.sortOrder == .firstLast ? titleFont : titleBoldFont, textColor: textColor)) - case .lastFirst: - string.append(NSAttributedString(string: lastName, font: item.sortOrder == .firstLast ? titleFont : titleBoldFont, textColor: textColor)) - string.append(NSAttributedString(string: " ", font: titleFont, textColor: textColor)) - string.append(NSAttributedString(string: firstName, font: item.sortOrder == .firstLast ? titleBoldFont : titleFont, textColor: textColor)) - } - titleAttributedString = string - } else if let firstName = user.firstName, !firstName.isEmpty { - titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: textColor) - } else if let lastName = user.lastName, !lastName.isEmpty { - titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: textColor) - } else { - titleAttributedString = NSAttributedString(string: item.strings.User_DeletedAccount, font: titleBoldFont, textColor: textColor) - } - } else if let group = peer as? TelegramGroup { - titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) - } else if let channel = peer as? TelegramChannel { - titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) - } - - switch item.status { - case .none: - break - case let .presence(presence, dateTimeFormat): - let presence = (presence as? TelegramUserPresence) ?? TelegramUserPresence(status: .none, lastActivity: 0) - userPresence = presence - let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, dateTimeFormat: dateTimeFormat, presence: presence, relativeTo: Int32(timestamp)) - statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.theme.list.itemAccentColor : item.theme.list.itemSecondaryTextColor) - case let .addressName(suffix): - if let addressName = peer.addressName { - let addressNameString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.theme.list.itemAccentColor) - if !suffix.isEmpty { - let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) - let finalString = NSMutableAttributedString() - finalString.append(addressNameString) - finalString.append(suffixString) - statusAttributedString = finalString - } else { - statusAttributedString = addressNameString - } - } else if !suffix.isEmpty { - statusAttributedString = NSAttributedString(string: suffix, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) - } - case let .custom(text): - statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) - } - } - case let .deviceContact(_, contact): - let textColor: UIColor = item.theme.list.itemPrimaryTextColor - - if !contact.firstName.isEmpty, !contact.lastName.isEmpty { - let string = NSMutableAttributedString() - string.append(NSAttributedString(string: contact.firstName, font: titleFont, textColor: textColor)) - string.append(NSAttributedString(string: " ", font: titleFont, textColor: textColor)) - string.append(NSAttributedString(string: contact.lastName, font: titleBoldFont, textColor: textColor)) - titleAttributedString = string - } else if !contact.firstName.isEmpty { - titleAttributedString = NSAttributedString(string: contact.firstName, font: titleBoldFont, textColor: textColor) - } else if !contact.lastName.isEmpty { - titleAttributedString = NSAttributedString(string: contact.lastName, font: titleBoldFont, textColor: textColor) + case let .peer(peer, chatPeer): + if let peer = peer { + let textColor: UIColor + if let _ = chatPeer as? TelegramSecretChat { + textColor = item.presentationData.theme.chatList.secretTitleColor } else { - titleAttributedString = NSAttributedString(string: item.strings.User_DeletedAccount, font: titleBoldFont, textColor: textColor) + textColor = item.presentationData.theme.list.itemPrimaryTextColor + } + if let user = peer as? TelegramUser { + if peer.id == item.context.account.peerId, case .generalSearch = item.peerMode { + titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_SavedMessages, font: titleBoldFont, textColor: textColor) + } else if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { + let string = NSMutableAttributedString() + switch item.displayOrder { + case .firstLast: + string.append(NSAttributedString(string: firstName, font: item.sortOrder == .firstLast ? titleBoldFont : titleFont, textColor: textColor)) + string.append(NSAttributedString(string: " ", font: titleFont, textColor: textColor)) + string.append(NSAttributedString(string: lastName, font: item.sortOrder == .firstLast ? titleFont : titleBoldFont, textColor: textColor)) + case .lastFirst: + string.append(NSAttributedString(string: lastName, font: item.sortOrder == .firstLast ? titleFont : titleBoldFont, textColor: textColor)) + string.append(NSAttributedString(string: " ", font: titleFont, textColor: textColor)) + string.append(NSAttributedString(string: firstName, font: item.sortOrder == .firstLast ? titleBoldFont : titleFont, textColor: textColor)) + } + titleAttributedString = string + } else if let firstName = user.firstName, !firstName.isEmpty { + titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: textColor) + } else if let lastName = user.lastName, !lastName.isEmpty { + titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: textColor) + } else { + titleAttributedString = NSAttributedString(string: item.presentationData.strings.User_DeletedAccount, font: titleBoldFont, textColor: textColor) + } + } else if let group = peer as? TelegramGroup { + titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + } else if let channel = peer as? TelegramChannel { + titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) } switch item.status { - case let .custom(text): - statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) - default: - break + case .none: + break + case let .presence(presence, dateTimeFormat): + let presence = (presence as? TelegramUserPresence) ?? TelegramUserPresence(status: .none, lastActivity: 0) + userPresence = presence + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + let (string, activity) = stringAndActivityForUserPresence(strings: item.presentationData.strings, dateTimeFormat: dateTimeFormat, presence: presence, relativeTo: Int32(timestamp)) + statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor) + case let .addressName(suffix): + if let addressName = peer.addressName { + let addressNameString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.presentationData.theme.list.itemAccentColor) + if !suffix.isEmpty { + let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + let finalString = NSMutableAttributedString() + finalString.append(addressNameString) + finalString.append(suffixString) + statusAttributedString = finalString + } else { + statusAttributedString = addressNameString + } + } else if !suffix.isEmpty { + statusAttributedString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + } + case let .custom(text): + statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) } + } + case let .deviceContact(_, contact): + let textColor: UIColor = item.presentationData.theme.list.itemPrimaryTextColor + + if !contact.firstName.isEmpty, !contact.lastName.isEmpty { + let string = NSMutableAttributedString() + string.append(NSAttributedString(string: contact.firstName, font: titleFont, textColor: textColor)) + string.append(NSAttributedString(string: " ", font: titleFont, textColor: textColor)) + string.append(NSAttributedString(string: contact.lastName, font: titleBoldFont, textColor: textColor)) + titleAttributedString = string + } else if !contact.firstName.isEmpty { + titleAttributedString = NSAttributedString(string: contact.firstName, font: titleBoldFont, textColor: textColor) + } else if !contact.lastName.isEmpty { + titleAttributedString = NSAttributedString(string: contact.lastName, font: titleBoldFont, textColor: textColor) + } else { + titleAttributedString = NSAttributedString(string: item.presentationData.strings.User_DeletedAccount, font: titleBoldFont, textColor: textColor) + } + + switch item.status { + case let .custom(text): + statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + default: + break + } } var badgeTextLayoutAndApply: (TextNodeLayout, () -> TextNode)? @@ -584,11 +598,11 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let badgeTextColor: UIColor switch badge.type { case .inactive: - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.theme) - badgeTextColor = item.theme.chatList.unreadBadgeInactiveTextColor + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: 20.0) + badgeTextColor = item.presentationData.theme.chatList.unreadBadgeInactiveTextColor case .active: - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.theme) - badgeTextColor = item.theme.chatList.unreadBadgeActiveTextColor + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme, diameter: 20.0) + badgeTextColor = item.presentationData.theme.chatList.unreadBadgeActiveTextColor } let badgeAttributedString = NSAttributedString(string: badge.count > 0 ? "\(badge.count)" : " ", font: badgeFont, textColor: badgeTextColor) badgeTextLayoutAndApply = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: badgeAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 50.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -613,13 +627,23 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset - badgeSize), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 50.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) + let titleVerticalInset: CGFloat = statusAttributedString == nil ? 13.0 : 6.0 + let verticalInset: CGFloat = statusAttributedString == nil ? 13.0 : 6.0 + + let statusHeightComponent: CGFloat + if statusAttributedString == nil { + statusHeightComponent = 0.0 + } else { + statusHeightComponent = -1.0 + statusLayout.size.height + } + + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + statusHeightComponent), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) let titleFrame: CGRect if statusAttributedString != nil { - titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 6.0), size: titleLayout.size) + titleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleVerticalInset), size: titleLayout.size) } else { - titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 14.0), size: titleLayout.size) + titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) } let peerRevealOptions: [ItemListRevealOption] @@ -631,14 +655,14 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let textColor: UIColor switch option.type { case .neutral: - color = item.theme.list.itemDisclosureActions.constructive.fillColor - textColor = item.theme.list.itemDisclosureActions.constructive.foregroundColor + color = item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor + textColor = item.presentationData.theme.list.itemDisclosureActions.constructive.foregroundColor case .warning: - color = item.theme.list.itemDisclosureActions.warning.fillColor - textColor = item.theme.list.itemDisclosureActions.warning.foregroundColor + color = item.presentationData.theme.list.itemDisclosureActions.warning.fillColor + textColor = item.presentationData.theme.list.itemDisclosureActions.warning.foregroundColor case .destructive: - color = item.theme.list.itemDisclosureActions.destructive.fillColor - textColor = item.theme.list.itemDisclosureActions.destructive.foregroundColor + color = item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor + textColor = item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor } mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, textColor: textColor)) index += 1 @@ -652,7 +676,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { if let strongSelf = self { return (.complete(), { [weak strongSelf] animated, synchronousLoads in if let strongSelf = strongSelf { - strongSelf.layoutParams = (item, params, first, last, firstWithHeader) + strongSelf.layoutParams = (item, params, first, last, firstWithHeader, neighbors) strongSelf.accessibilityLabel = titleAttributedString?.string strongSelf.accessibilityValue = statusAttributedString?.string @@ -664,12 +688,12 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { case let .peer(peer, _): if let peer = peer { var overrideImage: AvatarNodeImageOverride? - if peer.id == item.account.peerId, case .generalSearch = item.peerMode { + if peer.id == item.context.account.peerId, case .generalSearch = item.peerMode { overrideImage = .savedMessagesIcon } else if peer.isDeleted { overrideImage = .deletedIcon } - strongSelf.avatarNode.setPeer(account: item.account, theme: item.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads) + strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads) } case let .deviceContact(_, contact): let letters: [String] @@ -695,12 +719,32 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let revealOffset = strongSelf.revealOffset if let _ = updatedTheme { - strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + switch item.style { + case .plain: + strongSelf.topSeparatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor + strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor + case .blocks: + strongSelf.topSeparatorNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + } + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } - transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: 5.0), size: CGSize(width: 40.0, height: 40.0))) + switch item.style { + case .plain: + strongSelf.topSeparatorNode.isHidden = true + case .blocks: + switch neighbors.top { + case .sameSection(false): + strongSelf.topSeparatorNode.isHidden = true + default: + strongSelf.topSeparatorNode.isHidden = false + } + } + + transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: floor((nodeLayout.contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter))) let _ = titleApply() transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame.offsetBy(dx: revealOffset, dy: 0.0)) @@ -709,7 +753,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { strongSelf.statusNode.alpha = item.enabled ? 1.0 : 1.0 let _ = statusApply() - let statusFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 27.0), size: statusLayout.size) + let statusFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: strongSelf.titleNode.frame.maxY - 1.0), size: statusLayout.size) let previousStatusFrame = strongSelf.statusNode.frame strongSelf.statusNode.frame = statusFrame @@ -816,6 +860,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let topHighlightInset: CGFloat = (first || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset)) + strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(nodeLayout.insets.top, separatorHeight)), size: CGSize(width: nodeLayout.contentSize.width, height: separatorHeight)) strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: max(0.0, nodeLayout.size.width - leftInset), height: separatorHeight)) strongSelf.separatorNode.isHidden = last @@ -826,7 +871,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { strongSelf.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) if item.editing.editable { - strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)])) strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) } else { strongSelf.setRevealOptions((left: [], right: peerRevealOptions)) @@ -947,7 +992,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } override public func header() -> ListViewItemHeader? { - if let (item, _, _, _, _) = self.layoutParams { + if let (item, _, _, _, _, _) = self.layoutParams { return item.header } else { return nil diff --git a/submodules/ContextUI/BUCK b/submodules/ContextUI/BUCK index dd389963ff..9249d703dd 100644 --- a/submodules/ContextUI/BUCK +++ b/submodules/ContextUI/BUCK @@ -12,6 +12,7 @@ static_library( "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/TextSelectionNode:TextSelectionNode", "//submodules/ReactionSelectionNode:ReactionSelectionNode", + "//submodules/AppBundle:AppBundle", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/ContextUI/Sources/ContextActionNode.swift b/submodules/ContextUI/Sources/ContextActionNode.swift index 470101545a..dece341b12 100644 --- a/submodules/ContextUI/Sources/ContextActionNode.swift +++ b/submodules/ContextUI/Sources/ContextActionNode.swift @@ -3,8 +3,6 @@ import AsyncDisplayKit import Display import TelegramPresentationData -private let textFont = Font.regular(17.0) - enum ContextActionSibling { case none case item @@ -23,17 +21,19 @@ final class ContextActionNode: ASDisplayNode { private let iconNode: ASImageNode private let buttonNode: HighlightTrackingButtonNode - init(theme: PresentationTheme, action: ContextMenuActionItem, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + init(presentationData: PresentationData, action: ContextMenuActionItem, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { self.action = action self.getController = getController self.actionSelected = actionSelected + let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize) + self.backgroundNode = ASDisplayNode() self.backgroundNode.isAccessibilityElement = false - self.backgroundNode.backgroundColor = theme.contextMenu.itemBackgroundColor + self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isAccessibilityElement = false - self.highlightedBackgroundNode.backgroundColor = theme.contextMenu.itemHighlightedBackgroundColor + self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor self.highlightedBackgroundNode.alpha = 0.0 self.textNode = ImmediateTextNode() @@ -43,9 +43,9 @@ final class ContextActionNode: ASDisplayNode { let textColor: UIColor switch action.textColor { case .primary: - textColor = theme.contextMenu.primaryColor + textColor = presentationData.theme.contextMenu.primaryColor case .destructive: - textColor = theme.contextMenu.destructiveColor + textColor = presentationData.theme.contextMenu.destructiveColor } self.textNode.attributedText = NSAttributedString(string: action.text, font: textFont, textColor: textColor) @@ -62,7 +62,7 @@ final class ContextActionNode: ASDisplayNode { statusNode.isAccessibilityElement = false statusNode.isUserInteractionEnabled = false statusNode.displaysAsynchronously = false - statusNode.attributedText = NSAttributedString(string: value, font: textFont, textColor: theme.contextMenu.secondaryColor) + statusNode.attributedText = NSAttributedString(string: value, font: textFont, textColor: presentationData.theme.contextMenu.secondaryColor) statusNode.maximumNumberOfLines = 1 self.statusNode = statusNode } @@ -72,7 +72,7 @@ final class ContextActionNode: ASDisplayNode { self.iconNode.displaysAsynchronously = false self.iconNode.displayWithoutProcessing = true self.iconNode.isUserInteractionEnabled = false - self.iconNode.image = action.icon(theme) + self.iconNode.image = action.icon(presentationData.theme) self.buttonNode = HighlightTrackingButtonNode() self.buttonNode.isAccessibilityElement = true @@ -149,27 +149,30 @@ final class ContextActionNode: ASDisplayNode { } } - func updateTheme(theme: PresentationTheme) { - self.backgroundNode.backgroundColor = theme.contextMenu.itemBackgroundColor - self.highlightedBackgroundNode.backgroundColor = theme.contextMenu.itemHighlightedBackgroundColor + func updateTheme(presentationData: PresentationData) { + self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor + self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor let textColor: UIColor switch action.textColor { case .primary: - textColor = theme.contextMenu.primaryColor + textColor = presentationData.theme.contextMenu.primaryColor case .destructive: - textColor = theme.contextMenu.destructiveColor + textColor = presentationData.theme.contextMenu.destructiveColor } + + let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize) + self.textNode.attributedText = NSAttributedString(string: self.action.text, font: textFont, textColor: textColor) switch self.action.textLayout { case let .secondLineWithValue(value): - self.statusNode?.attributedText = NSAttributedString(string: value, font: textFont, textColor: theme.contextMenu.secondaryColor) + self.statusNode?.attributedText = NSAttributedString(string: value, font: textFont, textColor: presentationData.theme.contextMenu.secondaryColor) default: break } - self.iconNode.image = self.action.icon(theme) + self.iconNode.image = self.action.icon(presentationData.theme) } @objc private func buttonPressed() { diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift index e1c5a9fb21..50cb4d90a5 100644 --- a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift +++ b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift @@ -2,6 +2,8 @@ import Foundation import AsyncDisplayKit import Display import TelegramPresentationData +import TextSelectionNode +import AppBundle private final class ContextActionsSelectionGestureRecognizer: UIPanGestureRecognizer { var updateLocation: ((CGPoint, Bool) -> Void)? @@ -38,7 +40,8 @@ private enum ContextItemNode { case separator(ASDisplayNode) } -final class ContextActionsContainerNode: ASDisplayNode { +private final class InnerActionsContainerNode: ASDisplayNode { + private let presentationData: PresentationData private var effectView: UIVisualEffectView? private var itemNodes: [ContextItemNode] private let feedbackTap: () -> Void @@ -46,22 +49,23 @@ final class ContextActionsContainerNode: ASDisplayNode { private(set) var gesture: UIGestureRecognizer? private var currentHighlightedActionNode: ContextActionNode? - init(theme: PresentationTheme, items: [ContextMenuItem], getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void) { + init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void) { + self.presentationData = presentationData self.feedbackTap = feedbackTap var itemNodes: [ContextItemNode] = [] for i in 0 ..< items.count { switch items[i] { case let .action(action): - itemNodes.append(.action(ContextActionNode(theme: theme, action: action, getController: getController, actionSelected: actionSelected))) + itemNodes.append(.action(ContextActionNode(presentationData: presentationData, action: action, getController: getController, actionSelected: actionSelected))) if i != items.count - 1, case .action = items[i + 1] { let separatorNode = ASDisplayNode() - separatorNode.backgroundColor = theme.contextMenu.itemSeparatorColor + separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor itemNodes.append(.itemSeparator(separatorNode)) } case .separator: let separatorNode = ASDisplayNode() - separatorNode.backgroundColor = theme.contextMenu.sectionSeparatorColor + separatorNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor itemNodes.append(.separator(separatorNode)) } } @@ -73,7 +77,7 @@ final class ContextActionsContainerNode: ASDisplayNode { self.clipsToBounds = true self.cornerRadius = 14.0 - self.backgroundColor = theme.contextMenu.backgroundColor + self.backgroundColor = presentationData.theme.contextMenu.backgroundColor self.itemNodes.forEach({ itemNode in switch itemNode { @@ -130,7 +134,13 @@ final class ContextActionsContainerNode: ASDisplayNode { case .regular: if self.effectView == nil { let effectView: UIVisualEffectView - if #available(iOS 10.0, *) { + if #available(iOS 13.0, *) { + if self.presentationData.theme.overallDarkAppearance { + effectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterialDark)) + } else { + effectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterialLight)) + } + } else if #available(iOS 10.0, *) { effectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) } else { effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) @@ -205,19 +215,19 @@ final class ContextActionsContainerNode: ASDisplayNode { return size } - func updateTheme(theme: PresentationTheme) { + func updateTheme(presentationData: PresentationData) { for itemNode in self.itemNodes { switch itemNode { case let .action(action): - action.updateTheme(theme: theme) + action.updateTheme(presentationData: presentationData) case let .separator(separator): - separator.backgroundColor = theme.contextMenu.sectionSeparatorColor + separator.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor case let .itemSeparator(itemSeparator): - itemSeparator.backgroundColor = theme.contextMenu.itemSeparatorColor + itemSeparator.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor } } - self.backgroundColor = theme.contextMenu.backgroundColor + self.backgroundColor = presentationData.theme.contextMenu.backgroundColor } func actionNode(at point: CGPoint) -> ContextActionNode? { @@ -234,3 +244,179 @@ final class ContextActionsContainerNode: ASDisplayNode { return nil } } + +private final class InnerTextSelectionTipContainerNode: ASDisplayNode { + private let presentationData: PresentationData + private var effectView: UIVisualEffectView? + private let textNode: TextNode + private var textSelectionNode: TextSelectionNode? + private let iconNode: ASImageNode + + private let text: String + private let targetSelectionIndex: Int + + init(presentationData: PresentationData) { + self.presentationData = presentationData + self.textNode = TextNode() + + var rawText = self.presentationData.strings.ChatContextMenu_TextSelectionTip + if let range = rawText.range(of: "|") { + rawText.removeSubrange(range) + self.text = rawText + self.targetSelectionIndex = NSRange(range, in: rawText).lowerBound + } else { + self.text = rawText + self.targetSelectionIndex = 1 + } + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Tip"), color: presentationData.theme.contextMenu.primaryColor) + + super.init() + + self.clipsToBounds = true + self.cornerRadius = 14.0 + + self.backgroundColor = presentationData.theme.contextMenu.backgroundColor + + let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: presentationData.theme.contextMenu.primaryColor.withAlphaComponent(0.15), knob: presentationData.theme.contextMenu.primaryColor, knobDiameter: 8.0), strings: presentationData.strings, textNode: self.textNode, updateIsActive: { _ in + }, present: { _, _ in + }, rootNode: self, performAction: { _, _ in + }) + self.textSelectionNode = textSelectionNode + + self.addSubnode(self.textNode) + self.addSubnode(self.iconNode) + + self.textSelectionNode.flatMap(self.addSubnode) + + self.addSubnode(textSelectionNode.highlightAreaNode) + } + + func updateLayout(widthClass: ContainerViewLayoutSizeClass, width: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize { + switch widthClass { + case .compact: + if let effectView = self.effectView { + self.effectView = nil + effectView.removeFromSuperview() + } + case .regular: + if self.effectView == nil { + let effectView: UIVisualEffectView + if #available(iOS 13.0, *) { + if self.presentationData.theme.overallDarkAppearance { + effectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterialDark)) + } else { + effectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterialLight)) + } + } else if #available(iOS 10.0, *) { + effectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + } else { + effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) + } + self.effectView = effectView + self.view.insertSubview(effectView, at: 0) + } + } + + let verticalInset: CGFloat = 10.0 + let horizontalInset: CGFloat = 16.0 + let standardIconWidth: CGFloat = 32.0 + let iconSideInset: CGFloat = 12.0 + + let textFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0)) + + let iconSize = self.iconNode.image?.size ?? CGSize(width: 16.0, height: 16.0) + + let makeTextLayout = TextNode.asyncLayout(self.textNode) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.text, font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor), backgroundColor: nil, minimumNumberOfLines: 0, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - horizontalInset * 2.0 - iconSize.width - 8.0, height: .greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets(), lineColor: nil, textShadowColor: nil, textStroke: nil)) + let _ = textApply() + + let textFrame = CGRect(origin: CGPoint(x: horizontalInset, y: verticalInset), size: textLayout.size) + transition.updateFrame(node: self.textNode, frame: textFrame) + + let size = CGSize(width: width, height: textLayout.size.height + verticalInset * 2.0) + + let iconFrame = CGRect(origin: CGPoint(x: size.width - standardIconWidth - iconSideInset + floor((standardIconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) + transition.updateFrame(node: self.iconNode, frame: iconFrame) + + if let textSelectionNode = self.textSelectionNode { + transition.updateFrame(node: textSelectionNode, frame: textFrame) + textSelectionNode.highlightAreaNode.frame = textFrame + } + + if let effectView = self.effectView { + transition.updateFrame(view: effectView, frame: CGRect(origin: CGPoint(), size: size)) + } + + return size + } + + func updateTheme(presentationData: PresentationData) { + self.backgroundColor = presentationData.theme.contextMenu.backgroundColor + } + + func animateIn() { + if let textSelectionNode = self.textSelectionNode { + textSelectionNode.pretendInitiateSelection() + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.textSelectionNode?.pretendExtendSelection(to: strongSelf.targetSelectionIndex) + }) + } + } +} + +final class ContextActionsContainerNode: ASDisplayNode { + private let actionsNode: InnerActionsContainerNode + private let textSelectionTipNode: InnerTextSelectionTipContainerNode? + + init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, displayTextSelectionTip: Bool) { + self.actionsNode = InnerActionsContainerNode(presentationData: presentationData, items: items, getController: getController, actionSelected: actionSelected, feedbackTap: feedbackTap) + if displayTextSelectionTip { + let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: presentationData) + textSelectionTipNode.isUserInteractionEnabled = false + self.textSelectionTipNode = textSelectionTipNode + } else { + self.textSelectionTipNode = nil + } + + super.init() + + self.addSubnode(self.actionsNode) + self.textSelectionTipNode.flatMap(self.addSubnode) + } + + func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize { + let actionsSize = self.actionsNode.updateLayout(widthClass: widthClass, constrainedWidth: constrainedWidth, transition: transition) + + var contentSize = actionsSize + transition.updateFrame(node: self.actionsNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: actionsSize)) + + if let textSelectionTipNode = self.textSelectionTipNode { + contentSize.height += 8.0 + let textSelectionTipSize = textSelectionTipNode.updateLayout(widthClass: widthClass, width: actionsSize.width, transition: transition) + transition.updateFrame(node: textSelectionTipNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: textSelectionTipSize)) + contentSize.height += textSelectionTipSize.height + } + + return contentSize + } + + func actionNode(at point: CGPoint) -> ContextActionNode? { + return self.actionsNode.actionNode(at: self.view.convert(point, to: self.actionsNode.view)) + } + + func updateTheme(presentationData: PresentationData) { + self.actionsNode.updateTheme(presentationData: presentationData) + self.textSelectionTipNode?.updateTheme(presentationData: presentationData) + } + + func animateIn() { + self.textSelectionTipNode?.animateIn() + } +} diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 5aa8a38c00..ef9600e46c 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -60,8 +60,7 @@ private func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIV } private final class ContextControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { - private var theme: PresentationTheme - private var strings: PresentationStrings + private var presentationData: PresentationData private let source: ContextContentSource private var items: Signal<[ContextMenuItem], NoError> private let beginDismiss: (ContextMenuActionResult) -> Void @@ -70,6 +69,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi private let attemptTransitionControllerIntoNavigation: () -> Void private let getController: () -> ContextController? private weak var gesture: ContextGesture? + private var displayTextSelectionTip: Bool private var didSetItemsReady = false let itemsReady = Promise() @@ -108,9 +108,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi private let itemsDisposable = MetaDisposable() - init(account: Account, controller: ContextController, theme: PresentationTheme, strings: PresentationStrings, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], beginDismiss: @escaping (ContextMenuActionResult) -> Void, recognizer: TapLongTapOrDoubleTapGestureRecognizer?, gesture: ContextGesture?, reactionSelected: @escaping (String) -> Void, beganAnimatingOut: @escaping () -> Void, attemptTransitionControllerIntoNavigation: @escaping () -> Void) { - self.theme = theme - self.strings = strings + init(account: Account, controller: ContextController, presentationData: PresentationData, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], beginDismiss: @escaping (ContextMenuActionResult) -> Void, recognizer: TapLongTapOrDoubleTapGestureRecognizer?, gesture: ContextGesture?, reactionSelected: @escaping (String) -> Void, beganAnimatingOut: @escaping () -> Void, attemptTransitionControllerIntoNavigation: @escaping () -> Void, displayTextSelectionTip: Bool) { + self.presentationData = presentationData self.source = source self.items = items self.beginDismiss = beginDismiss @@ -118,6 +117,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.beganAnimatingOut = beganAnimatingOut self.attemptTransitionControllerIntoNavigation = attemptTransitionControllerIntoNavigation self.gesture = gesture + self.displayTextSelectionTip = displayTextSelectionTip self.getController = { [weak controller] in return controller @@ -126,7 +126,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.effectView = UIVisualEffectView() if #available(iOS 9.0, *) { } else { - if theme.rootController.keyboardColor == .dark { + if presentationData.theme.rootController.keyboardColor == .dark { self.effectView.effect = UIBlurEffect(style: .dark) } else { self.effectView.effect = UIBlurEffect(style: .light) @@ -135,7 +135,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = theme.contextMenu.dimColor + self.dimNode.backgroundColor = presentationData.theme.contextMenu.dimColor self.dimNode.alpha = 0.0 self.withoutBlurDimNode = ASDisplayNode() @@ -159,16 +159,16 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi var feedbackTap: (() -> Void)? - self.actionsContainerNode = ContextActionsContainerNode(theme: theme, items: [], getController: { [weak controller] in + self.actionsContainerNode = ContextActionsContainerNode(presentationData: presentationData, items: [], getController: { [weak controller] in return controller }, actionSelected: { result in beginDismiss(result) }, feedbackTap: { feedbackTap?() - }) + }, displayTextSelectionTip: self.displayTextSelectionTip) if !reactionItems.isEmpty { - let reactionContextNode = ReactionContextNode(account: account, theme: theme, items: reactionItems) + let reactionContextNode = ReactionContextNode(account: account, theme: presentationData.theme, items: reactionItems) self.reactionContextNode = reactionContextNode } else { self.reactionContextNode = nil @@ -520,7 +520,6 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.effectView.effect = makeCustomZoomBlurEffect() self.effectView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) self.propertyAnimator = UIViewPropertyAnimator(duration: 0.2 * animationDurationFactor * UIView.animationDurationFactor(), curve: .easeInOut, animations: { [weak self] in - //self?.effectView.effect = makeCustomZoomBlurEffect() }) } @@ -531,6 +530,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi }, completion: { [weak self] in self?.didCompleteAnimationIn = true self?.hapticFeedback.prepareTap() + self?.actionsContainerNode.animateIn() }) } } else { @@ -538,6 +538,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.effectView.effect = makeCustomZoomBlurEffect() }, completion: { [weak self] _ in self?.didCompleteAnimationIn = true + self?.actionsContainerNode.animateIn() }) } @@ -923,7 +924,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } - func animateOutToReaction(value: String, into targetNode: ASImageNode, hideNode: Bool, completion: @escaping () -> Void) { + func animateOutToReaction(value: String, into targetNode: ASDisplayNode, hideNode: Bool, completion: @escaping () -> Void) { guard let reactionContextNode = self.reactionContextNode else { self.animateOut(result: .default, completion: completion) return @@ -967,13 +968,13 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.currentItems = items let previousActionsContainerNode = self.actionsContainerNode - self.actionsContainerNode = ContextActionsContainerNode(theme: self.theme, items: items, getController: { [weak self] in + self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: items, getController: { [weak self] in return self?.getController() }, actionSelected: { [weak self] result in self?.beginDismiss(result) }, feedbackTap: { [weak self] in self?.hapticFeedback.tap() - }) + }, displayTextSelectionTip: self.displayTextSelectionTip) self.scrollNode.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode) if let layout = self.validLayout { @@ -985,15 +986,16 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi if !self.didSetItemsReady { self.didSetItemsReady = true + self.displayTextSelectionTip = false self.itemsReady.set(.single(true)) } } - func updateTheme(theme: PresentationTheme) { - self.theme = theme + func updateTheme(presentationData: PresentationData) { + self.presentationData = presentationData - self.dimNode.backgroundColor = theme.contextMenu.dimColor - self.actionsContainerNode.updateTheme(theme: theme) + self.dimNode.backgroundColor = presentationData.theme.contextMenu.dimColor + self.actionsContainerNode.updateTheme(presentationData: presentationData) if let validLayout = self.validLayout { self.updateLayout(layout: validLayout, transition: .immediate, previousActionsContainerNode: nil) @@ -1365,8 +1367,7 @@ public enum ContextContentSource { public final class ContextController: ViewController, StandalonePresentableController { private let account: Account - private var theme: PresentationTheme - private var strings: PresentationStrings + private var presentationData: PresentationData private let source: ContextContentSource private var items: Signal<[ContextMenuItem], NoError> private var reactionItems: [ReactionContextItem] @@ -1378,6 +1379,7 @@ public final class ContextController: ViewController, StandalonePresentableContr private weak var recognizer: TapLongTapOrDoubleTapGestureRecognizer? private weak var gesture: ContextGesture? + private let displayTextSelectionTip: Bool private var animatedDidAppear = false private var wasDismissed = false @@ -1388,15 +1390,15 @@ public final class ContextController: ViewController, StandalonePresentableContr public var reactionSelected: ((String) -> Void)? - public init(account: Account, theme: PresentationTheme, strings: PresentationStrings, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil) { + public init(account: Account, presentationData: PresentationData, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, displayTextSelectionTip: Bool = false) { self.account = account - self.theme = theme - self.strings = strings + self.presentationData = presentationData self.source = source self.items = items self.reactionItems = reactionItems self.recognizer = recognizer self.gesture = gesture + self.displayTextSelectionTip = displayTextSelectionTip super.init(navigationBarPresentationData: nil) @@ -1408,7 +1410,7 @@ public final class ContextController: ViewController, StandalonePresentableContr } override public func loadDisplayNode() { - self.displayNode = ContextControllerNode(account: self.account, controller: self, theme: self.theme, strings: self.strings, source: self.source, items: self.items, reactionItems: self.reactionItems, beginDismiss: { [weak self] result in + self.displayNode = ContextControllerNode(account: self.account, controller: self, presentationData: self.presentationData, source: self.source, items: self.items, reactionItems: self.reactionItems, beginDismiss: { [weak self] result in self?.dismiss(result: result, completion: nil) }, recognizer: self.recognizer, gesture: self.gesture, reactionSelected: { [weak self] value in guard let strongSelf = self else { @@ -1430,7 +1432,7 @@ public final class ContextController: ViewController, StandalonePresentableContr default: break } - }) + }, displayTextSelectionTip: self.displayTextSelectionTip) self.displayNodeDidLoad() @@ -1465,10 +1467,10 @@ public final class ContextController: ViewController, StandalonePresentableContr } } - public func updateTheme(theme: PresentationTheme) { - self.theme = theme + public func updateTheme(presentationData: PresentationData) { + self.presentationData = presentationData if self.isNodeLoaded { - self.controllerNode.updateTheme(theme: theme) + self.controllerNode.updateTheme(presentationData: presentationData) } } @@ -1486,7 +1488,7 @@ public final class ContextController: ViewController, StandalonePresentableContr self.dismiss(result: .default, completion: completion) } - public func dismissWithReaction(value: String, into targetNode: ASImageNode, hideNode: Bool, completion: (() -> Void)?) { + public func dismissWithReaction(value: String, into targetNode: ASDisplayNode, hideNode: Bool, completion: (() -> Void)?) { if !self.wasDismissed { self.wasDismissed = true self.controllerNode.animateOutToReaction(value: value, into: targetNode, hideNode: hideNode, completion: { [weak self] in diff --git a/submodules/ContextUI/Sources/ContextControllerSourceNode.swift b/submodules/ContextUI/Sources/ContextControllerSourceNode.swift index 51b7fca583..e375015ece 100644 --- a/submodules/ContextUI/Sources/ContextControllerSourceNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerSourceNode.swift @@ -14,6 +14,12 @@ public final class ContextControllerSourceNode: ASDisplayNode { public var shouldBegin: ((CGPoint) -> Bool)? public var customActivationProgress: ((CGFloat, ContextGestureTransition) -> Void)? + public func cancelGesture() { + self.contextGesture?.cancel() + self.contextGesture?.isEnabled = false + self.contextGesture?.isEnabled = self.isGestureEnabled + } + override public func didLoad() { super.didLoad() diff --git a/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionControllerNode.swift b/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionControllerNode.swift index a422c2f291..32dbc0c4bf 100644 --- a/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionControllerNode.swift +++ b/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionControllerNode.swift @@ -98,6 +98,11 @@ final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode, self.searchTableView = UITableView(frame: CGRect(), style: .plain) self.searchTableView.isHidden = true + if #available(iOS 11.0, *) { + self.tableView.contentInsetAdjustmentBehavior = .never + self.searchTableView.contentInsetAdjustmentBehavior = .never + } + let countryNamesAndCodes = localizedContryNamesAndCodes(strings: strings) var sections: [(String, [((String, String), String, Int)])] = [] @@ -147,6 +152,8 @@ final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode, } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.tableView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0) + self.searchTableView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0) transition.updateFrame(view: self.tableView, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight))) transition.updateFrame(view: self.searchTableView, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight))) } diff --git a/submodules/Database/ValueBox/Sources/SqliteValueBox.swift b/submodules/Database/ValueBox/Sources/SqliteValueBox.swift index 4bfb081c1c..a26511d475 100644 --- a/submodules/Database/ValueBox/Sources/SqliteValueBox.swift +++ b/submodules/Database/ValueBox/Sources/SqliteValueBox.swift @@ -1773,8 +1773,14 @@ public final class SqliteValueBox: ValueBox { statement.reset() } - public func fullTextRemove(_ table: ValueBoxFullTextTable, itemId: String) { + public func fullTextRemove(_ table: ValueBoxFullTextTable, itemId: String, secure: Bool) { if let _ = self.fullTextTables[table.id] { + if secure != self.secureDeleteEnabled { + self.secureDeleteEnabled = secure + let result = database.execute("PRAGMA secure_delete=\(secure ? 1 : 0)") + precondition(result) + } + guard let itemIdData = itemId.data(using: .utf8) else { return } diff --git a/submodules/Database/ValueBox/Sources/ValueBox.swift b/submodules/Database/ValueBox/Sources/ValueBox.swift index 18cc7faeaa..255ee011ed 100644 --- a/submodules/Database/ValueBox/Sources/ValueBox.swift +++ b/submodules/Database/ValueBox/Sources/ValueBox.swift @@ -83,7 +83,7 @@ public protocol ValueBox { func removeRange(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey) func fullTextSet(_ table: ValueBoxFullTextTable, collectionId: String, itemId: String, contents: String, tags: String) func fullTextMatch(_ table: ValueBoxFullTextTable, collectionId: String?, query: String, tags: String?, values: (String, String) -> Bool) - func fullTextRemove(_ table: ValueBoxFullTextTable, itemId: String) + func fullTextRemove(_ table: ValueBoxFullTextTable, itemId: String, secure: Bool) func removeAllFromTable(_ table: ValueBoxTable) func removeTable(_ table: ValueBoxTable) func renameTable(_ table: ValueBoxTable, to toTable: ValueBoxTable) diff --git a/submodules/DateSelectionUI/Sources/DateSelectionActionSheetController.swift b/submodules/DateSelectionUI/Sources/DateSelectionActionSheetController.swift index b11e3572ca..ee5ee5f509 100644 --- a/submodules/DateSelectionUI/Sources/DateSelectionActionSheetController.swift +++ b/submodules/DateSelectionUI/Sources/DateSelectionActionSheetController.swift @@ -20,11 +20,11 @@ public final class DateSelectionActionSheetController: ActionSheetController { let theme = presentationData.theme let strings = presentationData.strings - super.init(theme: ActionSheetControllerTheme(presentationTheme: theme)) + super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationTheme: presentationData.theme) + strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) } }) diff --git a/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift b/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift index bbaefbcb19..1e1d100d6f 100644 --- a/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift +++ b/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift @@ -56,6 +56,8 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { self.theme = theme self.strings = strings + let peerFont = Font.regular(floor(theme.baseFontSize * 14.0 / 17.0)) + self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode.isAccessibilityElement = false @@ -74,13 +76,13 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { self.addSubnode(self.accessibilityArea) if chatPeer.id == context.account.peerId { - self.avatarNode.setPeer(account: context.account, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .savedMessagesIcon) + self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .savedMessagesIcon) } else { var overrideImage: AvatarNodeImageOverride? if chatPeer.isDeleted { overrideImage = .deletedIcon } - self.avatarNode.setPeer(account: context.account, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: overrideImage) + self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: overrideImage) } var attributedText: NSAttributedString? @@ -88,9 +90,9 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { case .clearCache, .clearCacheSuggestion: switch action { case .clearCache: - attributedText = NSAttributedString(string: strings.ClearCache_Description, font: Font.regular(14.0), textColor: theme.primaryTextColor) + attributedText = NSAttributedString(string: strings.ClearCache_Description, font: peerFont, textColor: theme.primaryTextColor) case .clearCacheSuggestion: - attributedText = NSAttributedString(string: strings.ClearCache_FreeSpaceDescription, font: Font.regular(14.0), textColor: theme.primaryTextColor) + attributedText = NSAttributedString(string: strings.ClearCache_FreeSpaceDescription, font: peerFont, textColor: theme.primaryTextColor) default: break } @@ -115,9 +117,9 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { break } if let text = text { - var formattedAttributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: text.0, font: Font.regular(14.0), textColor: theme.primaryTextColor)) + var formattedAttributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: text.0, font: peerFont, textColor: theme.primaryTextColor)) for (_, range) in text.1 { - formattedAttributedText.addAttribute(.font, value: Font.semibold(14.0), range: range) + formattedAttributedText.addAttribute(.font, value: peerFont, range: range) } attributedText = formattedAttributedText } diff --git a/submodules/DeviceAccess/BUCK b/submodules/DeviceAccess/BUCK index 3d2abcb731..89cb4a25f6 100644 --- a/submodules/DeviceAccess/BUCK +++ b/submodules/DeviceAccess/BUCK @@ -16,5 +16,6 @@ static_library( ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/CoreLocation.framework", ], ) diff --git a/submodules/DeviceAccess/Sources/DeviceAccess.swift b/submodules/DeviceAccess/Sources/DeviceAccess.swift index c70bc19e15..b0d0bb4bf9 100644 --- a/submodules/DeviceAccess/Sources/DeviceAccess.swift +++ b/submodules/DeviceAccess/Sources/DeviceAccess.swift @@ -244,7 +244,7 @@ public final class DeviceAccess { } } - public static func authorizeAccess(to subject: DeviceAccessSubject, registerForNotifications: ((@escaping (Bool) -> Void) -> Void)? = nil, requestSiriAuthorization: ((@escaping (Bool) -> Void) -> Void)? = nil, presentationData: PresentationData? = nil, present: @escaping (ViewController, Any?) -> Void = { _, _ in }, openSettings: @escaping () -> Void = { }, displayNotificationFromBackground: @escaping (String) -> Void = { _ in }, _ completion: @escaping (Bool) -> Void = { _ in }) { + public static func authorizeAccess(to subject: DeviceAccessSubject, registerForNotifications: ((@escaping (Bool) -> Void) -> Void)? = nil, requestSiriAuthorization: ((@escaping (Bool) -> Void) -> Void)? = nil, locationManager: LocationManager? = nil, presentationData: PresentationData? = nil, present: @escaping (ViewController, Any?) -> Void = { _, _ in }, openSettings: @escaping () -> Void = { }, displayNotificationFromBackground: @escaping (String) -> Void = { _ in }, _ completion: @escaping (Bool) -> Void = { _ in }) { switch subject { case .camera: let status = PGCamera.cameraAuthorizationStatus() @@ -254,7 +254,7 @@ public final class DeviceAccess { completion(response) if !response, let presentationData = presentationData { let text = presentationData.strings.AccessDenied_Camera - present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } @@ -268,7 +268,7 @@ public final class DeviceAccess { text = presentationData.strings.AccessDenied_Camera } completion(false) - present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } else if status == PGCameraAuthorizationStatusAuthorized { @@ -282,24 +282,26 @@ public final class DeviceAccess { completion(true) } else { AVAudioSession.sharedInstance().requestRecordPermission({ granted in - if granted { - completion(true) - } else if let presentationData = presentationData { - completion(false) - let text: String - switch microphoneSubject { - case .audio: - text = presentationData.strings.AccessDenied_VoiceMicrophone - case .video: - text = presentationData.strings.AccessDenied_VideoMicrophone - case .voiceCall: - text = presentationData.strings.AccessDenied_CallMicrophone - } - present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { - openSettings() - })]), nil) - if case .voiceCall = microphoneSubject { - displayNotificationFromBackground(text) + Queue.mainQueue().async { + if granted { + completion(true) + } else if let presentationData = presentationData { + completion(false) + let text: String + switch microphoneSubject { + case .audio: + text = presentationData.strings.AccessDenied_VoiceMicrophone + case .video: + text = presentationData.strings.AccessDenied_VideoMicrophone + case .voiceCall: + text = presentationData.strings.AccessDenied_CallMicrophone + } + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + openSettings() + })]), nil) + if case .voiceCall = microphoneSubject { + displayNotificationFromBackground(text) + } } } }) @@ -320,7 +322,7 @@ public final class DeviceAccess { case .wallpaper: text = presentationData.strings.AccessDenied_Wallpapers } - present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } @@ -356,7 +358,7 @@ public final class DeviceAccess { completion(false) if let presentationData = presentationData { let text = presentationData.strings.AccessDenied_LocationAlwaysDenied - present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } @@ -375,12 +377,23 @@ public final class DeviceAccess { } else { text = presentationData.strings.AccessDenied_LocationDisabled } - present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } case .notDetermined: - completion(true) + switch locationSubject { + case .send, .tracking: + locationManager?.requestWhenInUseAuthorization(completion: { status in + completion(status == .authorizedWhenInUse || status == .authorizedAlways) + }) + case .live: + locationManager?.requestAlwaysAuthorization(completion: { status in + completion(status == .authorizedAlways) + }) + default: + break + } @unknown default: fatalError() } @@ -448,7 +461,7 @@ public final class DeviceAccess { } case .cellularData: if let presentationData = presentationData { - present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.Permissions_CellularDataTitle_v0, text: presentationData.strings.Permissions_CellularDataText_v0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.Permissions_CellularDataTitle_v0, text: presentationData.strings.Permissions_CellularDataText_v0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } diff --git a/submodules/DeviceAccess/Sources/LocationManager.swift b/submodules/DeviceAccess/Sources/LocationManager.swift new file mode 100644 index 0000000000..99a6c39c38 --- /dev/null +++ b/submodules/DeviceAccess/Sources/LocationManager.swift @@ -0,0 +1,39 @@ +import Foundation +import CoreLocation + +public final class LocationManager: NSObject, CLLocationManagerDelegate { + public let manager = CLLocationManager() + var pendingCompletion: ((CLAuthorizationStatus) -> Void, CLAuthorizationStatus)? + + public override init() { + super.init() + self.manager.delegate = self + } + + func requestWhenInUseAuthorization(completion: @escaping (CLAuthorizationStatus) -> Void) { + let status = CLLocationManager.authorizationStatus() + if status == .notDetermined { + self.manager.requestWhenInUseAuthorization() + self.pendingCompletion = (completion, .authorizedWhenInUse) + } else { + completion(status) + } + } + + func requestAlwaysAuthorization(completion: @escaping (CLAuthorizationStatus) -> Void) { + let status = CLLocationManager.authorizationStatus() + if status == .notDetermined { + self.manager.requestWhenInUseAuthorization() + self.pendingCompletion = (completion, .authorizedAlways) + } else { + completion(status) + } + } + + public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + if let (pendingCompletion, _) = self.pendingCompletion { + pendingCompletion(status) + self.pendingCompletion = nil + } + } +} diff --git a/submodules/DirectionalPanGesture/Sources/DirectionalPanGestureRecognizer.swift b/submodules/DirectionalPanGesture/Sources/DirectionalPanGestureRecognizer.swift index 74ec8d23ca..ce7a0fc69b 100644 --- a/submodules/DirectionalPanGesture/Sources/DirectionalPanGestureRecognizer.swift +++ b/submodules/DirectionalPanGesture/Sources/DirectionalPanGestureRecognizer.swift @@ -5,6 +5,8 @@ public class DirectionalPanGestureRecognizer: UIPanGestureRecognizer { private var validatedGesture = false private var firstLocation: CGPoint = CGPoint() + public var shouldBegin: ((CGPoint) -> Bool)? + override public init(target: Any?, action: Selector?) { super.init(target: target, action: action) @@ -21,7 +23,13 @@ public class DirectionalPanGestureRecognizer: UIPanGestureRecognizer { super.touchesBegan(touches, with: event) let touch = touches.first! - self.firstLocation = touch.location(in: self.view) + let point = touch.location(in: self.view) + if let shouldBegin = self.shouldBegin, !shouldBegin(point) { + self.state = .failed + return + } + + self.firstLocation = point if let target = self.view?.hitTest(self.firstLocation, with: event) { if target == self.view { diff --git a/submodules/Display/Source/ActionSheetButtonItem.swift b/submodules/Display/Source/ActionSheetButtonItem.swift index 979c265f8a..5c02b3c448 100644 --- a/submodules/Display/Source/ActionSheetButtonItem.swift +++ b/submodules/Display/Source/ActionSheetButtonItem.swift @@ -47,8 +47,8 @@ public class ActionSheetButtonItem: ActionSheetItem { public class ActionSheetButtonNode: ActionSheetItemNode { private let theme: ActionSheetControllerTheme - public static let defaultFont: UIFont = Font.regular(20.0) - public static let boldFont: UIFont = Font.medium(20.0) + private let defaultFont: UIFont + private let boldFont: UIFont private var item: ActionSheetButtonItem? @@ -59,6 +59,9 @@ public class ActionSheetButtonNode: ActionSheetItemNode { override public init(theme: ActionSheetControllerTheme) { self.theme = theme + self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) + self.boldFont = Font.medium(floor(theme.baseFontSize * 20.0 / 17.0)) + self.button = HighlightTrackingButton() self.button.isAccessibilityElement = false @@ -113,9 +116,9 @@ public class ActionSheetButtonNode: ActionSheetItemNode { } switch item.font { case .default: - textFont = ActionSheetButtonNode.defaultFont + textFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) case .bold: - textFont = ActionSheetButtonNode.boldFont + textFont = Font.medium(floor(theme.baseFontSize * 20.0 / 17.0)) } self.label.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: textColor) self.label.isAccessibilityElement = false diff --git a/submodules/Display/Source/ActionSheetCheckboxItem.swift b/submodules/Display/Source/ActionSheetCheckboxItem.swift index ebe4580066..e4472f0539 100644 --- a/submodules/Display/Source/ActionSheetCheckboxItem.swift +++ b/submodules/Display/Source/ActionSheetCheckboxItem.swift @@ -39,7 +39,7 @@ public class ActionSheetCheckboxItem: ActionSheetItem { } public class ActionSheetCheckboxItemNode: ActionSheetItemNode { - public static let defaultFont: UIFont = Font.regular(20.0) + private let defaultFont: UIFont private let theme: ActionSheetControllerTheme @@ -54,6 +54,7 @@ public class ActionSheetCheckboxItemNode: ActionSheetItemNode { override public init(theme: ActionSheetControllerTheme) { self.theme = theme + self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) self.button = HighlightTrackingButton() self.button.isAccessibilityElement = false @@ -117,8 +118,10 @@ public class ActionSheetCheckboxItemNode: ActionSheetItemNode { func setItem(_ item: ActionSheetCheckboxItem) { self.item = item - self.titleNode.attributedText = NSAttributedString(string: item.title, font: ActionSheetCheckboxItemNode.defaultFont, textColor: self.theme.primaryTextColor) - self.labelNode.attributedText = NSAttributedString(string: item.label, font: ActionSheetCheckboxItemNode.defaultFont, textColor: self.theme.secondaryTextColor) + let defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) + + self.titleNode.attributedText = NSAttributedString(string: item.title, font: defaultFont, textColor: self.theme.primaryTextColor) + self.labelNode.attributedText = NSAttributedString(string: item.label, font: defaultFont, textColor: self.theme.secondaryTextColor) self.checkNode.isHidden = !item.value self.accessibilityArea.accessibilityLabel = item.title diff --git a/submodules/Display/Source/ActionSheetSwitchItem.swift b/submodules/Display/Source/ActionSheetSwitchItem.swift index e024b1db26..110a4b4277 100644 --- a/submodules/Display/Source/ActionSheetSwitchItem.swift +++ b/submodules/Display/Source/ActionSheetSwitchItem.swift @@ -42,6 +42,7 @@ public class ActionSheetSwitchNode: ActionSheetItemNode { override public init(theme: ActionSheetControllerTheme) { self.theme = theme + let defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) self.button = HighlightTrackingButton() self.button.isAccessibilityElement = false @@ -85,7 +86,9 @@ public class ActionSheetSwitchNode: ActionSheetItemNode { func setItem(_ item: ActionSheetSwitchItem) { self.item = item - self.label.attributedText = NSAttributedString(string: item.title, font: ActionSheetButtonNode.defaultFont, textColor: self.theme.primaryTextColor) + let defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) + + self.label.attributedText = NSAttributedString(string: item.title, font: defaultFont, textColor: self.theme.primaryTextColor) self.label.isAccessibilityElement = false self.switchNode.isOn = item.isOn diff --git a/submodules/Display/Source/ActionSheetTextItem.swift b/submodules/Display/Source/ActionSheetTextItem.swift index 0d24cc5a47..c49add5114 100644 --- a/submodules/Display/Source/ActionSheetTextItem.swift +++ b/submodules/Display/Source/ActionSheetTextItem.swift @@ -26,7 +26,7 @@ public class ActionSheetTextItem: ActionSheetItem { } public class ActionSheetTextNode: ActionSheetItemNode { - public static let defaultFont: UIFont = Font.regular(13.0) + private let defaultFont: UIFont private let theme: ActionSheetControllerTheme @@ -38,6 +38,7 @@ public class ActionSheetTextNode: ActionSheetItemNode { override public init(theme: ActionSheetControllerTheme) { self.theme = theme + self.defaultFont = Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)) self.label = ImmediateTextNode() self.label.isUserInteractionEnabled = false @@ -60,7 +61,9 @@ public class ActionSheetTextNode: ActionSheetItemNode { func setItem(_ item: ActionSheetTextItem) { self.item = item - self.label.attributedText = NSAttributedString(string: item.title, font: ActionSheetTextNode.defaultFont, textColor: self.theme.secondaryTextColor, paragraphAlignment: .center) + let defaultFont = Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)) + + self.label.attributedText = NSAttributedString(string: item.title, font: defaultFont, textColor: self.theme.secondaryTextColor, paragraphAlignment: .center) self.accessibilityArea.accessibilityLabel = item.title self.setNeedsLayout() diff --git a/submodules/Display/Source/ActionSheetTheme.swift b/submodules/Display/Source/ActionSheetTheme.swift index 1911cb8b3d..89ef25d5f6 100644 --- a/submodules/Display/Source/ActionSheetTheme.swift +++ b/submodules/Display/Source/ActionSheetTheme.swift @@ -21,8 +21,9 @@ public final class ActionSheetControllerTheme: Equatable { public let switchFrameColor: UIColor public let switchContentColor: UIColor public let switchHandleColor: UIColor + public let baseFontSize: CGFloat - public init(dimColor: UIColor, backgroundType: ActionSheetControllerThemeBackgroundType, itemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, standardActionTextColor: UIColor, destructiveActionTextColor: UIColor, disabledActionTextColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlAccentColor: UIColor, controlColor: UIColor, switchFrameColor: UIColor, switchContentColor: UIColor, switchHandleColor: UIColor) { + public init(dimColor: UIColor, backgroundType: ActionSheetControllerThemeBackgroundType, itemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, standardActionTextColor: UIColor, destructiveActionTextColor: UIColor, disabledActionTextColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlAccentColor: UIColor, controlColor: UIColor, switchFrameColor: UIColor, switchContentColor: UIColor, switchHandleColor: UIColor, baseFontSize: CGFloat) { self.dimColor = dimColor self.backgroundType = backgroundType self.itemBackgroundColor = itemBackgroundColor @@ -37,6 +38,7 @@ public final class ActionSheetControllerTheme: Equatable { self.switchFrameColor = switchFrameColor self.switchContentColor = switchContentColor self.switchHandleColor = switchHandleColor + self.baseFontSize = min(26.0, baseFontSize) } public static func ==(lhs: ActionSheetControllerTheme, rhs: ActionSheetControllerTheme) -> Bool { @@ -82,6 +84,9 @@ public final class ActionSheetControllerTheme: Equatable { if lhs.switchHandleColor != rhs.switchHandleColor { return false } + if lhs.baseFontSize != rhs.baseFontSize { + return false + } return true } } diff --git a/submodules/Display/Source/AlertController.swift b/submodules/Display/Source/AlertController.swift index 870c1a2a14..f60748ddf7 100644 --- a/submodules/Display/Source/AlertController.swift +++ b/submodules/Display/Source/AlertController.swift @@ -17,8 +17,9 @@ public final class AlertControllerTheme: Equatable { public let accentColor: UIColor public let destructiveColor: UIColor public let disabledColor: UIColor + public let baseFontSize: CGFloat - public init(backgroundType: ActionSheetControllerThemeBackgroundType, backgroundColor: UIColor, separatorColor: UIColor, highlightedItemColor: UIColor, primaryColor: UIColor, secondaryColor: UIColor, accentColor: UIColor, destructiveColor: UIColor, disabledColor: UIColor) { + public init(backgroundType: ActionSheetControllerThemeBackgroundType, backgroundColor: UIColor, separatorColor: UIColor, highlightedItemColor: UIColor, primaryColor: UIColor, secondaryColor: UIColor, accentColor: UIColor, destructiveColor: UIColor, disabledColor: UIColor, baseFontSize: CGFloat) { self.backgroundType = backgroundType self.backgroundColor = backgroundColor self.separatorColor = separatorColor @@ -28,6 +29,7 @@ public final class AlertControllerTheme: Equatable { self.accentColor = accentColor self.destructiveColor = destructiveColor self.disabledColor = disabledColor + self.baseFontSize = baseFontSize } public static func ==(lhs: AlertControllerTheme, rhs: AlertControllerTheme) -> Bool { @@ -58,6 +60,9 @@ public final class AlertControllerTheme: Equatable { if lhs.disabledColor != rhs.disabledColor { return false } + if lhs.baseFontSize != rhs.baseFontSize { + return false + } return true } } diff --git a/submodules/Display/Source/CAAnimationUtils.swift b/submodules/Display/Source/CAAnimationUtils.swift index 5462e3839b..6eb000add6 100644 --- a/submodules/Display/Source/CAAnimationUtils.swift +++ b/submodules/Display/Source/CAAnimationUtils.swift @@ -72,7 +72,7 @@ public extension CALayer { animation.isAdditive = additive if !delay.isZero { - animation.beginTime = CACurrentMediaTime() + delay + animation.beginTime = CACurrentMediaTime() + delay * UIView.animationDurationFactor() animation.fillMode = .both } @@ -102,7 +102,7 @@ public extension CALayer { } if !delay.isZero { - animation.beginTime = CACurrentMediaTime() + delay + animation.beginTime = CACurrentMediaTime() + delay * UIView.animationDurationFactor() animation.fillMode = .both } @@ -179,7 +179,7 @@ public extension CALayer { } if !delay.isZero { - animation.beginTime = CACurrentMediaTime() + delay + animation.beginTime = CACurrentMediaTime() + delay * UIView.animationDurationFactor() animation.fillMode = .both } diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 32ef80c7b4..7635ad88ba 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -141,6 +141,27 @@ public extension ContainedViewLayoutTransition { } } + func updateFrameAdditiveToCenter(node: ASDisplayNode, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if node.frame.equalTo(frame) && !force { + completion?(true) + } else { + switch self { + case .immediate: + node.position = frame.center + node.bounds = CGRect(origin: node.bounds.origin, size: frame.size) + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousBounds = node.bounds + let previousCenter = node.frame.center + node.position = frame.center + node.bounds = CGRect(origin: node.bounds.origin, size: frame.size) + self.animatePositionAdditive(node: node, offset: CGPoint(x: previousCenter.x - frame.midX, y: previousCenter.y - frame.midY)) + } + } + } + func updateBounds(node: ASDisplayNode, bounds: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.bounds.equalTo(bounds) && !force { completion?(true) @@ -330,7 +351,7 @@ public extension ContainedViewLayoutTransition { func animatePositionAdditive(node: ASDisplayNode, offset: CGFloat, removeOnCompletion: Bool = true, completion: @escaping (Bool) -> Void) { switch self { case .immediate: - break + completion(true) case let .animated(duration, curve): node.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion) } @@ -339,7 +360,7 @@ public extension ContainedViewLayoutTransition { func animatePositionAdditive(layer: CALayer, offset: CGFloat, removeOnCompletion: Bool = true, completion: @escaping (Bool) -> Void) { switch self { case .immediate: - break + completion(true) case let .animated(duration, curve): layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion) } @@ -348,7 +369,7 @@ public extension ContainedViewLayoutTransition { func animatePositionAdditive(node: ASDisplayNode, offset: CGPoint, removeOnCompletion: Bool = true, completion: (() -> Void)? = nil) { switch self { case .immediate: - break + completion?() case let .animated(duration, curve): node.layer.animatePosition(from: offset, to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { _ in completion?() @@ -359,7 +380,7 @@ public extension ContainedViewLayoutTransition { func animatePositionAdditive(layer: CALayer, offset: CGPoint, to toOffset: CGPoint = CGPoint(), removeOnCompletion: Bool = true, completion: (() -> Void)? = nil) { switch self { case .immediate: - break + completion?() case let .animated(duration, curve): layer.animatePosition(from: offset, to: toOffset, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { _ in completion?() @@ -546,6 +567,30 @@ public extension ContainedViewLayoutTransition { } } + func animateTransformScale(view: UIView, from fromScale: CGFloat, completion: ((Bool) -> Void)? = nil) { + let t = view.layer.transform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + if currentScale.isEqual(to: fromScale) { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + view.layer.animateScale(from: fromScale, to: currentScale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + func updateTransformScale(node: ASDisplayNode, scale: CGFloat, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { let t = node.layer.transform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) @@ -637,6 +682,40 @@ public extension ContainedViewLayoutTransition { } } + func updateSublayerTransformScaleAdditive(node: ASDisplayNode, scale: CGFloat, completion: ((Bool) -> Void)? = nil) { + if !node.isNodeLoaded { + node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0) + completion?(true) + return + } + let t = node.layer.sublayerTransform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + if currentScale.isEqual(to: scale) { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0) + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let t = node.layer.sublayerTransform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0) + node.layer.animate(from: -(scale - currentScale) as NSNumber, to: 0.0 as NSNumber, keyPath: "sublayerTransform.scale", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: true, completion: { + result in + if let completion = completion { + completion(result) + } + }) + } + } + func updateSublayerTransformScaleAndOffset(node: ASDisplayNode, scale: CGFloat, offset: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if !node.isNodeLoaded { node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0) @@ -808,6 +887,39 @@ public extension ContainedViewLayoutTransition { }) } } + + func updateTransformRotation(node: ASDisplayNode, angle: CGFloat, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { + let t = node.layer.transform + let currentAngle = atan2(t.m12, t.m11) + if currentAngle.isEqual(to: angle) { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + node.layer.transform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0) + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousAngle: CGFloat + if beginWithCurrentState, let presentation = node.layer.presentation() { + let t = presentation.transform + previousAngle = atan2(t.m12, t.m11) + } else { + previousAngle = currentAngle + } + node.layer.transform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0) + node.layer.animateRotation(from: previousAngle, to: angle, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } } #if os(iOS) diff --git a/submodules/Display/Source/DeviceMetrics.swift b/submodules/Display/Source/DeviceMetrics.swift index ac0304f736..a8371644f8 100644 --- a/submodules/Display/Source/DeviceMetrics.swift +++ b/submodules/Display/Source/DeviceMetrics.swift @@ -182,7 +182,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { case .iPhone4, .iPhone5, .iPhone6: return 216.0 case .iPhone6Plus: - return 227.0 + return 226.0 case .iPhoneX: return 291.0 case .iPhoneXSMax: @@ -215,8 +215,10 @@ public enum DeviceMetrics: CaseIterable, Equatable { switch self { case .iPhone4, .iPhone5: return 37.0 - case .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax: + case .iPhone6, .iPhoneX, .iPhoneXSMax: return 44.0 + case .iPhone6Plus: + return 45.0 case .iPad, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen: return 50.0 case .unknown: diff --git a/submodules/Display/Source/GenerateImage.swift b/submodules/Display/Source/GenerateImage.swift index 0994683eb9..4b7f38fbe9 100644 --- a/submodules/Display/Source/GenerateImage.swift +++ b/submodules/Display/Source/GenerateImage.swift @@ -238,6 +238,8 @@ public func generateStretchableFilledCircleImage(diameter: CGFloat, color: UICol let cap: Int if intDiameter == 3 { cap = 1 + } else if intDiameter == 2 { + cap = 3 } else if intRadius == 1 { cap = 2 } else { @@ -266,6 +268,35 @@ public func generateVerticallyStretchableFilledCircleImage(radius: CGFloat, colo })?.stretchableImage(withLeftCapWidth: Int(radius), topCapHeight: Int(radius)) } +public func generateSmallHorizontalStretchableFilledCircleImage(diameter: CGFloat, color: UIColor?, backgroundColor: UIColor? = nil) -> UIImage? { + return generateImage(CGSize(width: diameter + 1.0, height: diameter), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + if let subImage = generateImage(CGSize(width: diameter + 1.0, height: diameter), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.black.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: diameter, height: diameter))) + context.fill(CGRect(origin: CGPoint(x: diameter / 2.0, y: 0.0), size: CGSize(width: 1.0, height: diameter))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 1.0, y: 0.0), size: CGSize(width: diameter, height: diameter))) + }) { + if let backgroundColor = backgroundColor { + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + } + + if let color = color { + context.setFillColor(color.cgColor) + } else { + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + } + + context.clip(to: CGRect(origin: CGPoint(), size: size), mask: subImage.cgImage!) + context.fill(CGRect(origin: CGPoint(), size: size)) + } + })?.stretchableImage(withLeftCapWidth: Int(diameter / 2), topCapHeight: Int(diameter / 2)) +} + public func generateTintedImage(image: UIImage?, color: UIColor, backgroundColor: UIColor? = nil) -> UIImage? { guard let image = image else { return nil @@ -297,6 +328,43 @@ public func generateTintedImage(image: UIImage?, color: UIColor, backgroundColor return tintedImage } +public func generateGradientTintedImage(image: UIImage?, colors: [UIColor]) -> UIImage? { + guard let image = image else { + return nil + } + + let imageSize = image.size + + UIGraphicsBeginImageContextWithOptions(imageSize, false, image.scale) + if let context = UIGraphicsGetCurrentContext() { + let imageRect = CGRect(origin: CGPoint(), size: imageSize) + context.saveGState() + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + context.clip(to: imageRect, mask: image.cgImage!) + + let gradientColors = colors.map { $0.cgColor } as CFArray + let delta: CGFloat = 1.0 / (CGFloat(colors.count) - 1.0) + + var locations: [CGFloat] = [] + for i in 0 ..< colors.count { + locations.append(delta * CGFloat(i)) + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: imageRect.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions()) + + context.restoreGState() + } + + let tintedImage = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + + return tintedImage +} + public func generateScaledImage(image: UIImage?, size: CGSize, opaque: Bool = true, scale: CGFloat? = nil) -> UIImage? { guard let image = image else { return nil diff --git a/submodules/Display/Source/GlobalOverlayPresentationContext.swift b/submodules/Display/Source/GlobalOverlayPresentationContext.swift index b289ca56de..127aa07ddd 100644 --- a/submodules/Display/Source/GlobalOverlayPresentationContext.swift +++ b/submodules/Display/Source/GlobalOverlayPresentationContext.swift @@ -23,10 +23,10 @@ func isViewVisibleInHierarchy(_ view: UIView, _ initial: Bool = true) -> Bool { } } -private final class HierarchyTrackingNode: ASDisplayNode { +public final class HierarchyTrackingNode: ASDisplayNode { private let f: (Bool) -> Void - init(_ f: @escaping (Bool) -> Void) { + public init(_ f: @escaping (Bool) -> Void) { self.f = f super.init() @@ -34,13 +34,13 @@ private final class HierarchyTrackingNode: ASDisplayNode { self.isLayerBacked = true } - override func didEnterHierarchy() { + override public func didEnterHierarchy() { super.didEnterHierarchy() self.f(true) } - override func didExitHierarchy() { + override public func didExitHierarchy() { super.didExitHierarchy() self.f(false) diff --git a/submodules/Display/Source/GridNode.swift b/submodules/Display/Source/GridNode.swift index 304b3c6f30..754fe54029 100644 --- a/submodules/Display/Source/GridNode.swift +++ b/submodules/Display/Source/GridNode.swift @@ -225,6 +225,8 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { public var presentationLayoutUpdated: ((GridNodeCurrentPresentationLayout, ContainedViewLayoutTransition) -> Void)? public var scrollingInitiated: (() -> Void)? public var scrollingCompleted: (() -> Void)? + public var interactiveScrollingEnded: (() -> Void)? + public var interactiveScrollingWillBeEnded: ((CGPoint, CGPoint) -> Void)? public var visibleContentOffsetChanged: (GridNodeVisibleContentOffset) -> Void = { _ in } public final var floatingSections = false @@ -373,7 +375,12 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { self.scrollingInitiated?() } + public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + self.interactiveScrollingWillBeEnded?(velocity, targetContentOffset.pointee) + } + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + self.interactiveScrollingEnded?() if !decelerate { self.updateItemNodeVisibilititesAndScrolling() self.updateVisibleContentOffset() @@ -478,7 +485,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { } else if let fillWidth = fillWidth, fillWidth { let nextItemOriginX = nextItemOrigin.x + itemSize.width + itemSpacing let remainingWidth = remainingWidth - CGFloat(itemsInRow - 1) * itemSpacing - if nextItemOriginX + itemSize.width > self.gridLayout.size.width && remainingWidth > 0.0 { + if nextItemOriginX + itemSize.width > self.gridLayout.size.width - itemInsets.right && remainingWidth > 0.0 { itemSize.width += remainingWidth } } @@ -492,7 +499,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { index += 1 nextItemOrigin.x += itemSize.width + itemSpacing - if nextItemOrigin.x + itemSize.width > gridLayout.size.width { + if nextItemOrigin.x + itemSize.width > gridLayout.size.width - itemInsets.right { nextItemOrigin.x = initialSpacing + itemInsets.left nextItemOrigin.y += itemSize.height + lineSpacing incrementedCurrentRow = false diff --git a/submodules/Display/Source/ImageCorners.swift b/submodules/Display/Source/ImageCorners.swift new file mode 100644 index 0000000000..948a4d385d --- /dev/null +++ b/submodules/Display/Source/ImageCorners.swift @@ -0,0 +1,216 @@ +import Foundation +import UIKit +import CoreGraphics +import SwiftSignalKit + +private enum Corner: Hashable { + case TopLeft(Int), TopRight(Int), BottomLeft(Int), BottomRight(Int) + + var hashValue: Int { + switch self { + case let .TopLeft(radius): + return radius | (1 << 24) + case let .TopRight(radius): + return radius | (2 << 24) + case let .BottomLeft(radius): + return radius | (3 << 24) + case let .BottomRight(radius): + return radius | (4 << 24) + } + } + + var radius: Int { + switch self { + case let .TopLeft(radius): + return radius + case let .TopRight(radius): + return radius + case let .BottomLeft(radius): + return radius + case let .BottomRight(radius): + return radius + } + } +} + +private func ==(lhs: Corner, rhs: Corner) -> Bool { + switch lhs { + case let .TopLeft(lhsRadius): + switch rhs { + case let .TopLeft(rhsRadius) where rhsRadius == lhsRadius: + return true + default: + return false + } + case let .TopRight(lhsRadius): + switch rhs { + case let .TopRight(rhsRadius) where rhsRadius == lhsRadius: + return true + default: + return false + } + case let .BottomLeft(lhsRadius): + switch rhs { + case let .BottomLeft(rhsRadius) where rhsRadius == lhsRadius: + return true + default: + return false + } + case let .BottomRight(lhsRadius): + switch rhs { + case let .BottomRight(rhsRadius) where rhsRadius == lhsRadius: + return true + default: + return false + } + } +} + +private enum Tail: Hashable { + case BottomLeft(Int) + case BottomRight(Int) + + var hashValue: Int { + switch self { + case let .BottomLeft(radius): + return radius | (1 << 24) + case let .BottomRight(radius): + return radius | (2 << 24) + } + } + + var radius: Int { + switch self { + case let .BottomLeft(radius): + return radius + case let .BottomRight(radius): + return radius + } + } +} + +private func ==(lhs: Tail, rhs: Tail) -> Bool { + switch lhs { + case let .BottomLeft(lhsRadius): + switch rhs { + case let .BottomLeft(rhsRadius) where rhsRadius == lhsRadius: + return true + default: + return false + } + case let .BottomRight(lhsRadius): + switch rhs { + case let .BottomRight(rhsRadius) where rhsRadius == lhsRadius: + return true + default: + return false + } + } +} + +private var cachedCorners = Atomic<[Corner: DrawingContext]>(value: [:]) + +private func cornerContext(_ corner: Corner) -> DrawingContext { + let cached: DrawingContext? = cachedCorners.with { + return $0[corner] + } + + if let cached = cached { + return cached + } else { + let context = DrawingContext(size: CGSize(width: CGFloat(corner.radius), height: CGFloat(corner.radius)), clear: true) + + context.withContext { c in + c.clear(CGRect(origin: CGPoint(), size: CGSize(width: CGFloat(corner.radius), height: CGFloat(corner.radius)))) + c.setFillColor(UIColor.black.cgColor) + switch corner { + case let .TopLeft(radius): + let rect = CGRect(origin: CGPoint(), size: CGSize(width: CGFloat(radius * 2), height: CGFloat(radius * 2))) + c.fillEllipse(in: rect) + case let .TopRight(radius): + let rect = CGRect(origin: CGPoint(x: -CGFloat(radius), y: 0.0), size: CGSize(width: CGFloat(radius * 2), height: CGFloat(radius * 2))) + c.fillEllipse(in: rect) + case let .BottomLeft(radius): + let rect = CGRect(origin: CGPoint(x: 0.0, y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius * 2), height: CGFloat(radius * 2))) + c.fillEllipse(in: rect) + case let .BottomRight(radius): + let rect = CGRect(origin: CGPoint(x: -CGFloat(radius), y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius * 2), height: CGFloat(radius * 2))) + c.fillEllipse(in: rect) + } + } + + let _ = cachedCorners.modify { current in + var current = current + current[corner] = context + return current + } + + return context + } +} + +public func addCorners(_ context: DrawingContext, arguments: TransformImageArguments) { + let corners = arguments.corners + let drawingRect = arguments.drawingRect + if case let .Corner(radius) = corners.topLeft, radius > CGFloat.ulpOfOne { + let corner = cornerContext(.TopLeft(Int(radius))) + context.blt(corner, at: CGPoint(x: drawingRect.minX, y: drawingRect.minY)) + } + + if case let .Corner(radius) = corners.topRight, radius > CGFloat.ulpOfOne { + let corner = cornerContext(.TopRight(Int(radius))) + context.blt(corner, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.minY)) + } + + switch corners.bottomLeft { + case let .Corner(radius): + if radius > CGFloat.ulpOfOne { + let corner = cornerContext(.BottomLeft(Int(radius))) + context.blt(corner, at: CGPoint(x: drawingRect.minX, y: drawingRect.maxY - radius)) + } + case let .Tail(radius, image): + if radius > CGFloat.ulpOfOne { + let color = context.colorAt(CGPoint(x: drawingRect.minX, y: drawingRect.maxY - 1.0)) + context.withContext { c in + c.clear(CGRect(x: drawingRect.minX - 4.0, y: 0.0, width: 4.0, height: drawingRect.maxY - 6.0)) + c.setFillColor(color.cgColor) + c.fill(CGRect(x: 0.0, y: drawingRect.maxY - 7.0, width: 4.0, height: 7.0)) + c.setBlendMode(.destinationIn) + let cornerRect = CGRect(origin: CGPoint(x: drawingRect.minX - 6.0, y: drawingRect.maxY - image.size.height), size: image.size) + c.translateBy(x: cornerRect.midX, y: cornerRect.midY) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -cornerRect.midX, y: -cornerRect.midY) + c.draw(image.cgImage!, in: cornerRect) + c.translateBy(x: cornerRect.midX, y: cornerRect.midY) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -cornerRect.midX, y: -cornerRect.midY) + } + } + } + + switch corners.bottomRight { + case let .Corner(radius): + if radius > CGFloat.ulpOfOne { + let corner = cornerContext(.BottomRight(Int(radius))) + context.blt(corner, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.maxY - radius)) + } + case let .Tail(radius, image): + if radius > CGFloat.ulpOfOne { + let color = context.colorAt(CGPoint(x: drawingRect.maxX - 1.0, y: drawingRect.maxY - 1.0)) + context.withContext { c in + c.clear(CGRect(x: drawingRect.maxX, y: 0.0, width: 4.0, height: drawingRect.maxY - image.size.height)) + c.setFillColor(color.cgColor) + c.fill(CGRect(x: drawingRect.maxX, y: drawingRect.maxY - 7.0, width: 5.0, height: 7.0)) + c.setBlendMode(.destinationIn) + let cornerRect = CGRect(origin: CGPoint(x: drawingRect.maxX - image.size.width + 6.0, y: drawingRect.maxY - image.size.height), size: image.size) + c.translateBy(x: cornerRect.midX, y: cornerRect.midY) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -cornerRect.midX, y: -cornerRect.midY) + c.draw(image.cgImage!, in: cornerRect) + c.translateBy(x: cornerRect.midX, y: cornerRect.midY) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -cornerRect.midX, y: -cornerRect.midY) + } + } + } +} diff --git a/submodules/Display/Source/ImageNode.swift b/submodules/Display/Source/ImageNode.swift index 99192e4895..87422abbc6 100644 --- a/submodules/Display/Source/ImageNode.swift +++ b/submodules/Display/Source/ImageNode.swift @@ -8,12 +8,12 @@ private let dispatcher = displayLinkDispatcher public enum ImageCorner: Equatable { case Corner(CGFloat) - case Tail(CGFloat, Bool) + case Tail(CGFloat, UIImage) public var extendedInsets: CGSize { switch self { case .Tail: - return CGSize(width: 3.0, height: 0.0) + return CGSize(width: 4.0, height: 0.0) default: return CGSize() } @@ -36,15 +36,6 @@ public enum ImageCorner: Equatable { return radius } } - - public func scaledBy(_ scale: CGFloat) -> ImageCorner { - switch self { - case let .Corner(radius): - return .Corner(radius * scale) - case let .Tail(radius, enabled): - return .Tail(radius * scale, enabled) - } - } } public func ==(lhs: ImageCorner, rhs: ImageCorner) -> Bool { @@ -56,8 +47,8 @@ public func ==(lhs: ImageCorner, rhs: ImageCorner) -> Bool { default: return false } - case let .Tail(lhsRadius, lhsEnabled): - if case let .Tail(rhsRadius, rhsEnabled) = rhs, lhsRadius.isEqual(to: rhsRadius), lhsEnabled == rhsEnabled { + case let .Tail(lhsRadius, lhsImage): + if case let .Tail(rhsRadius, rhsImage) = rhs, lhsRadius.isEqual(to: rhsRadius), lhsImage === rhsImage { return true } else { return false @@ -124,10 +115,6 @@ public struct ImageCorners: Equatable { public func withRemovedTails() -> ImageCorners { return ImageCorners(topLeft: self.topLeft.withoutTail, topRight: self.topRight.withoutTail, bottomLeft: self.bottomLeft.withoutTail, bottomRight: self.bottomRight.withoutTail) } - - public func scaledBy(_ scale: CGFloat) -> ImageCorners { - return ImageCorners(topLeft: self.topLeft.scaledBy(scale), topRight: self.topRight.scaledBy(scale), bottomLeft: self.bottomLeft.scaledBy(scale), bottomRight: self.bottomRight.scaledBy(scale)) - } } public func ==(lhs: ImageCorners, rhs: ImageCorners) -> Bool { diff --git a/submodules/Display/Source/ImmediateTextNode.swift b/submodules/Display/Source/ImmediateTextNode.swift index 1493c5ff8b..f7e8dfc340 100644 --- a/submodules/Display/Source/ImmediateTextNode.swift +++ b/submodules/Display/Source/ImmediateTextNode.swift @@ -14,6 +14,8 @@ public class ImmediateTextNode: TextNode { public var lineSpacing: CGFloat = 0.0 public var insets: UIEdgeInsets = UIEdgeInsets() public var textShadowColor: UIColor? + public var textStroke: (UIColor, CGFloat)? + public var cutout: TextNodeCutout? private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? private var linkHighlightingNode: LinkHighlightingNode? @@ -33,9 +35,30 @@ public class ImmediateTextNode: TextNode { public var tapAttributeAction: (([NSAttributedString.Key: Any]) -> Void)? public var longTapAttributeAction: (([NSAttributedString.Key: Any]) -> Void)? + public func makeCopy() -> TextNode { + let node = TextNode() + node.cachedLayout = self.cachedLayout + node.frame = self.frame + if let subnodes = self.subnodes { + for subnode in subnodes { + if let subnode = subnode as? ASImageNode { + let copySubnode = ASImageNode() + copySubnode.isLayerBacked = subnode.isLayerBacked + copySubnode.image = subnode.image + copySubnode.displaysAsynchronously = false + copySubnode.displayWithoutProcessing = true + copySubnode.frame = subnode.frame + copySubnode.alpha = subnode.alpha + node.addSubnode(copySubnode) + } + } + } + return node + } + public func updateLayout(_ constrainedSize: CGSize) -> CGSize { let makeLayout = TextNode.asyncLayout(self) - let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: nil, insets: self.insets, textShadowColor: self.textShadowColor)) + let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, textShadowColor: self.textShadowColor, textStroke: self.textStroke)) let _ = apply() if layout.numberOfLines > 1 { self.trailingLineWidth = layout.trailingLineWidth @@ -47,7 +70,7 @@ public class ImmediateTextNode: TextNode { public func updateLayoutInfo(_ constrainedSize: CGSize) -> ImmediateTextNodeLayoutInfo { let makeLayout = TextNode.asyncLayout(self) - let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: nil, insets: self.insets)) + let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets)) let _ = apply() return ImmediateTextNodeLayoutInfo(size: layout.size, truncated: layout.truncated) } diff --git a/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift b/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift index 25b3a4db81..6462a0d325 100644 --- a/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift +++ b/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift @@ -5,6 +5,9 @@ private func hasHorizontalGestures(_ view: UIView, point: CGPoint?) -> Bool { if view.disablesInteractiveTransitionGestureRecognizer { return true } + if let disablesInteractiveTransitionGestureRecognizerNow = view.disablesInteractiveTransitionGestureRecognizerNow, disablesInteractiveTransitionGestureRecognizerNow() { + return true + } if let point = point, let test = view.interactiveTransitionGestureRecognizerTest, test(point) { return true @@ -28,27 +31,42 @@ private func hasHorizontalGestures(_ view: UIView, point: CGPoint?) -> Bool { } } -class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { - var validatedGesture = false - var firstLocation: CGPoint = CGPoint() - private let canBegin: () -> Bool +public struct InteractiveTransitionGestureRecognizerDirections: OptionSet { + public var rawValue: Int - init(target: Any?, action: Selector?, canBegin: @escaping () -> Bool) { - self.canBegin = canBegin + public init(rawValue: Int) { + self.rawValue = rawValue + } + + public static let left = InteractiveTransitionGestureRecognizerDirections(rawValue: 1 << 0) + public static let right = InteractiveTransitionGestureRecognizerDirections(rawValue: 1 << 1) +} + +public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { + private let allowedDirections: () -> InteractiveTransitionGestureRecognizerDirections + + private var validatedGesture = false + private var firstLocation: CGPoint = CGPoint() + private var currentAllowedDirections: InteractiveTransitionGestureRecognizerDirections = [] + + public init(target: Any?, action: Selector?, allowedDirections: @escaping () -> InteractiveTransitionGestureRecognizerDirections) { + self.allowedDirections = allowedDirections super.init(target: target, action: action) self.maximumNumberOfTouches = 1 } - override func reset() { + override public func reset() { super.reset() - validatedGesture = false + self.validatedGesture = false + self.currentAllowedDirections = [] } - override func touchesBegan(_ touches: Set, with event: UIEvent) { - if !self.canBegin() { + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + self.currentAllowedDirections = self.allowedDirections() + if self.currentAllowedDirections.isEmpty { self.state = .failed return } @@ -65,22 +83,24 @@ class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { } } - override func touchesMoved(_ touches: Set, with event: UIEvent) { + override public func touchesMoved(_ touches: Set, with event: UIEvent) { let location = touches.first!.location(in: self.view) let translation = CGPoint(x: location.x - firstLocation.x, y: location.y - firstLocation.y) let absTranslationX: CGFloat = abs(translation.x) let absTranslationY: CGFloat = abs(translation.y) - if !validatedGesture { - if self.firstLocation.x < 16.0 { - validatedGesture = true - } else if translation.x < 0.0 { + if !self.validatedGesture { + if self.currentAllowedDirections.contains(.right) && self.firstLocation.x < 16.0 { + self.validatedGesture = true + } else if !self.currentAllowedDirections.contains(.left) && translation.x < 0.0 { + self.state = .failed + } else if !self.currentAllowedDirections.contains(.right) && translation.x > 0.0 { self.state = .failed } else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 { self.state = .failed } else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX { - validatedGesture = true + self.validatedGesture = true } } diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 351cc6e0ee..0fca0955f9 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -61,6 +61,12 @@ public final class ListViewBackingView: UIView { override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.isHidden, let target = self.target { + if target.bounds.contains(point) { + if target.decelerationAnimator != nil { + target.decelerationAnimator?.isPaused = true + target.decelerationAnimator = nil + } + } if target.limitHitTestToNodes, !target.internalHitTest(point, with: event) { return nil } @@ -126,7 +132,7 @@ public enum GeneralScrollDirection { } open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGestureRecognizerDelegate { - final let scroller: ListViewScroller + public final let scroller: ListViewScroller private final var visibleSize: CGSize = CGSize() public private(set) final var insets = UIEdgeInsets() public final var visualInsets: UIEdgeInsets? @@ -231,6 +237,22 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture public final var synchronousNodes = false public final var debugInfo = false + public var enableExtractedBackgrounds: Bool = false { + didSet { + if self.enableExtractedBackgrounds != oldValue { + if self.enableExtractedBackgrounds { + let extractedBackgroundsContainerNode = ASDisplayNode() + self.extractedBackgroundsContainerNode = extractedBackgroundsContainerNode + self.insertSubnode(extractedBackgroundsContainerNode, at: 0) + } else if let extractedBackgroundsContainerNode = self.extractedBackgroundsContainerNode { + self.extractedBackgroundsContainerNode = nil + extractedBackgroundsContainerNode.removeFromSupernode() + } + } + } + } + private final var extractedBackgroundsContainerNode: ASDisplayNode? + private final var items: [ListViewItem] = [] private final var itemNodes: [ListViewItemNode] = [] private final var itemHeaderNodes: [Int64: ListViewItemHeaderNode] = [:] @@ -271,6 +293,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture private var reorderNode: ListViewReorderingItemNode? private var reorderFeedback: HapticFeedback? private var reorderFeedbackDisposable: MetaDisposable? + private var isReorderingItems: Bool = false + private var reorderingItemsCompleted: (() -> Void)? private let waitingForNodesDisposable = MetaDisposable() @@ -426,22 +450,41 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self.addSubnode(reorderNode) } itemNode.isHidden = true + + if self.reorderFeedback == nil { + self.reorderFeedback = HapticFeedback() + } + self.reorderFeedback?.impact() } private func endReordering() { - if let reorderNode = self.reorderNode { - self.reorderNode = nil - if let itemNode = reorderNode.itemNode, itemNode.supernode == self { - self.reorderItemNodeToFront(itemNode) - reorderNode.animateCompletion(completion: { [weak reorderNode] in - reorderNode?.removeFromSupernode() - }) - self.setNeedsAnimations() - } else { - reorderNode.removeFromSupernode() + self.itemReorderingTimer?.invalidate() + self.itemReorderingTimer = nil + self.lastReorderingOffset = nil + + let f: () -> Void = { [weak self] in + guard let strongSelf = self else { + return } + if let reorderNode = strongSelf.reorderNode { + strongSelf.reorderNode = nil + if let itemNode = reorderNode.itemNode, itemNode.supernode == strongSelf { + strongSelf.reorderItemNodeToFront(itemNode) + reorderNode.animateCompletion(completion: { [weak reorderNode] in + reorderNode?.removeFromSupernode() + }) + strongSelf.setNeedsAnimations() + } else { + reorderNode.removeFromSupernode() + } + } + strongSelf.reorderCompleted(strongSelf.opaqueTransactionState) + } + if self.isReorderingItems { + self.reorderingItemsCompleted = f + } else { + f() } - self.reorderCompleted(self.opaqueTransactionState) } private func updateReordering(offset: CGFloat) { @@ -451,11 +494,32 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } - private func checkItemReordering() { - if let reorderNode = self.reorderNode, let reorderItemNode = reorderNode.itemNode, let reorderItemIndex = reorderItemNode.index, reorderItemNode.supernode == self { - guard let verticalTopOffset = reorderNode.currentOffset() else { - return - } + private var itemReorderingTimer: SwiftSignalKit.Timer? + private var lastReorderingOffset: CGFloat? + + private func checkItemReordering(force: Bool = false) { + guard let reorderNode = self.reorderNode, let verticalTopOffset = reorderNode.currentOffset() else { + return + } + + if let lastReorderingOffset = self.lastReorderingOffset, abs(lastReorderingOffset - verticalTopOffset) < 4.0 && !force { + return + } + + self.itemReorderingTimer?.invalidate() + self.itemReorderingTimer = nil + + self.lastReorderingOffset = verticalTopOffset + + if !force { + self.itemReorderingTimer = SwiftSignalKit.Timer(timeout: 0.025, repeat: false, completion: { [weak self] in + self?.checkItemReordering(force: true) + }, queue: Queue.mainQueue()) + self.itemReorderingTimer?.start() + return + } + + if let reorderItemNode = reorderNode.itemNode, let reorderItemIndex = reorderItemNode.index, reorderItemNode.supernode == self { let verticalOffset = verticalTopOffset var closestIndex: (Int, CGFloat)? for i in 0 ..< self.itemNodes.count { @@ -493,9 +557,20 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture if self.reorderFeedbackDisposable == nil { self.reorderFeedbackDisposable = MetaDisposable() } + self.isReorderingItems = true self.reorderFeedbackDisposable?.set((self.reorderItem(reorderItemIndex, toIndex, self.opaqueTransactionState) |> deliverOnMainQueue).start(next: { [weak self] value in - guard let strongSelf = self, value else { + guard let strongSelf = self else { + return + } + + strongSelf.isReorderingItems = false + if let reorderingItemsCompleted = strongSelf.reorderingItemsCompleted { + strongSelf.reorderingItemsCompleted = nil + reorderingItemsCompleted() + } + + if !value { return } if strongSelf.reorderFeedback == nil { @@ -617,6 +692,48 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } + fileprivate var decelerationAnimator: ConstantDisplayLinkAnimator? + private var accumulatedTransferVelocityOffset: CGFloat = 0.0 + + public func transferVelocity(_ velocity: CGFloat) { + self.decelerationAnimator?.isPaused = true + let startTime = CACurrentMediaTime() + let decelerationRate: CGFloat = 0.998 + self.decelerationAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in + guard let strongSelf = self else { + return + } + let t = CACurrentMediaTime() - startTime + var currentVelocity = velocity * 15.0 * CGFloat(pow(Double(decelerationRate), 1000.0 * t)) + strongSelf.accumulatedTransferVelocityOffset += currentVelocity + let signFactor: CGFloat = strongSelf.accumulatedTransferVelocityOffset >= 0.0 ? 1.0 : -1.0 + let remainder = abs(strongSelf.accumulatedTransferVelocityOffset).remainder(dividingBy: UIScreenPixel) + //print("accumulated \(strongSelf.accumulatedTransferVelocityOffset), \(remainder), resulting accumulated \(strongSelf.accumulatedTransferVelocityOffset - remainder * signFactor) add delta \(strongSelf.accumulatedTransferVelocityOffset - remainder * signFactor)") + var currentOffset = strongSelf.scroller.contentOffset + let addedDela = strongSelf.accumulatedTransferVelocityOffset - remainder * signFactor + currentOffset.y += addedDela + strongSelf.accumulatedTransferVelocityOffset -= addedDela + let maxOffset = strongSelf.scroller.contentSize.height - strongSelf.scroller.bounds.height + if currentOffset.y >= maxOffset { + currentOffset.y = maxOffset + currentVelocity = 0.0 + } + if currentOffset.y < 0.0 { + currentOffset.y = 0.0 + currentVelocity = 0.0 + } + + if abs(currentVelocity) < 0.1 { + strongSelf.decelerationAnimator?.isPaused = true + strongSelf.decelerationAnimator = nil + } + var contentOffset = strongSelf.scroller.contentOffset + contentOffset.y = floorToScreenPixels(currentOffset.y) + strongSelf.scroller.setContentOffset(contentOffset, animated: false) + }) + self.decelerationAnimator?.isPaused = false + } + public func scrollViewDidScroll(_ scrollView: UIScrollView) { self.updateScrollViewDidScroll(scrollView, synchronous: false) } @@ -831,7 +948,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture var bottomItemEdge: CGFloat = 0.0 for i in 0 ..< self.itemNodes.count { - if let index = itemNodes[i].index { + if let index = self.itemNodes[i].index { if index == 0 { topItemFound = true } @@ -846,12 +963,12 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } if topItemFound { - topItemEdge = itemNodes[0].apparentFrame.origin.y + topItemEdge = self.itemNodes[0].apparentFrame.origin.y } var bottomItemNode: ListViewItemNode? for i in (0 ..< self.itemNodes.count).reversed() { - if let index = itemNodes[i].index { + if let index = self.itemNodes[i].index { if index == self.items.count - 1 { bottomItemNode = itemNodes[i] bottomItemFound = true @@ -861,7 +978,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } if bottomItemFound { - bottomItemEdge = itemNodes[itemNodes.count - 1].apparentFrame.maxY + bottomItemEdge = self.itemNodes[self.itemNodes.count - 1].apparentFrame.maxY } if topItemFound && bottomItemFound { @@ -922,7 +1039,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } else { offset = effectiveInsets.top + areaHeight - overscroll - bottomItemEdge } - } else if topItemEdge > effectiveInsets.top - overscroll && /*snapTopItem*/ !self.isTracking { + } else if topItemEdge > effectiveInsets.top - overscroll { offset = (effectiveInsets.top - overscroll) - topItemEdge } } @@ -1043,7 +1160,11 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture topItemOverscrollBackground = ListViewOverscrollBackgroundNode(color: value.color) topItemOverscrollBackground.isLayerBacked = true self.topItemOverscrollBackground = topItemOverscrollBackground - self.insertSubnode(topItemOverscrollBackground, at: 0) + if let extractedBackgroundsContainerNode = self.extractedBackgroundsContainerNode { + self.insertSubnode(topItemOverscrollBackground, aboveSubnode: extractedBackgroundsContainerNode) + } else { + self.insertSubnode(topItemOverscrollBackground, at: 0) + } } var topItemFound = false var topItemNodeIndex: Int? @@ -1151,7 +1272,11 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture bottomItemOverscrollBackground = ASDisplayNode() bottomItemOverscrollBackground.backgroundColor = color bottomItemOverscrollBackground.isLayerBacked = true - self.insertSubnode(bottomItemOverscrollBackground, at: 0) + if let extractedBackgroundsContainerNode = self.extractedBackgroundsContainerNode { + self.insertSubnode(bottomItemOverscrollBackground, aboveSubnode: extractedBackgroundsContainerNode) + } else { + self.insertSubnode(bottomItemOverscrollBackground, at: 0) + } self.bottomItemOverscrollBackground = bottomItemOverscrollBackground } @@ -1660,7 +1785,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture updateIndices.subtract(explicitelyUpdateIndices) - self.updateNodes(synchronous: options.contains(.Synchronous), synchronousLoads: options.contains(.PreferSynchronousResourceLoading), animated: animated, updateIndicesAndItems: updateIndicesAndItems, inputState: updatedState, previousNodes: previousNodes, inputOperations: operations, completion: { updatedState, operations in + self.updateNodes(synchronous: options.contains(.Synchronous), synchronousLoads: options.contains(.PreferSynchronousResourceLoading), crossfade: options.contains(.AnimateCrossfade), animated: animated, updateIndicesAndItems: updateIndicesAndItems, inputState: updatedState, previousNodes: previousNodes, inputOperations: operations, completion: { updatedState, operations in self.updateAdjacent(synchronous: options.contains(.Synchronous), animated: animated, state: updatedState, updateAdjacentItemsIndices: updateIndices, operations: operations, completion: { state, operations in var updatedState = state var updatedOperations = operations @@ -1866,7 +1991,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } - private func updateNodes(synchronous: Bool, synchronousLoads: Bool, animated: Bool, updateIndicesAndItems: [ListViewUpdateItem], inputState: ListViewState, previousNodes: [Int: QueueLocalObject], inputOperations: [ListViewStateOperation], completion: @escaping (ListViewState, [ListViewStateOperation]) -> Void) { + private func updateNodes(synchronous: Bool, synchronousLoads: Bool, crossfade: Bool, animated: Bool, updateIndicesAndItems: [ListViewUpdateItem], inputState: ListViewState, previousNodes: [Int: QueueLocalObject], inputOperations: [ListViewStateOperation], completion: @escaping (ListViewState, [ListViewStateOperation]) -> Void) { var state = inputState var operations = inputOperations var updateIndicesAndItems = updateIndicesAndItems @@ -1878,11 +2003,19 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } else { let updateItem = updateIndicesAndItems[0] if let previousNode = previousNodes[updateItem.index] { - self.nodeForItem(synchronous: synchronous, synchronousLoads: synchronousLoads, item: updateItem.item, previousNode: previousNode, index: updateItem.index, previousItem: updateItem.index == 0 ? nil : self.items[updateItem.index - 1], nextItem: updateItem.index == (self.items.count - 1) ? nil : self.items[updateItem.index + 1], params: ListViewItemLayoutParams(width: state.visibleSize.width, leftInset: state.insets.left, rightInset: state.insets.right, availableHeight: state.visibleSize.height - state.insets.top - state.insets.bottom), updateAnimation: animated ? .System(duration: insertionAnimationDuration) : .None, completion: { _, layout, apply in + let updateAnimation: ListViewItemUpdateAnimation + if crossfade { + updateAnimation = .Crossfade + } else if animated { + updateAnimation = .System(duration: insertionAnimationDuration) + } else { + updateAnimation = .None + } + self.nodeForItem(synchronous: synchronous, synchronousLoads: synchronousLoads, item: updateItem.item, previousNode: previousNode, index: updateItem.index, previousItem: updateItem.index == 0 ? nil : self.items[updateItem.index - 1], nextItem: updateItem.index == (self.items.count - 1) ? nil : self.items[updateItem.index + 1], params: ListViewItemLayoutParams(width: state.visibleSize.width, leftInset: state.insets.left, rightInset: state.insets.right, availableHeight: state.visibleSize.height - state.insets.top - state.insets.bottom), updateAnimation: updateAnimation, completion: { _, layout, apply in state.updateNodeAtItemIndex(updateItem.index, layout: layout, direction: updateItem.directionHint, animation: animated ? .System(duration: insertionAnimationDuration) : .None, apply: apply, operations: &operations) updateIndicesAndItems.remove(at: 0) - self.updateNodes(synchronous: synchronous, synchronousLoads: synchronousLoads, animated: animated, updateIndicesAndItems: updateIndicesAndItems, inputState: state, previousNodes: previousNodes, inputOperations: operations, completion: completion) + self.updateNodes(synchronous: synchronous, synchronousLoads: synchronousLoads, crossfade: crossfade, animated: animated, updateIndicesAndItems: updateIndicesAndItems, inputState: state, previousNodes: previousNodes, inputOperations: operations, completion: completion) }) break } else { @@ -1976,7 +2109,11 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture node.addHeightAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) } if node.animationForKey("apparentHeight") == nil || !(node is ListViewTempItemNode) { - node.addApparentHeightAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + node.addApparentHeightAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in + if let node = node { + node.animateFrameTransition(progress, currentValue) + } + }) } node.animateRemoved(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor()) } else if animated { @@ -2000,8 +2137,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } else { if !nodeFrame.size.height.isEqual(to: node.apparentHeight) { + let addAnimation = previousFrame?.height != nodeFrame.size.height node.addApparentHeightAnimation(nodeFrame.size.height, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in - if let node = node { + if let node = node, addAnimation { node.animateFrameTransition(progress, currentValue) } }) @@ -2159,6 +2297,13 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture let timestamp = CACurrentMediaTime() + var sizeOrInsetsUpdated = false + if let updateSizeAndInsets = updateSizeAndInsets { + if updateSizeAndInsets.size != self.visibleSize || updateSizeAndInsets.insets != self.insets { + sizeOrInsetsUpdated = true + } + } + let listInsets = updateSizeAndInsets?.insets ?? self.insets if let updateOpaqueState = updateOpaqueState { @@ -2223,13 +2368,21 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } else { self.addSubnode(node) } + if let extractedBackgroundsNode = node.extractedBackgroundNode { + self.extractedBackgroundsContainerNode?.addSubnode(extractedBackgroundsNode) + } } else { if animated { if let topItemOverscrollBackground = self.topItemOverscrollBackground { self.insertSubnode(node, aboveSubnode: topItemOverscrollBackground) + } else if let extractedBackgroundsContainerNode = self.extractedBackgroundsContainerNode { + self.insertSubnode(node, aboveSubnode: extractedBackgroundsContainerNode) } else { self.insertSubnode(node, at: 0) } + if let extractedBackgroundsNode = node.extractedBackgroundNode { + self.extractedBackgroundsContainerNode?.addSubnode(extractedBackgroundsNode) + } } else { if let itemNode = self.reorderNode?.itemNode, itemNode.supernode == self { self.insertSubnode(node, belowSubnode: itemNode) @@ -2240,6 +2393,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } else { self.addSubnode(node) } + if let extractedBackgroundsNode = node.extractedBackgroundNode { + self.extractedBackgroundsContainerNode?.addSubnode(extractedBackgroundsNode) + } } } case let .InsertDisappearingPlaceholder(index, referenceNodeObject, offsetDirection): @@ -2268,6 +2424,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } else { self.addSubnode(referenceNode) } + if let extractedBackgroundsNode = referenceNode.extractedBackgroundNode { + self.extractedBackgroundsContainerNode?.addSubnode(extractedBackgroundsNode) + } } } else { assertionFailure() @@ -2409,7 +2568,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture //print("replay after \(self.itemNodes.map({"\($0.index) \(unsafeAddressOf($0))"}))") } - if let scrollToItem = scrollToItem, !self.areAllItemsOnScreen() { + if let scrollToItem = scrollToItem, !self.areAllItemsOnScreen() || !sizeOrInsetsUpdated { self.stopScrolling() for itemNode in self.itemNodes { @@ -2527,7 +2686,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture itemNode.updateFrame(itemNode.frame.offsetBy(dx: 0.0, dy: offsetFix), within: self.visibleSize) } - let (snappedTopInset, snapToBoundsOffset) = self.snapToBounds(snapTopItem: scrollToItem != nil, stackFromBottom: self.stackFromBottom, updateSizeAndInsets: updateSizeAndInsets, isExperimentalSnapToScrollToItem: isExperimentalSnapToScrollToItem, insetDeltaOffsetFix: insetDeltaOffsetFix) + let (snappedTopInset, snapToBoundsOffset) = self.snapToBounds(snapTopItem: scrollToItem != nil && scrollToItem?.directionHint != .Down, stackFromBottom: self.stackFromBottom, updateSizeAndInsets: updateSizeAndInsets, isExperimentalSnapToScrollToItem: isExperimentalSnapToScrollToItem, insetDeltaOffsetFix: insetDeltaOffsetFix) if !snappedTopInset.isZero && (previousVisibleSize.height.isZero || previousApparentFrames.isEmpty) { offsetFix += snappedTopInset @@ -2597,7 +2756,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } else { self.visibleSize = updateSizeAndInsets.size - if !self.snapToBounds(snapTopItem: scrollToItem != nil, stackFromBottom: self.stackFromBottom, insetDeltaOffsetFix: 0.0).offset.isZero { + if !self.snapToBounds(snapTopItem: scrollToItem != nil && scrollToItem?.directionHint != .Down, stackFromBottom: self.stackFromBottom, insetDeltaOffsetFix: 0.0).offset.isZero { self.updateVisibleContentOffset() } } @@ -2651,18 +2810,18 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self.scroller.contentOffset = self.lastContentOffset self.ignoreScrollingEvents = wasIgnoringScrollingEvents } else { - let (snappedTopInset, snapToBoundsOffset) = self.snapToBounds(snapTopItem: scrollToItem != nil, stackFromBottom: self.stackFromBottom, updateSizeAndInsets: updateSizeAndInsets, scrollToItem: scrollToItem, insetDeltaOffsetFix: 0.0) - + let (snappedTopInset, snapToBoundsOffset) = self.snapToBounds(snapTopItem: scrollToItem != nil && scrollToItem?.directionHint != .Down, stackFromBottom: self.stackFromBottom, updateSizeAndInsets: updateSizeAndInsets, scrollToItem: scrollToItem, insetDeltaOffsetFix: 0.0) + if !snappedTopInset.isZero && previousApparentFrames.isEmpty { for itemNode in self.itemNodes { itemNode.updateFrame(itemNode.frame.offsetBy(dx: 0.0, dy: snappedTopInset), within: self.visibleSize) } } - + if !snapToBoundsOffset.isZero { self.updateVisibleContentOffset() } - + if let snapshotView = snapshotView { snapshotView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: snapshotView.frame.size) self.view.addSubview(snapshotView) @@ -2774,6 +2933,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } else { self.addSubnode(itemNode) } + if let extractedBackgroundsNode = itemNode.extractedBackgroundNode { + self.extractedBackgroundsContainerNode?.addSubnode(extractedBackgroundsNode) + } } var temporaryHeaderNodes: [ListViewItemHeaderNode] = [] @@ -2873,6 +3035,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture animation.completion = { _ in for itemNode in temporaryPreviousNodes { itemNode.removeFromSupernode() + itemNode.extractedBackgroundNode?.removeFromSupernode() } for headerNode in temporaryHeaderNodes { headerNode.removeFromSupernode() @@ -2951,7 +3114,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture let node = self.itemNodes[index] self.itemNodes.remove(at: index) node.removeFromSupernode() - + node.extractedBackgroundNode?.removeFromSupernode() node.accessoryItemNode?.removeFromSupernode() node.setAccessoryItemNode(nil, leftInset: self.insets.left, rightInset: self.insets.right) node.headerAccessoryItemNode?.removeFromSupernode() @@ -2965,21 +3128,25 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture let flashing = self.headerItemsAreFlashing() - let addHeader: (_ id: Int64, _ upperBound: CGFloat, _ lowerBound: CGFloat, _ item: ListViewItemHeader, _ hasValidNodes: Bool) -> Void = { id, upperBound, lowerBound, item, hasValidNodes in + func addHeader(id: Int64, upperBound: CGFloat, upperBoundEdge: CGFloat, lowerBound: CGFloat, item: ListViewItemHeader, hasValidNodes: Bool) { let itemHeaderHeight: CGFloat = item.height let headerFrame: CGRect let stickLocationDistanceFactor: CGFloat let stickLocationDistance: CGFloat switch item.stickDirection { - case .top: - headerFrame = CGRect(origin: CGPoint(x: 0.0, y: min(max(upperDisplayBound, upperBound), lowerBound - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight)) - stickLocationDistance = headerFrame.minY - upperBound - stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight)) - case .bottom: - headerFrame = CGRect(origin: CGPoint(x: 0.0, y: max(upperBound, min(lowerBound, lowerDisplayBound) - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight)) - stickLocationDistance = lowerBound - headerFrame.maxY - stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight)) + case .top: + headerFrame = CGRect(origin: CGPoint(x: 0.0, y: min(max(upperDisplayBound, upperBound), lowerBound - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight)) + stickLocationDistance = headerFrame.minY - upperBound + stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight)) + case .topEdge: + headerFrame = CGRect(origin: CGPoint(x: 0.0, y: min(max(upperDisplayBound, upperBoundEdge - itemHeaderHeight), lowerBound - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight)) + stickLocationDistance = headerFrame.minY - upperBoundEdge + itemHeaderHeight + stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight)) + case .bottom: + headerFrame = CGRect(origin: CGPoint(x: 0.0, y: max(upperBound, min(lowerBound, lowerDisplayBound) - itemHeaderHeight)), size: CGSize(width: self.visibleSize.width, height: itemHeaderHeight)) + stickLocationDistance = lowerBound - headerFrame.maxY + stickLocationDistanceFactor = max(0.0, min(1.0, stickLocationDistance / itemHeaderHeight)) } visibleHeaderNodes.insert(id) if let headerNode = self.itemHeaderNodes[id] { @@ -3007,6 +3174,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } + if headerNode.item !== item { + item.updateNode(headerNode, previous: nil, next: nil) + headerNode.item = item + } headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset) headerNode.updateInternalStickLocationDistanceFactor(stickLocationDistanceFactor, animated: true) headerNode.internalStickLocationDistance = stickLocationDistance @@ -3024,6 +3195,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: transition.0) } else { let headerNode = item.node() + if headerNode.item !== item { + item.updateNode(headerNode, previous: nil, next: nil) + headerNode.item = item + } headerNode.updateFlashingOnScrolling(flashing, animated: false) headerNode.frame = headerFrame headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset) @@ -3042,31 +3217,32 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } - var previousHeader: (Int64, CGFloat, CGFloat, ListViewItemHeader, Bool)? + var previousHeader: (id: Int64, upperBound: CGFloat, upperBoundEdge: CGFloat, lowerBound: CGFloat, item: ListViewItemHeader, hasValidNodes: Bool)? for itemNode in self.itemNodes { let itemFrame = itemNode.apparentFrame + let itemTopInset = itemNode.insets.top if let itemHeader = itemNode.header() { - if let (previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader { + if let (previousHeaderId, previousUpperBound, previousUpperBoundEdge, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader { if previousHeaderId == itemHeader.id { - previousHeader = (previousHeaderId, previousUpperBound, itemFrame.maxY, previousHeaderItem, hasValidNodes || itemNode.index != nil) + previousHeader = (previousHeaderId, previousUpperBound, previousUpperBoundEdge, itemFrame.maxY, previousHeaderItem, hasValidNodes || itemNode.index != nil) } else { - addHeader(previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes) + addHeader(id: previousHeaderId, upperBound: previousUpperBound, upperBoundEdge: previousUpperBoundEdge, lowerBound: previousLowerBound, item: previousHeaderItem, hasValidNodes: hasValidNodes) - previousHeader = (itemHeader.id, itemFrame.minY, itemFrame.maxY, itemHeader, itemNode.index != nil) + previousHeader = (itemHeader.id, itemFrame.minY, itemFrame.minY + itemTopInset, itemFrame.maxY, itemHeader, itemNode.index != nil) } } else { - previousHeader = (itemHeader.id, itemFrame.minY, itemFrame.maxY, itemHeader, itemNode.index != nil) + previousHeader = (itemHeader.id, itemFrame.minY, itemFrame.minY + itemTopInset, itemFrame.maxY, itemHeader, itemNode.index != nil) } } else { - if let (previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader { - addHeader(previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes) + if let (previousHeaderId, previousUpperBound, previousUpperBoundEdge, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader { + addHeader(id: previousHeaderId, upperBound: previousUpperBound, upperBoundEdge: previousUpperBoundEdge, lowerBound: previousLowerBound, item: previousHeaderItem, hasValidNodes: hasValidNodes) } previousHeader = nil } } - if let (previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader { - addHeader(previousHeaderId, previousUpperBound, previousLowerBound, previousHeaderItem, hasValidNodes) + if let (previousHeaderId, previousUpperBound, previousUpperBoundEdge, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader { + addHeader(id: previousHeaderId, upperBound: previousUpperBound, upperBoundEdge: previousUpperBoundEdge, lowerBound: previousLowerBound, item: previousHeaderItem, hasValidNodes: hasValidNodes) } let currentIds = Set(self.itemHeaderNodes.keys) @@ -3752,22 +3928,22 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } - public func ensureItemNodeVisible(_ node: ListViewItemNode, animated: Bool = true, overflow: CGFloat = 0.0) { + public func ensureItemNodeVisible(_ node: ListViewItemNode, animated: Bool = true, overflow: CGFloat = 0.0, curve: ListViewAnimationCurve = .Default(duration: 0.25)) { if let index = node.index { if node.apparentHeight > self.visibleSize.height - self.insets.top - self.insets.bottom { if node.frame.maxY > self.visibleSize.height - self.insets.bottom { - self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: ListViewScrollPosition.bottom(-overflow), animated: animated, curve: ListViewAnimationCurve.Default(duration: 0.25), directionHint: ListViewScrollToItemDirectionHint.Down), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - }/* else if node.frame.minY < self.insets.top { - self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: ListViewScrollPosition.top(0.0), animated: true, curve: ListViewAnimationCurve.Default(duration: 0.25), directionHint: ListViewScrollToItemDirectionHint.Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - }*/ + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: ListViewScrollPosition.bottom(-overflow), animated: animated, curve: curve, directionHint: ListViewScrollToItemDirectionHint.Down), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } else if node.frame.minY < self.insets.top && overflow > 0.0 { + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: ListViewScrollPosition.top(-overflow), animated: animated, curve: curve, directionHint: ListViewScrollToItemDirectionHint.Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } } else { if self.experimentalSnapScrollToItem { self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: ListViewScrollPosition.visible, animated: animated, curve: ListViewAnimationCurve.Default(duration: nil), directionHint: ListViewScrollToItemDirectionHint.Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } else { if node.frame.minY < self.insets.top { - self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: ListViewScrollPosition.top(overflow), animated: animated, curve: ListViewAnimationCurve.Default(duration: 0.25), directionHint: ListViewScrollToItemDirectionHint.Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: ListViewScrollPosition.top(overflow), animated: animated, curve: curve, directionHint: ListViewScrollToItemDirectionHint.Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } else if node.frame.maxY > self.visibleSize.height - self.insets.bottom { - self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: ListViewScrollPosition.bottom(-overflow), animated: animated, curve: ListViewAnimationCurve.Default(duration: 0.25), directionHint: ListViewScrollToItemDirectionHint.Down), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: ListViewScrollPosition.bottom(-overflow), animated: animated, curve: curve, directionHint: ListViewScrollToItemDirectionHint.Down), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } } } @@ -3986,7 +4162,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture case .down: var contentOffset = initialOffset contentOffset.y += distance - contentOffset.y = max(self.scroller.contentInset.top, min(contentOffset.y, self.scroller.contentSize.height - self.visibleSize.height - self.insets.bottom - self.insets.top)) + contentOffset.y = max(self.scroller.contentInset.top, min(contentOffset.y, self.scroller.contentSize.height - self.scroller.frame.height)) if contentOffset.y > initialOffset.y { self.ignoreScrollingEvents = true self.scroller.setContentOffset(contentOffset, animated: false) diff --git a/submodules/Display/Source/ListViewAnimation.swift b/submodules/Display/Source/ListViewAnimation.swift index 00364546d0..57dc0d4b2e 100644 --- a/submodules/Display/Source/ListViewAnimation.swift +++ b/submodules/Display/Source/ListViewAnimation.swift @@ -177,3 +177,29 @@ public final class ListViewAnimation { self.update(ct, self.valueAt(ct)) } } + +public func listViewAnimationDurationAndCurve(transition: ContainedViewLayoutTransition) -> (Double, ListViewAnimationCurve) { + var duration: Double = 0.0 + var curve: UInt = 0 + switch transition { + case .immediate: + break + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut, .custom: + break + case .spring: + curve = 7 + } + } + + let listViewCurve: ListViewAnimationCurve + if curve == 7 { + listViewCurve = .Spring(duration: duration) + } else { + listViewCurve = .Default(duration: duration) + } + + return (duration, listViewCurve) +} diff --git a/submodules/Display/Source/ListViewIntermediateState.swift b/submodules/Display/Source/ListViewIntermediateState.swift index 30733d3ebf..ccc1344711 100644 --- a/submodules/Display/Source/ListViewIntermediateState.swift +++ b/submodules/Display/Source/ListViewIntermediateState.swift @@ -810,7 +810,7 @@ struct ListViewState { i += 1 if node.index == itemIndex { switch animation { - case .None: + case .None, .Crossfade: let offsetDirection: ListViewInsertionOffsetDirection if let direction = direction { offsetDirection = ListViewInsertionOffsetDirection(direction) diff --git a/submodules/Display/Source/ListViewItem.swift b/submodules/Display/Source/ListViewItem.swift index 9ca2c287c6..da8d0d792f 100644 --- a/submodules/Display/Source/ListViewItem.swift +++ b/submodules/Display/Source/ListViewItem.swift @@ -5,6 +5,7 @@ import SwiftSignalKit public enum ListViewItemUpdateAnimation { case None case System(duration: Double) + case Crossfade public var isAnimated: Bool { if case .None = self { diff --git a/submodules/Display/Source/ListViewItemHeader.swift b/submodules/Display/Source/ListViewItemHeader.swift index 6d1439cd4f..fb288b3187 100644 --- a/submodules/Display/Source/ListViewItemHeader.swift +++ b/submodules/Display/Source/ListViewItemHeader.swift @@ -4,6 +4,7 @@ import AsyncDisplayKit public enum ListViewItemHeaderStickDirection { case top + case topEdge case bottom } @@ -15,6 +16,7 @@ public protocol ListViewItemHeader: class { var height: CGFloat { get } func node() -> ListViewItemHeaderNode + func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) } open class ListViewItemHeaderNode: ASDisplayNode { @@ -25,6 +27,8 @@ open class ListViewItemHeaderNode: ASDisplayNode { final var internalStickLocationDistance: CGFloat = 0.0 private var isFlashingOnScrolling = false + var item: ListViewItemHeader? + func updateInternalStickLocationDistanceFactor(_ factor: CGFloat, animated: Bool) { self.internalStickLocationDistanceFactor = factor } @@ -46,9 +50,24 @@ open class ListViewItemHeaderNode: ASDisplayNode { self.spring = ListViewItemSpring(stiffness: -280.0, damping: -24.0, mass: 0.85) } - super.init() + if seeThrough { + if (layerBacked) { + super.init() + self.setLayerBlock({ + return CASeeThroughTracingLayer() + }) + } else { + super.init() - self.isLayerBacked = layerBacked + self.setViewBlock({ + return CASeeThroughTracingView() + }) + } + } else { + super.init() + + self.isLayerBacked = layerBacked + } } open func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { diff --git a/submodules/Display/Source/ListViewItemNode.swift b/submodules/Display/Source/ListViewItemNode.swift index c1b805061f..1f41a35689 100644 --- a/submodules/Display/Source/ListViewItemNode.swift +++ b/submodules/Display/Source/ListViewItemNode.swift @@ -110,6 +110,10 @@ open class ListViewItemNode: ASDisplayNode { } } + open var extractedBackgroundNode: ASDisplayNode? { + return nil + } + private final var spring: ListViewItemSpring? private final var animations: [(String, ListViewAnimation)] = [] @@ -540,11 +544,22 @@ open class ListViewItemNode: ASDisplayNode { public func updateFrame(_ frame: CGRect, within containerSize: CGSize) { self.frame = frame self.updateAbsoluteRect(frame, within: containerSize) + if let extractedBackgroundNode = self.extractedBackgroundNode { + extractedBackgroundNode.frame = frame.offsetBy(dx: 0.0, dy: -self.insets.top) + } } open func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { } open func applyAbsoluteOffset(value: CGFloat, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + if let extractedBackgroundNode = self.extractedBackgroundNode { + let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: animationCurve) + transition.animatePositionAdditive(node: extractedBackgroundNode, offset: CGPoint(x: 0.0, y: -value)) + } + } + + open func snapshotForReordering() -> UIView? { + return self.view.snapshotContentTree(keepTransform: true) } } diff --git a/submodules/Display/Source/ListViewReorderingItemNode.swift b/submodules/Display/Source/ListViewReorderingItemNode.swift index 0fc0dc6b3f..ed902eb3c8 100644 --- a/submodules/Display/Source/ListViewReorderingItemNode.swift +++ b/submodules/Display/Source/ListViewReorderingItemNode.swift @@ -2,47 +2,105 @@ import Foundation import UIKit import AsyncDisplayKit +private func generateShadowImage(mirror: Bool) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + if mirror { + 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.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 10.0, color: UIColor(white: 0.0, alpha: 0.4).cgColor) + context.setFillColor(UIColor(white: 0.0, alpha: 1.0).cgColor) + for _ in 0 ..< 1 { + context.fill(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: 15.0))) + } + context.clear(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: 15.0))) + }) +} + +private final class CopyView: UIView { + let topShadow: UIImageView + let bottomShadow: UIImageView + + override init(frame: CGRect) { + self.topShadow = UIImageView() + self.bottomShadow = UIImageView() + + super.init(frame: frame) + + self.topShadow.image = generateShadowImage(mirror: true) + self.bottomShadow.image = generateShadowImage(mirror: false) + + self.addSubview(self.topShadow) + self.addSubview(self.bottomShadow) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + final class ListViewReorderingItemNode: ASDisplayNode { weak var itemNode: ListViewItemNode? var currentState: (Int, Int)? - private let copyView: UIView? + private let copyView: CopyView private let initialLocation: CGPoint init(itemNode: ListViewItemNode, initialLocation: CGPoint) { self.itemNode = itemNode - self.copyView = itemNode.view.snapshotView(afterScreenUpdates: false) + self.copyView = CopyView(frame: CGRect()) + let snapshotView = itemNode.snapshotForReordering() self.initialLocation = initialLocation super.init() - if let copyView = self.copyView { - self.view.addSubview(copyView) - copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y), size: copyView.bounds.size) - copyView.bounds = itemNode.bounds + if let snapshotView = snapshotView { + snapshotView.frame = CGRect(origin: CGPoint(), size: itemNode.bounds.size) + snapshotView.bounds.origin = itemNode.bounds.origin + self.copyView.addSubview(snapshotView) } + self.view.addSubview(self.copyView) + self.copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y), size: itemNode.bounds.size) + + self.copyView.topShadow.frame = CGRect(origin: CGPoint(x: 0.0, y: -15.0), size: CGSize(width: copyView.bounds.size.width, height: 30.0)) + + self.copyView.bottomShadow.image = generateShadowImage(mirror: false) + self.copyView.bottomShadow.frame = CGRect(origin: CGPoint(x: 0.0, y: self.copyView.bounds.size.height - 15.0), size: CGSize(width: self.copyView.bounds.size.width, height: 30.0)) + + self.copyView.topShadow.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + self.copyView.bottomShadow.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } func updateOffset(offset: CGFloat) { - if let copyView = self.copyView { - copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y + offset), size: copyView.bounds.size) - } + self.copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y + offset), size: copyView.bounds.size) } func currentOffset() -> CGFloat? { - if let copyView = self.copyView { - return copyView.center.y - } - return nil + return self.copyView.center.y } func animateCompletion(completion: @escaping () -> Void) { - if let copyView = self.copyView, let itemNode = self.itemNode { + if let itemNode = self.itemNode { + let offset = itemNode.frame.midY - copyView.frame.midY itemNode.isHidden = false - itemNode.transitionOffset = itemNode.apparentFrame.midY - copyView.frame.midY - itemNode.addTransitionOffsetAnimation(0.0, duration: 0.2, beginAt: CACurrentMediaTime()) + self.copyView.isHidden = true + itemNode.transitionOffset = offset + itemNode.addTransitionOffsetAnimation(0.0, duration: 0.3 * UIView.animationDurationFactor(), beginAt: CACurrentMediaTime()) completion() + + /*itemNode.transitionOffset = 0.0 + itemNode.setAnimationForKey("transitionOffset", animation: nil) + self.copyView.topShadow.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.copyView.bottomShadow.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.copyView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: itemNode.frame.midY - copyView.frame.midY), duration: 0.2, removeOnCompletion: false, additive: true, force: true, completion: { [weak itemNode] _ in + itemNode?.isHidden = false + completion() + })*/ } else { completion() } diff --git a/submodules/Display/Source/ListViewScroller.swift b/submodules/Display/Source/ListViewScroller.swift index dd8c2f9e75..c77b2207cb 100644 --- a/submodules/Display/Source/ListViewScroller.swift +++ b/submodules/Display/Source/ListViewScroller.swift @@ -1,31 +1,40 @@ import UIKit -class ListViewScroller: UIScrollView, UIGestureRecognizerDelegate { - override init(frame: CGRect) { +public final class ListViewScroller: UIScrollView, UIGestureRecognizerDelegate { + override public init(frame: CGRect) { super.init(frame: frame) - #if os(iOS) self.scrollsToTop = false if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.contentInsetAdjustmentBehavior = .never } - #endif } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { if otherGestureRecognizer is ListViewTapGestureRecognizer { return true } return false } - #if os(iOS) - override func touchesShouldCancel(in view: UIView) -> Bool { + override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPanGestureRecognizer, let gestureRecognizers = gestureRecognizer.view?.gestureRecognizers { + for otherGestureRecognizer in gestureRecognizers { + if otherGestureRecognizer !== gestureRecognizer, let panGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer, panGestureRecognizer.minimumNumberOfTouches == 2 { + return gestureRecognizer.numberOfTouches < 2 + } + } + return true + } else { + return true + } + } + + override public func touchesShouldCancel(in view: UIView) -> Bool { return true } - #endif } diff --git a/submodules/Display/Source/NativeWindowHostView.swift b/submodules/Display/Source/NativeWindowHostView.swift index 7529222a06..b408471db7 100644 --- a/submodules/Display/Source/NativeWindowHostView.swift +++ b/submodules/Display/Source/NativeWindowHostView.swift @@ -246,6 +246,10 @@ private final class WindowRootViewController: UIViewController, UIViewController self.previousPreviewingHostView = nil } } + + override public func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) { + super.present(viewControllerToPresent, animated: flag, completion: completion) + } } private final class NativeWindow: UIWindow, WindowHost { diff --git a/submodules/Display/Source/Navigation/NavigationContainer.swift b/submodules/Display/Source/Navigation/NavigationContainer.swift index ec6f8803d5..c45e446656 100644 --- a/submodules/Display/Source/Navigation/NavigationContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationContainer.swift @@ -111,11 +111,11 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate { override func didLoad() { super.didLoad() - let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), canBegin: { [weak self] in - guard let strongSelf = self else { - return false + let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] in + guard let strongSelf = self, strongSelf.controllers.count > 1 else { + return [] } - return strongSelf.controllers.count > 1 + return .right }) panRecognizer.delegate = self panRecognizer.delaysTouchesBegan = false @@ -133,6 +133,13 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate { }*/ } + func hasNonReadyControllers() -> Bool { + if let pending = self.state.pending, !pending.isReady { + return true + } + return false + } + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return false } @@ -181,10 +188,21 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate { bottomController.viewWillAppear(true) let bottomNode = bottomController.displayNode - let navigationTransitionCoordinator = NavigationTransitionCoordinator(transition: .Pop, container: self, topNode: topNode, topNavigationBar: topController.navigationBar, bottomNode: bottomNode, bottomNavigationBar: bottomController.navigationBar, didUpdateProgress: { [weak self] progress, transition, topFrame, bottomFrame in + let navigationTransitionCoordinator = NavigationTransitionCoordinator(transition: .Pop, isInteractive: true, container: self, topNode: topNode, topNavigationBar: topController.navigationBar, bottomNode: bottomNode, bottomNavigationBar: bottomController.navigationBar, didUpdateProgress: { [weak self, weak bottomController] progress, transition, topFrame, bottomFrame in if let strongSelf = self { if let top = strongSelf.state.top { strongSelf.syncKeyboard(leftEdge: top.value.displayNode.frame.minX, transition: transition) + + var updatedStatusBarStyle = strongSelf.statusBarStyle + if let bottomController = bottomController, progress >= 0.3 { + updatedStatusBarStyle = bottomController.statusBar.statusBarStyle + } else { + updatedStatusBarStyle = top.value.statusBar.statusBarStyle + } + if strongSelf.statusBarStyle != updatedStatusBarStyle { + strongSelf.statusBarStyle = updatedStatusBarStyle + strongSelf.statusBarStyleUpdated?(.animated(duration: 0.3, curve: .easeInOut)) + } } } }) @@ -337,7 +355,24 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate { } } self.applyLayout(layout: updatedLayout, to: top, isMaster: true, transition: transition) - updatedStatusBarStyle = top.value.statusBar.statusBarStyle + if let childTransition = self.state.transition, childTransition.coordinator.isInteractive { + switch childTransition.type { + case .push: + if childTransition.coordinator.progress >= 0.3 { + updatedStatusBarStyle = top.value.statusBar.statusBarStyle + } else { + updatedStatusBarStyle = childTransition.previous.value.statusBar.statusBarStyle + } + case .pop: + if childTransition.coordinator.progress >= 0.3 { + updatedStatusBarStyle = childTransition.previous.value.statusBar.statusBarStyle + } else { + updatedStatusBarStyle = top.value.statusBar.statusBarStyle + } + } + } else { + updatedStatusBarStyle = top.value.statusBar.statusBarStyle + } } else { updatedStatusBarStyle = .Ignore } @@ -377,7 +412,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate { } toValue.value.setIgnoreAppearanceMethodInvocations(false) - let topTransition = TopTransition(type: transitionType, previous: fromValue, coordinator: NavigationTransitionCoordinator(transition: mappedTransitionType, container: self, topNode: topController.displayNode, topNavigationBar: topController.navigationBar, bottomNode: bottomController.displayNode, bottomNavigationBar: bottomController.navigationBar, didUpdateProgress: { [weak self] _, transition, topFrame, bottomFrame in + let topTransition = TopTransition(type: transitionType, previous: fromValue, coordinator: NavigationTransitionCoordinator(transition: mappedTransitionType, isInteractive: false, container: self, topNode: topController.displayNode, topNavigationBar: topController.navigationBar, bottomNode: bottomController.displayNode, bottomNavigationBar: bottomController.navigationBar, didUpdateProgress: { [weak self] _, transition, topFrame, bottomFrame in guard let strongSelf = self else { return } diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index e6e0216745..8964945933 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -125,8 +125,6 @@ open class NavigationController: UINavigationController, ContainableController, private let mode: NavigationControllerMode private var theme: NavigationControllerTheme - public private(set) weak var overlayPresentingController: ViewController? - var inCallNavigate: (() -> Void)? private var inCallStatusBar: StatusBar? private var globalScrollToTopNode: ScrollToTopNode? @@ -134,8 +132,11 @@ open class NavigationController: UINavigationController, ContainableController, private var rootModalFrame: NavigationModalFrame? private var modalContainers: [NavigationModalContainer] = [] private var overlayContainers: [NavigationOverlayContainer] = [] + private var globalOverlayContainers: [NavigationOverlayContainer] = [] private var globalOverlayContainerParent: GlobalOverlayContainerParent? + public var globalOverlayControllersUpdated: (() -> Void)? + private var validLayout: ContainerViewLayout? private var validStatusBarStyle: NavigationStatusBarStyle? private var validStatusBarHidden: Bool = false @@ -190,6 +191,8 @@ open class NavigationController: UINavigationController, ContainableController, } var keyboardViewManager: KeyboardViewManager? + var updateSupportedOrientations: (() -> Void)? + public func updateMasterDetailsBlackout(_ blackout: MasterDetailLayoutBlackout?, transition: ContainedViewLayoutTransition) { self.masterDetailsBlackout = blackout if isViewLoaded { @@ -268,6 +271,18 @@ open class NavigationController: UINavigationController, ContainableController, return true } } + if let rootContainer = self.rootContainer { + switch rootContainer { + case let .flat(container): + if container.hasNonReadyControllers() { + return true + } + case let .split(splitContainer): + if splitContainer.hasNonReadyControllers() { + return true + } + } + } return false } @@ -448,6 +463,9 @@ open class NavigationController: UINavigationController, ContainableController, var topVisibleOverlayContainerWithStatusBar: NavigationOverlayContainer? + var notifyGlobalOverlayControllersUpdated = false + + var modalStyleOverlayTransitionFactor: CGFloat = 0.0 var previousGlobalOverlayContainer: NavigationOverlayContainer? for i in (0 ..< self.globalOverlayContainers.count).reversed() { let overlayContainer = self.globalOverlayContainers[i] @@ -462,6 +480,8 @@ open class NavigationController: UINavigationController, ContainableController, containerTransition.updateFrame(node: overlayContainer, frame: CGRect(origin: CGPoint(), size: overlayLayout.size)) overlayContainer.update(layout: overlayLayout, transition: containerTransition) + modalStyleOverlayTransitionFactor = max(modalStyleOverlayTransitionFactor, overlayContainer.controller.modalStyleOverlayTransitionFactor) + if overlayContainer.supernode == nil && overlayContainer.isReady { if let previousGlobalOverlayContainer = previousGlobalOverlayContainer { self.globalOverlayContainerParent?.insertSubnode(overlayContainer, belowSubnode: previousGlobalOverlayContainer) @@ -469,6 +489,7 @@ open class NavigationController: UINavigationController, ContainableController, self.globalOverlayContainerParent?.addSubnode(overlayContainer) } overlayContainer.transitionIn() + notifyGlobalOverlayControllersUpdated = true } if overlayContainer.supernode != nil { @@ -504,6 +525,8 @@ open class NavigationController: UINavigationController, ContainableController, containerTransition.updateFrame(node: overlayContainer, frame: CGRect(origin: CGPoint(), size: layout.size)) overlayContainer.update(layout: layout, transition: containerTransition) + modalStyleOverlayTransitionFactor = max(modalStyleOverlayTransitionFactor, overlayContainer.controller.modalStyleOverlayTransitionFactor) + if overlayContainer.supernode == nil && overlayContainer.isReady { if let previousOverlayContainer = previousOverlayContainer { self.displayNode.insertSubnode(overlayContainer, belowSubnode: previousOverlayContainer) @@ -740,12 +763,14 @@ open class NavigationController: UINavigationController, ContainableController, } } + topModalDismissProgress = max(topModalDismissProgress, modalStyleOverlayTransitionFactor) + switch layout.metrics.widthClass { case .compact: - if visibleModalCount != 0 { + if visibleModalCount != 0 || !modalStyleOverlayTransitionFactor.isZero { let effectiveRootModalDismissProgress: CGFloat let visibleRootModalDismissProgress: CGFloat - let additionalModalFrameProgress: CGFloat + var additionalModalFrameProgress: CGFloat if visibleModalCount == 1 { effectiveRootModalDismissProgress = topModalIsFlat ? 1.0 : topModalDismissProgress visibleRootModalDismissProgress = effectiveRootModalDismissProgress @@ -755,9 +780,13 @@ open class NavigationController: UINavigationController, ContainableController, visibleRootModalDismissProgress = topModalDismissProgress additionalModalFrameProgress = 1.0 - topModalDismissProgress } else { - effectiveRootModalDismissProgress = 0.0 + effectiveRootModalDismissProgress = 1.0 - modalStyleOverlayTransitionFactor visibleRootModalDismissProgress = effectiveRootModalDismissProgress - additionalModalFrameProgress = 1.0 + if visibleModalCount == 0 { + additionalModalFrameProgress = 0.0 + } else { + additionalModalFrameProgress = 1.0 + } } let rootModalFrame: NavigationModalFrame @@ -813,7 +842,7 @@ open class NavigationController: UINavigationController, ContainableController, if topModalIsFlat { maxScale = 1.0 maxOffset = 0.0 - } else if visibleModalCount == 1 { + } else if visibleModalCount <= 1 { maxScale = (layout.size.width - 16.0 * 2.0) / layout.size.width maxOffset = (topInset - (layout.size.height - layout.size.height * maxScale) / 2.0) } else { @@ -823,7 +852,7 @@ open class NavigationController: UINavigationController, ContainableController, let scale = 1.0 * visibleRootModalDismissProgress + (1.0 - visibleRootModalDismissProgress) * maxScale let offset = (1.0 - visibleRootModalDismissProgress) * maxOffset - transition.updateSublayerTransformScaleAndOffset(node: rootContainerNode, scale: scale, offset: CGPoint(x: 0.0, y: offset)) + transition.updateSublayerTransformScaleAndOffset(node: rootContainerNode, scale: scale, offset: CGPoint(x: 0.0, y: offset), beginWithCurrentState: true) } } else { if let rootModalFrame = self.rootModalFrame { @@ -906,6 +935,12 @@ open class NavigationController: UINavigationController, ContainableController, } self.isUpdatingContainers = false + + if notifyGlobalOverlayControllersUpdated { + self.globalOverlayControllersUpdated?() + } + + self.updateSupportedOrientations?() } private func controllerRemoved(_ controller: ViewController) { @@ -975,27 +1010,8 @@ open class NavigationController: UINavigationController, ContainableController, } public func pushViewController(_ controller: ViewController, animated: Bool = true, completion: @escaping () -> Void) { - let navigateAction: () -> Void = { [weak self] in - guard let strongSelf = self else { - return - } - - if !controller.hasActiveInput { - //strongSelf.view.endEditing(true) - } - /*strongSelf.scheduleAfterLayout({ - guard let strongSelf = self else { - return - }*/ - strongSelf.pushViewController(controller, animated: animated) - completion() - //}) - } - - /*if let lastController = self.viewControllers.last as? ViewController, !lastController.attemptNavigation(navigateAction) { - } else {*/ - navigateAction() - //} + self.pushViewController(controller, animated: animated) + completion() } open override func pushViewController(_ viewController: UIViewController, animated: Bool) { @@ -1117,6 +1133,7 @@ open class NavigationController: UINavigationController, ContainableController, if overlayContainer.controller === controller { overlayContainer.removeFromSupernode() strongSelf.globalOverlayContainers.remove(at: i) + strongSelf.globalOverlayControllersUpdated?() break } } @@ -1136,6 +1153,11 @@ open class NavigationController: UINavigationController, ContainableController, return } strongSelf.updateContainersNonReentrant(transition: transition) + }, modalStyleOverlayTransitionFactorUpdated: { [weak self] transition in + guard let strongSelf = self else { + return + } + strongSelf.updateContainersNonReentrant(transition: transition) }) if inGlobal { self.globalOverlayContainers.append(container) @@ -1273,4 +1295,24 @@ open class NavigationController: UINavigationController, ContainableController, } } } + + public var overlayControllers: [ViewController] { + return self.overlayContainers.compactMap { container in + if container.isReady { + return container.controller + } else { + return nil + } + } + } + + public var globalOverlayControllers: [ViewController] { + return self.globalOverlayContainers.compactMap { container in + if container.isReady { + return container.controller + } else { + return nil + } + } + } } diff --git a/submodules/Display/Source/Navigation/NavigationModalContainer.swift b/submodules/Display/Source/Navigation/NavigationModalContainer.swift index f733538959..f837e4157a 100644 --- a/submodules/Display/Source/Navigation/NavigationModalContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationModalContainer.swift @@ -88,13 +88,14 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes self.scrollNode.view.contentInsetAdjustmentBehavior = .never } self.scrollNode.view.delaysContentTouches = false + self.scrollNode.view.clipsToBounds = false self.scrollNode.view.delegate = self - let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), canBegin: { [weak self] in - guard let strongSelf = self else { - return false + let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] in + guard let strongSelf = self, !strongSelf.isDismissed else { + return [] } - return !strongSelf.isDismissed + return .right }) self.panRecognizer = panRecognizer if let layout = self.validLayout { @@ -131,6 +132,16 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes return false } + private func checkInteractiveDismissWithControllers() -> Bool { + if let controller = self.container.controllers.last { + if !controller.attemptNavigation({ + }) { + return false + } + } + return true + } + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: @@ -147,7 +158,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes let progress = translation / self.bounds.width let velocity = recognizer.velocity(in: self.view).x - if velocity > 1000 || progress > 0.2 { + if (velocity > 1000 || progress > 0.2) && self.checkInteractiveDismissWithControllers() { self.isDismissed = true self.horizontalDismissOffset = self.bounds.width self.dismissProgress = 1.0 @@ -243,7 +254,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes let duration = Double(min(0.3, velocityFactor)) let transition: ContainedViewLayoutTransition let dismissProgress: CGFloat - if velocity.y < -0.5 || progress >= 0.5 { + if (velocity.y < -0.5 || progress >= 0.5) && self.checkInteractiveDismissWithControllers() { dismissProgress = 1.0 targetOffset = 0.0 transition = .animated(duration: duration, curve: .easeInOut) @@ -300,13 +311,18 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(), size: layout.size)) self.ignoreScrolling = true self.scrollNode.view.isScrollEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0) && self.isInteractiveDimissEnabled - transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(x: self.horizontalDismissOffset ?? 0.0, y: 0.0), size: layout.size)) + let previousBounds = self.scrollNode.bounds + let scrollNodeFrame = CGRect(origin: CGPoint(x: self.horizontalDismissOffset ?? 0.0, y: 0.0), size: layout.size) + self.scrollNode.frame = scrollNodeFrame self.scrollNode.view.contentSize = CGSize(width: layout.size.width, height: layout.size.height * 2.0) if !self.scrollNode.view.isDecelerating && !self.scrollNode.view.isDragging { let defaultBounds = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: layout.size) if self.scrollNode.bounds != defaultBounds { self.scrollNode.bounds = defaultBounds } + if previousBounds.minY != defaultBounds.minY { + transition.animateOffsetAdditive(node: self.scrollNode, offset: previousBounds.minY - defaultBounds.minY) + } } self.ignoreScrolling = false diff --git a/submodules/Display/Source/Navigation/NavigationModalFrame.swift b/submodules/Display/Source/Navigation/NavigationModalFrame.swift index f2004efa20..023d255262 100644 --- a/submodules/Display/Source/Navigation/NavigationModalFrame.swift +++ b/submodules/Display/Source/Navigation/NavigationModalFrame.swift @@ -98,7 +98,7 @@ final class NavigationModalFrame: ASDisplayNode { } } - private func updateShades(layout: ContainerViewLayout, progress: CGFloat, additionalProgress: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + private func updateShades(layout: ContainerViewLayout, progress: CGFloat, additionalProgress: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { let sideInset: CGFloat = 16.0 var topInset: CGFloat = 0.0 if let statusBarHeight = layout.statusBarHeight { @@ -127,10 +127,10 @@ final class NavigationModalFrame: ASDisplayNode { let cornerSideOffset: CGFloat = progress * sideInset + additionalProgress * sideInset let cornerTopOffset: CGFloat = progress * topInset + additionalProgress * additionalTopInset let cornerBottomOffset: CGFloat = progress * bottomInset - transition.updateFrame(node: self.topLeftCorner, frame: CGRect(origin: CGPoint(x: cornerSideOffset, y: cornerTopOffset), size: CGSize(width: cornerSize, height: cornerSize))) - transition.updateFrame(node: self.topRightCorner, frame: CGRect(origin: CGPoint(x: layout.size.width - cornerSideOffset - cornerSize, y: cornerTopOffset), size: CGSize(width: cornerSize, height: cornerSize))) - transition.updateFrame(node: self.bottomLeftCorner, frame: CGRect(origin: CGPoint(x: cornerSideOffset, y: layout.size.height - cornerBottomOffset - cornerSize), size: CGSize(width: cornerSize, height: cornerSize))) - transition.updateFrame(node: self.bottomRightCorner, frame: CGRect(origin: CGPoint(x: layout.size.width - cornerSideOffset - cornerSize, y: layout.size.height - cornerBottomOffset - cornerSize), size: CGSize(width: cornerSize, height: cornerSize))) + transition.updateFrame(node: self.topLeftCorner, frame: CGRect(origin: CGPoint(x: cornerSideOffset, y: cornerTopOffset), size: CGSize(width: cornerSize, height: cornerSize)), beginWithCurrentState: true) + transition.updateFrame(node: self.topRightCorner, frame: CGRect(origin: CGPoint(x: layout.size.width - cornerSideOffset - cornerSize, y: cornerTopOffset), size: CGSize(width: cornerSize, height: cornerSize)), beginWithCurrentState: true) + transition.updateFrame(node: self.bottomLeftCorner, frame: CGRect(origin: CGPoint(x: cornerSideOffset, y: layout.size.height - cornerBottomOffset - cornerSize), size: CGSize(width: cornerSize, height: cornerSize)), beginWithCurrentState: true) + transition.updateFrame(node: self.bottomRightCorner, frame: CGRect(origin: CGPoint(x: layout.size.width - cornerSideOffset - cornerSize, y: layout.size.height - cornerBottomOffset - cornerSize), size: CGSize(width: cornerSize, height: cornerSize)), beginWithCurrentState: true) let topShadeOffset: CGFloat = progress * topInset + additionalProgress * additionalTopInset let bottomShadeOffset: CGFloat = progress * bottomInset @@ -138,10 +138,10 @@ final class NavigationModalFrame: ASDisplayNode { let rightShadeWidth: CGFloat = progress * sideInset + additionalProgress * sideInset let rightShadeOffset: CGFloat = layout.size.width - rightShadeWidth - transition.updateFrame(node: self.topShade, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: topShadeOffset))) + transition.updateFrame(node: self.topShade, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: topShadeOffset)), beginWithCurrentState: true) transition.updateFrame(node: self.bottomShade, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomShadeOffset), size: CGSize(width: layout.size.width, height: bottomShadeOffset))) - transition.updateFrame(node: self.leftShade, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: leftShadeOffset, height: layout.size.height))) - transition.updateFrame(node: self.rightShade, frame: CGRect(origin: CGPoint(x: rightShadeOffset, y: 0.0), size: CGSize(width: rightShadeWidth, height: layout.size.height)), completion: { _ in + transition.updateFrame(node: self.leftShade, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: leftShadeOffset, height: layout.size.height)), beginWithCurrentState: true) + transition.updateFrame(node: self.rightShade, frame: CGRect(origin: CGPoint(x: rightShadeOffset, y: 0.0), size: CGSize(width: rightShadeWidth, height: layout.size.height)), beginWithCurrentState: true, completion: { _ in completion() }) } diff --git a/submodules/Display/Source/Navigation/NavigationOverlayContainer.swift b/submodules/Display/Source/Navigation/NavigationOverlayContainer.swift index d1524321c8..68401608a0 100644 --- a/submodules/Display/Source/Navigation/NavigationOverlayContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationOverlayContainer.swift @@ -22,7 +22,7 @@ final class NavigationOverlayContainer: ASDisplayNode { } } - init(controller: ViewController, blocksInteractionUntilReady: Bool, controllerRemoved: @escaping (ViewController) -> Void, statusBarUpdated: @escaping (ContainedViewLayoutTransition) -> Void) { + init(controller: ViewController, blocksInteractionUntilReady: Bool, controllerRemoved: @escaping (ViewController) -> Void, statusBarUpdated: @escaping (ContainedViewLayoutTransition) -> Void, modalStyleOverlayTransitionFactorUpdated: @escaping (ContainedViewLayoutTransition) -> Void) { self.controller = controller self.blocksInteractionUntilReady = blocksInteractionUntilReady @@ -39,6 +39,10 @@ final class NavigationOverlayContainer: ASDisplayNode { statusBarUpdated(transition) } + self.controller.modalStyleOverlayTransitionFactorUpdated = { transition in + modalStyleOverlayTransitionFactorUpdated(transition) + } + self.isReadyDisposable = (self.controller.ready.get() |> filter { $0 } |> take(1) diff --git a/submodules/Display/Source/Navigation/NavigationSplitContainer.swift b/submodules/Display/Source/Navigation/NavigationSplitContainer.swift index 155fd2a186..ac6c7772db 100644 --- a/submodules/Display/Source/Navigation/NavigationSplitContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationSplitContainer.swift @@ -57,6 +57,16 @@ final class NavigationSplitContainer: ASDisplayNode { self.view.addSubview(self.detailScrollToTopView) } + func hasNonReadyControllers() -> Bool { + if self.masterContainer.hasNonReadyControllers() { + return true + } + if self.detailContainer.hasNonReadyControllers() { + return true + } + return false + } + func updateTheme(theme: NavigationControllerTheme) { self.separator.backgroundColor = theme.navigationBar.separatorColor } diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 4371ca027e..29ed6e4723 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -111,6 +111,9 @@ open class NavigationBar: ASDisplayNode { public var backPressed: () -> () = { } + public var userInfo: Any? + public var makeCustomTransitionNode: ((NavigationBar, Bool) -> CustomNavigationTransitionNode?)? + private var collapsed: Bool { get { return self.frame.size.height.isLess(than: 44.0) @@ -243,6 +246,8 @@ open class NavigationBar: ASDisplayNode { } } + public var customBackButtonText: String? + private var title: String? { didSet { if let title = self.title { @@ -261,7 +266,7 @@ open class NavigationBar: ASDisplayNode { } } - private var titleView: UIView? { + public private(set) var titleView: UIView? { didSet { if let oldValue = oldValue { oldValue.removeFromSuperview() @@ -377,7 +382,9 @@ open class NavigationBar: ASDisplayNode { case let .item(itemValue): self.previousItemListenerKey = itemValue.addSetTitleListener { [weak self] _, _ in if let strongSelf = self, let previousItem = strongSelf.previousItem, case let .item(itemValue) = previousItem { - if let backBarButtonItem = itemValue.backBarButtonItem { + if let customBackButtonText = strongSelf.customBackButtonText { + strongSelf.backButtonNode.updateManualText(customBackButtonText) + } else if let backBarButtonItem = itemValue.backBarButtonItem { strongSelf.backButtonNode.updateManualText(backBarButtonItem.title ?? "") } else { strongSelf.backButtonNode.updateManualText(itemValue.title ?? "") @@ -389,7 +396,9 @@ open class NavigationBar: ASDisplayNode { self.previousItemBackListenerKey = itemValue.addSetBackBarButtonItemListener { [weak self] _, _, _ in if let strongSelf = self, let previousItem = strongSelf.previousItem, case let .item(itemValue) = previousItem { - if let backBarButtonItem = itemValue.backBarButtonItem { + if let customBackButtonText = strongSelf.customBackButtonText { + strongSelf.backButtonNode.updateManualText(customBackButtonText) + } else if let backBarButtonItem = itemValue.backBarButtonItem { strongSelf.backButtonNode.updateManualText(backBarButtonItem.title ?? "") } else { strongSelf.backButtonNode.updateManualText(itemValue.title ?? "") @@ -505,7 +514,9 @@ open class NavigationBar: ASDisplayNode { self.leftButtonNode.removeFromSupernode() var backTitle: String? - if let leftBarButtonItem = item.leftBarButtonItem, leftBarButtonItem.backButtonAppearance { + if let customBackButtonText = self.customBackButtonText { + backTitle = customBackButtonText + } else if let leftBarButtonItem = item.leftBarButtonItem, leftBarButtonItem.backButtonAppearance { backTitle = leftBarButtonItem.title } else if let previousItem = self.previousItem { switch previousItem { @@ -589,12 +600,11 @@ open class NavigationBar: ASDisplayNode { self.updateAccessibilityElements() } - private let backButtonNode: NavigationButtonNode - private let badgeNode: NavigationBarBadgeNode - private let backButtonArrow: ASImageNode - private let leftButtonNode: NavigationButtonNode - private let rightButtonNode: NavigationButtonNode - + public let backButtonNode: NavigationButtonNode + public let badgeNode: NavigationBarBadgeNode + public let backButtonArrow: ASImageNode + public let leftButtonNode: NavigationButtonNode + public let rightButtonNode: NavigationButtonNode private var _transitionState: NavigationBarTransitionState? var transitionState: NavigationBarTransitionState? { @@ -693,6 +703,7 @@ open class NavigationBar: ASDisplayNode { self.leftButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor self.rightButtonNode.color = self.presentationData.theme.buttonColor self.rightButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor + self.rightButtonNode.rippleColor = self.presentationData.theme.primaryTextColor.withAlphaComponent(0.05) self.backButtonArrow.image = backArrowImage(color: self.presentationData.theme.buttonColor) if let title = self.title { self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: self.presentationData.theme.primaryTextColor) @@ -767,6 +778,7 @@ open class NavigationBar: ASDisplayNode { self.leftButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor self.rightButtonNode.color = self.presentationData.theme.buttonColor self.rightButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor + self.rightButtonNode.rippleColor = self.presentationData.theme.primaryTextColor.withAlphaComponent(0.05) self.backButtonArrow.image = backArrowImage(color: self.presentationData.theme.buttonColor) if let title = self.title { self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: self.presentationData.theme.primaryTextColor) @@ -820,7 +832,7 @@ open class NavigationBar: ASDisplayNode { transition.updateFrame(node: self.stripeNode, frame: CGRect(x: 0.0, y: size.height, width: size.width, height: UIScreenPixel)) - let nominalHeight: CGFloat = self.collapsed ? 32.0 : defaultHeight + let nominalHeight: CGFloat = defaultHeight let contentVerticalOrigin = size.height - nominalHeight - expansionHeight var leftTitleInset: CGFloat = leftInset + 1.0 @@ -957,7 +969,7 @@ open class NavigationBar: ASDisplayNode { if let titleView = self.titleView { let titleSize = CGSize(width: max(1.0, size.width - max(leftTitleInset, rightTitleInset) * 2.0), height: nominalHeight) - let titleFrame = CGRect(origin: CGPoint(x: leftTitleInset, y: contentVerticalOrigin), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize) titleView.frame = titleFrame if let titleView = titleView as? NavigationBarTitleView { @@ -995,7 +1007,7 @@ open class NavigationBar: ASDisplayNode { } } titleView.alpha = 1.0 - titleView.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize) + titleView.frame = titleFrame } } } @@ -1016,18 +1028,45 @@ open class NavigationBar: ASDisplayNode { } } - private func makeTransitionBackButtonNode(accentColor: UIColor) -> NavigationButtonNode? { + public func makeTransitionBackButtonNode(accentColor: UIColor) -> NavigationButtonNode? { if self.backButtonNode.supernode != nil { let node = NavigationButtonNode() node.updateManualText(self.backButtonNode.manualText) node.color = accentColor + if let (size, defaultHeight, _, _) = self.validLayout { + node.updateLayout(constrainedSize: CGSize(width: size.width, height: defaultHeight)) + node.frame = self.backButtonNode.frame + } return node } else { return nil } } - private func makeTransitionBackArrowNode(accentColor: UIColor) -> ASDisplayNode? { + public func makeTransitionRightButtonNode(accentColor: UIColor) -> NavigationButtonNode? { + if self.rightButtonNode.supernode != nil { + let node = NavigationButtonNode() + var items: [UIBarButtonItem] = [] + if let item = self.item { + if let rightBarButtonItems = item.rightBarButtonItems, !rightBarButtonItems.isEmpty { + items = rightBarButtonItems + } else if let rightBarButtonItem = item.rightBarButtonItem { + items = [rightBarButtonItem] + } + } + node.updateItems(items) + node.color = accentColor + if let (size, defaultHeight, _, _) = self.validLayout { + node.updateLayout(constrainedSize: CGSize(width: size.width, height: defaultHeight)) + node.frame = self.backButtonNode.frame + } + return node + } else { + return nil + } + } + + public func makeTransitionBackArrowNode(accentColor: UIColor) -> ASDisplayNode? { if self.backButtonArrow.supernode != nil { let node = ASImageNode() node.image = backArrowImage(color: accentColor) @@ -1039,7 +1078,7 @@ open class NavigationBar: ASDisplayNode { } } - private func makeTransitionBadgeNode() -> ASDisplayNode? { + public func makeTransitionBadgeNode() -> ASDisplayNode? { if self.badgeNode.supernode != nil && !self.badgeNode.isHidden { let node = NavigationBarBadgeNode(fillColor: self.presentationData.theme.badgeBackgroundColor, strokeColor: self.presentationData.theme.badgeStrokeColor, textColor: self.presentationData.theme.badgeTextColor) node.text = self.badgeNode.text @@ -1147,4 +1186,25 @@ open class NavigationBar: ASDisplayNode { } } } + + override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + /*if self.bounds.contains(point) { + if self.backButtonNode.supernode != nil && !self.backButtonNode.isHidden { + let effectiveBackButtonRect = CGRect(origin: CGPoint(), size: CGSize(width: self.backButtonNode.frame.maxX + 20.0, height: self.bounds.height)) + if effectiveBackButtonRect.contains(point) { + return self.backButtonNode.internalHitTest(self.view.convert(point, to: self.backButtonNode.view), with: event) + } + } + }*/ + + guard let result = super.hitTest(point, with: event) else { + return nil + } + + if result == self.view || result == self.clippingNode.view { + return nil + } + + return result + } } diff --git a/submodules/Display/Source/NavigationBarBadge.swift b/submodules/Display/Source/NavigationBarBadge.swift index f31c504a61..333bcfb825 100644 --- a/submodules/Display/Source/NavigationBarBadge.swift +++ b/submodules/Display/Source/NavigationBarBadge.swift @@ -2,7 +2,7 @@ import Foundation import UIKit import AsyncDisplayKit -final class NavigationBarBadgeNode: ASDisplayNode { +public final class NavigationBarBadgeNode: ASDisplayNode { private var fillColor: UIColor private var strokeColor: UIColor private var textColor: UIColor @@ -19,7 +19,7 @@ final class NavigationBarBadgeNode: ASDisplayNode { } } - init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { + public init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { self.fillColor = fillColor self.strokeColor = strokeColor self.textColor = textColor @@ -47,7 +47,7 @@ final class NavigationBarBadgeNode: ASDisplayNode { self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor) } - override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { let badgeSize = self.textNode.updateLayout(constrainedSize) let backgroundSize = CGSize(width: max(18.0, badgeSize.width + 10.0 + 1.0), height: 18.0) let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize) diff --git a/submodules/Display/Source/NavigationButtonNode.swift b/submodules/Display/Source/NavigationButtonNode.swift index 8b73b4b8d4..ffbaef8df0 100644 --- a/submodules/Display/Source/NavigationButtonNode.swift +++ b/submodules/Display/Source/NavigationButtonNode.swift @@ -53,6 +53,7 @@ private final class NavigationButtonItemNode: ImmediateTextNode { } private var imageNode: ASImageNode? + private let imageRippleNode: ASImageNode private var _image: UIImage? public var image: UIImage? { @@ -61,17 +62,33 @@ private final class NavigationButtonItemNode: ImmediateTextNode { } set(value) { _image = value - if let _ = value { + if let value = value { if self.imageNode == nil { let imageNode = ASImageNode() imageNode.displaysAsynchronously = false self.imageNode = imageNode + if false, value.size == CGSize(width: 30.0, height: 30.0) { + if self.imageRippleNode.supernode == nil { + self.addSubnode(self.imageRippleNode) + self.imageRippleNode.image = generateFilledCircleImage(diameter: 30.0, color: self.rippleColor) + } + } else { + if self.imageRippleNode.supernode != nil { + self.imageRippleNode.image = nil + self.imageRippleNode.removeFromSupernode() + } + } + self.addSubnode(imageNode) } self.imageNode?.image = image } else if let imageNode = self.imageNode { imageNode.removeFromSupernode() self.imageNode = nil + if self.imageRippleNode.supernode != nil { + self.imageRippleNode.image = nil + self.imageRippleNode.removeFromSupernode() + } } self.invalidateCalculatedLayout() @@ -100,6 +117,14 @@ private final class NavigationButtonItemNode: ImmediateTextNode { } } + public var rippleColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.05) { + didSet { + if self.imageRippleNode.image != nil { + self.imageRippleNode.image = generateFilledCircleImage(diameter: 30.0, color: self.rippleColor) + } + } + } + public var disabledColor: UIColor = UIColor(rgb: 0xd0d0d0) { didSet { if let text = self._text { @@ -159,6 +184,11 @@ private final class NavigationButtonItemNode: ImmediateTextNode { } override public init() { + self.imageRippleNode = ASImageNode() + self.imageRippleNode.displaysAsynchronously = false + self.imageRippleNode.displayWithoutProcessing = true + self.imageRippleNode.alpha = 0.0 + super.init() self.isAccessibilityElement = true @@ -182,7 +212,9 @@ private final class NavigationButtonItemNode: ImmediateTextNode { } else if let imageNode = self.imageNode { let nodeSize = imageNode.image?.size ?? CGSize() let size = CGSize(width: max(nodeSize.width, superSize.width), height: max(nodeSize.height, superSize.height)) - imageNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - nodeSize.width) / 2.0) + 5.0, y: floorToScreenPixels((size.height - nodeSize.height) / 2.0)), size: nodeSize) + let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - nodeSize.width) / 2.0) + 5.0, y: floorToScreenPixels((size.height - nodeSize.height) / 2.0)), size: nodeSize) + imageNode.frame = imageFrame + self.imageRippleNode.frame = imageFrame return size } return superSize @@ -208,7 +240,7 @@ private final class NavigationButtonItemNode: ImmediateTextNode { public override func touchesMoved(_ touches: Set, with event: UIEvent?) { super.touchesMoved(touches, with: event) - self.updateHighlightedState(self.touchInsideApparentBounds(touches.first!), animated: true) + //self.updateHighlightedState(self.touchInsideApparentBounds(touches.first!), animated: true) } public override func touchesEnded(_ touches: Set, with event: UIEvent?) { @@ -218,7 +250,7 @@ private final class NavigationButtonItemNode: ImmediateTextNode { let previousTouchCount = self.touchCount self.touchCount = max(0, self.touchCount - touches.count) - if previousTouchCount != 0 && self.touchCount == 0 && self.isEnabled && self.touchInsideApparentBounds(touches.first!) { + if previousTouchCount != 0 && self.touchCount == 0 && self.isEnabled { self.pressed() } } @@ -241,7 +273,15 @@ private final class NavigationButtonItemNode: ImmediateTextNode { } if shouldChangeHighlight { - self.alpha = !self.isEnabled ? 1.0 : (highlighted ? 0.4 : 1.0) + if let imageNode = self.imageNode { + let previousAlpha = self.imageRippleNode.alpha + self.imageRippleNode.alpha = highlighted ? 1.0 : 0.0 + if !highlighted { + self.imageRippleNode.layer.animateAlpha(from: previousAlpha, to: self.imageRippleNode.alpha, duration: 0.25) + } + } else { + self.alpha = !self.isEnabled ? 1.0 : (highlighted ? 0.4 : 1.0) + } self.highlightChanged(highlighted) } } @@ -255,9 +295,16 @@ private final class NavigationButtonItemNode: ImmediateTextNode { } -final class NavigationButtonNode: ASDisplayNode { +public final class NavigationButtonNode: ASDisplayNode { private var nodes: [NavigationButtonItemNode] = [] + public var singleCustomNode: ASDisplayNode? { + for node in self.nodes { + return node.node + } + return nil + } + public var pressed: (Int) -> () = { _ in } public var highlightChanged: (Int, Bool) -> () = { _, _ in } @@ -271,6 +318,16 @@ final class NavigationButtonNode: ASDisplayNode { } } + public var rippleColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.05) { + didSet { + if !self.rippleColor.isEqual(oldValue) { + for node in self.nodes { + node.rippleColor = self.rippleColor + } + } + } + } + public var disabledColor: UIColor = UIColor(rgb: 0xd0d0d0) { didSet { if !self.disabledColor.isEqual(oldValue) { @@ -288,7 +345,7 @@ final class NavigationButtonNode: ASDisplayNode { } } - override init() { + override public init() { super.init() self.isAccessibilityElement = false @@ -305,6 +362,7 @@ final class NavigationButtonNode: ASDisplayNode { } else { node = NavigationButtonItemNode() node.color = self.color + node.rippleColor = self.rippleColor node.highlightChanged = { [weak node, weak self] value in if let strongSelf = self, let node = node { if let index = strongSelf.nodes.firstIndex(where: { $0 === node }) { @@ -345,6 +403,7 @@ final class NavigationButtonNode: ASDisplayNode { } else { node = NavigationButtonItemNode() node.color = self.color + node.rippleColor = self.rippleColor node.highlightChanged = { [weak node, weak self] value in if let strongSelf = self, let node = node { if let index = strongSelf.nodes.firstIndex(where: { $0 === node }) { @@ -377,7 +436,7 @@ final class NavigationButtonNode: ASDisplayNode { } } - func updateLayout(constrainedSize: CGSize) -> CGSize { + public func updateLayout(constrainedSize: CGSize) -> CGSize { var nodeOrigin = CGPoint() var totalSize = CGSize() for node in self.nodes { @@ -395,4 +454,12 @@ final class NavigationButtonNode: ASDisplayNode { } return totalSize } + + func internalHitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.nodes.count == 1 { + return self.nodes[0].view + } else { + return super.hitTest(point, with: event) + } + } } diff --git a/submodules/Display/Source/NavigationTransitionCoordinator.swift b/submodules/Display/Source/NavigationTransitionCoordinator.swift index f86c13d6be..692f8fa287 100644 --- a/submodules/Display/Source/NavigationTransitionCoordinator.swift +++ b/submodules/Display/Source/NavigationTransitionCoordinator.swift @@ -16,12 +16,17 @@ private func generateShadow() -> UIImage? { context.setShadow(offset: CGSize(), blur: 16.0, color: UIColor(white: 0.0, alpha: 0.5).cgColor) context.fill(CGRect(origin: CGPoint(x: size.width, y: 0.0), size: CGSize(width: 16.0, height: 1.0))) }) - //return UIImage(named: "NavigationShadow", in: getAppBundle(), compatibleWith: nil)?.precomposed().resizableImage(withCapInsets: UIEdgeInsets(), resizingMode: .tile) } private let shadowImage = generateShadow() -class NavigationTransitionCoordinator { +public protocol CustomNavigationTransitionNode: ASDisplayNode { + func setup(topNavigationBar: NavigationBar, bottomNavigationBar: NavigationBar) + func update(containerSize: CGSize, fraction: CGFloat, transition: ContainedViewLayoutTransition) + func restore() +} + +final class NavigationTransitionCoordinator { private var _progress: CGFloat = 0.0 var progress: CGFloat { get { @@ -31,12 +36,14 @@ class NavigationTransitionCoordinator { private let container: ASDisplayNode private let transition: NavigationTransition + let isInteractive: Bool let topNode: ASDisplayNode let bottomNode: ASDisplayNode private let topNavigationBar: NavigationBar? private let bottomNavigationBar: NavigationBar? private let dimNode: ASDisplayNode private let shadowNode: ASImageNode + private let customTransitionNode: CustomNavigationTransitionNode? private let inlineNavigationBarTransition: Bool @@ -44,8 +51,9 @@ class NavigationTransitionCoordinator { private var currentCompletion: (() -> Void)? private var didUpdateProgress: ((CGFloat, ContainedViewLayoutTransition, CGRect, CGRect) -> Void)? - init(transition: NavigationTransition, container: ASDisplayNode, topNode: ASDisplayNode, topNavigationBar: NavigationBar?, bottomNode: ASDisplayNode, bottomNavigationBar: NavigationBar?, didUpdateProgress: ((CGFloat, ContainedViewLayoutTransition, CGRect, CGRect) -> Void)? = nil) { + init(transition: NavigationTransition, isInteractive: Bool, container: ASDisplayNode, topNode: ASDisplayNode, topNavigationBar: NavigationBar?, bottomNode: ASDisplayNode, bottomNavigationBar: NavigationBar?, didUpdateProgress: ((CGFloat, ContainedViewLayoutTransition, CGRect, CGRect) -> Void)? = nil) { self.transition = transition + self.isInteractive = isInteractive self.container = container self.didUpdateProgress = didUpdateProgress self.topNode = topNode @@ -58,25 +66,43 @@ class NavigationTransitionCoordinator { self.shadowNode.displaysAsynchronously = false self.shadowNode.image = shadowImage - if let topNavigationBar = topNavigationBar, let bottomNavigationBar = bottomNavigationBar, !topNavigationBar.isHidden, !bottomNavigationBar.isHidden, topNavigationBar.canTransitionInline, bottomNavigationBar.canTransitionInline, topNavigationBar.item?.leftBarButtonItem == nil { - var topFrame = topNavigationBar.view.convert(topNavigationBar.bounds, to: container.view) - var bottomFrame = bottomNavigationBar.view.convert(bottomNavigationBar.bounds, to: container.view) - topFrame.origin.x = 0.0 - bottomFrame.origin.x = 0.0 - self.inlineNavigationBarTransition = true// topFrame.equalTo(bottomFrame) + if let topNavigationBar = topNavigationBar, let bottomNavigationBar = bottomNavigationBar { + if let customTransitionNode = topNavigationBar.makeCustomTransitionNode?(bottomNavigationBar, isInteractive) { + self.inlineNavigationBarTransition = false + customTransitionNode.setup(topNavigationBar: topNavigationBar, bottomNavigationBar: bottomNavigationBar) + self.customTransitionNode = customTransitionNode + } else if let customTransitionNode = bottomNavigationBar.makeCustomTransitionNode?(topNavigationBar, isInteractive) { + self.inlineNavigationBarTransition = false + customTransitionNode.setup(topNavigationBar: topNavigationBar, bottomNavigationBar: bottomNavigationBar) + self.customTransitionNode = customTransitionNode + } else if !topNavigationBar.isHidden, !bottomNavigationBar.isHidden, topNavigationBar.canTransitionInline, bottomNavigationBar.canTransitionInline, topNavigationBar.item?.leftBarButtonItem == nil { + var topFrame = topNavigationBar.view.convert(topNavigationBar.bounds, to: container.view) + var bottomFrame = bottomNavigationBar.view.convert(bottomNavigationBar.bounds, to: container.view) + topFrame.origin.x = 0.0 + bottomFrame.origin.x = 0.0 + self.inlineNavigationBarTransition = true + self.customTransitionNode = nil + } else { + self.inlineNavigationBarTransition = false + self.customTransitionNode = nil + } } else { self.inlineNavigationBarTransition = false + self.customTransitionNode = nil } switch transition { - case .Push: - self.container.addSubnode(topNode) - case .Pop: - self.container.insertSubnode(bottomNode, belowSubnode: topNode) + case .Push: + self.container.addSubnode(topNode) + case .Pop: + self.container.insertSubnode(bottomNode, belowSubnode: topNode) } self.container.insertSubnode(self.dimNode, belowSubnode: topNode) - self.container.insertSubnode(self.shadowNode, belowSubnode: dimNode) + self.container.insertSubnode(self.shadowNode, belowSubnode: self.dimNode) + if let customTransitionNode = self.customTransitionNode { + self.container.addSubnode(customTransitionNode) + } self.maybeCreateNavigationBarTransition() self.updateProgress(0.0, transition: .immediate, completion: {}) @@ -91,10 +117,10 @@ class NavigationTransitionCoordinator { let position: CGFloat switch self.transition { - case .Push: - position = 1.0 - progress - case .Pop: - position = progress + case .Push: + position = 1.0 - progress + case .Pop: + position = progress } var dimInset: CGFloat = 0.0 @@ -107,9 +133,16 @@ class NavigationTransitionCoordinator { let topFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(position * containerSize.width), y: 0.0), size: containerSize) let bottomFrame = CGRect(origin: CGPoint(x: ((position - 1.0) * containerSize.width * 0.3), y: 0.0), size: containerSize) + var canInvokeCompletion = false + var hadEarlyCompletion = false transition.updateFrame(node: self.topNode, frame: topFrame, completion: { _ in - completion() + if canInvokeCompletion { + completion() + } else { + hadEarlyCompletion = true + } }) + canInvokeCompletion = true transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: dimInset), size: CGSize(width: max(0.0, topFrame.minX), height: self.container.bounds.size.height - dimInset))) transition.updateFrame(node: self.shadowNode, frame: CGRect(origin: CGPoint(x: self.dimNode.frame.maxX - shadowWidth, y: dimInset), size: CGSize(width: shadowWidth, height: containerSize.height - dimInset))) transition.updateAlpha(node: self.dimNode, alpha: (1.0 - position) * 0.15) @@ -119,10 +152,19 @@ class NavigationTransitionCoordinator { self.updateNavigationBarTransition(transition: transition) + if let customTransitionNode = self.customTransitionNode { + customTransitionNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerSize.width, height: containerSize.height)) + customTransitionNode.update(containerSize: containerSize, fraction: position, transition: transition) + } + self.didUpdateProgress?(self.progress, transition, topFrame, bottomFrame) + + if hadEarlyCompletion { + completion() + } } - func updateNavigationBarTransition(transition: ContainedViewLayoutTransition) { + private func updateNavigationBarTransition(transition: ContainedViewLayoutTransition) { if let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar, self.inlineNavigationBarTransition { let position: CGFloat switch self.transition { @@ -178,6 +220,9 @@ class NavigationTransitionCoordinator { strongSelf.dimNode.removeFromSupernode() strongSelf.shadowNode.removeFromSupernode() + strongSelf.customTransitionNode?.restore() + strongSelf.customTransitionNode?.removeFromSupernode() + strongSelf.endNavigationBarTransition() if let currentCompletion = strongSelf.currentCompletion { @@ -195,6 +240,9 @@ class NavigationTransitionCoordinator { self.dimNode.removeFromSupernode() self.shadowNode.removeFromSupernode() + self.customTransitionNode?.restore() + self.customTransitionNode?.removeFromSupernode() + self.endNavigationBarTransition() if let currentCompletion = self.currentCompletion { @@ -209,6 +257,9 @@ class NavigationTransitionCoordinator { strongSelf.dimNode.removeFromSupernode() strongSelf.shadowNode.removeFromSupernode() + strongSelf.customTransitionNode?.restore() + strongSelf.customTransitionNode?.removeFromSupernode() + strongSelf.endNavigationBarTransition() if let currentCompletion = strongSelf.currentCompletion { @@ -228,6 +279,9 @@ class NavigationTransitionCoordinator { self.dimNode.removeFromSupernode() self.shadowNode.removeFromSupernode() + self.customTransitionNode?.restore() + self.customTransitionNode?.removeFromSupernode() + self.endNavigationBarTransition() if let currentCompletion = self.currentCompletion { diff --git a/submodules/Display/Source/PeekController.swift b/submodules/Display/Source/PeekController.swift index 456c3ceb29..2947e1ecbf 100644 --- a/submodules/Display/Source/PeekController.swift +++ b/submodules/Display/Source/PeekController.swift @@ -37,6 +37,8 @@ public final class PeekController: ViewController { self.sourceNode = sourceNode super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/Display/Source/SubstringSearch.swift b/submodules/Display/Source/SubstringSearch.swift index 07a4ca2179..f3fecf8e90 100644 --- a/submodules/Display/Source/SubstringSearch.swift +++ b/submodules/Display/Source/SubstringSearch.swift @@ -6,34 +6,50 @@ public func findSubstringRanges(in string: String, query: String) -> ([Range)] = [] + if let index = rawSubstring.firstIndex(of: "'") { + let leftString = String(rawSubstring[.. 0 { - let length = Double(max(word.count, substring.count)) - if length > 0 { - let difference = abs(length - Double(count)) - let rating = difference / length - if rating < 0.33 { - var range = range - if hasLeadingSymbol && range.lowerBound > searchRange.lowerBound { - range = text.index(before: range.lowerBound).. 0 { + let length = Double(max(word.count, substring.count)) + if length > 0 { + let difference = abs(length - Double(count)) + let rating = difference / length + if rating < 0.33 { + var range = range + if hasLeadingSymbol && range.lowerBound > searchRange.lowerBound { + range = text.index(before: range.lowerBound).. (UIImage, CGFloat) { +private func tabBarItemImage(_ image: UIImage?, title: String, backgroundColor: UIColor, tintColor: UIColor, horizontal: Bool, imageMode: Bool, centered: Bool = false) -> (UIImage, CGFloat) { let font = horizontal ? Font.regular(13.0) : Font.medium(10.0) let titleSize = (title as NSString).boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin], attributes: [NSAttributedString.Key.font: font], context: nil).size @@ -26,10 +26,12 @@ private func tabBarItemImage(_ image: UIImage?, title: String, backgroundColor: let size: CGSize let contentWidth: CGFloat if horizontal { - size = CGSize(width: max(1.0, ceil(titleSize.width) + horizontalSpacing + imageSize.width), height: 34.0) + let width = max(1.0, centered ? imageSize.width : ceil(titleSize.width) + horizontalSpacing + imageSize.width) + size = CGSize(width: width, height: 34.0) contentWidth = size.width } else { - size = CGSize(width: max(1.0, max(ceil(titleSize.width), imageSize.width), 1.0), height: 45.0) + let width = max(1.0, centered ? imageSize.width : max(ceil(titleSize.width), imageSize.width), 1.0) + size = CGSize(width: width, height: 45.0) contentWidth = imageSize.width } @@ -54,7 +56,7 @@ private func tabBarItemImage(_ image: UIImage?, title: String, backgroundColor: } context.restoreGState() } else { - let imageRect = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - imageSize.width) / 2.0), y: 0.0), size: imageSize) + let imageRect = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - imageSize.width) / 2.0), y: centered ? floor((size.height - imageSize.height) / 2.0) : 0.0), size: imageSize) context.saveGState() context.translateBy(x: imageRect.midX, y: imageRect.midY) context.scaleBy(x: 1.0, y: -1.0) @@ -213,6 +215,7 @@ class TabBarNode: ASDisplayNode { private var theme: TabBarControllerTheme private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)? private var horizontal: Bool = false + private var centered: Bool = false private var badgeImage: UIImage @@ -287,6 +290,8 @@ class TabBarNode: ASDisplayNode { node.badgeContainerNode.removeFromSupernode() } + self.centered = self.theme.tabBarTextColor == .clear + var tabBarNodeContainers: [TabBarNodeContainer] = [] for i in 0 ..< self.tabBarItems.count { let item = self.tabBarItems[i] @@ -302,15 +307,15 @@ class TabBarNode: ASDisplayNode { self?.updateNodeImage(i, layout: true) }) if let selectedIndex = self.selectedIndex, selectedIndex == i { - let (textImage, contentWidth) = tabBarItemImage(item.selectedImage, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: false) - let (image, imageContentWidth) = tabBarItemImage(item.selectedImage, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: true) + let (textImage, contentWidth) = tabBarItemImage(item.selectedImage, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered) + let (image, imageContentWidth) = tabBarItemImage(item.selectedImage, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: true, centered: self.centered) node.textImageNode.image = textImage node.imageNode.image = image node.accessibilityLabel = item.title node.contentWidth = max(contentWidth, imageContentWidth) } else { - let (textImage, contentWidth) = tabBarItemImage(item.image, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarTextColor, horizontal: self.horizontal, imageMode: false) - let (image, imageContentWidth) = tabBarItemImage(item.image, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarTextColor, horizontal: self.horizontal, imageMode: true) + let (textImage, contentWidth) = tabBarItemImage(item.image, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered) + let (image, imageContentWidth) = tabBarItemImage(item.image, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered) node.textImageNode.image = textImage node.accessibilityLabel = item.title node.imageNode.image = image @@ -335,18 +340,20 @@ class TabBarNode: ASDisplayNode { let node = self.tabBarNodeContainers[index].imageNode let item = self.tabBarItems[index] + self.centered = self.theme.tabBarTextColor == .clear + let previousImageSize = node.imageNode.image?.size ?? CGSize() let previousTextImageSize = node.textImageNode.image?.size ?? CGSize() if let selectedIndex = self.selectedIndex, selectedIndex == index { - let (textImage, contentWidth) = tabBarItemImage(item.selectedImage, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: false) - let (image, imageContentWidth) = tabBarItemImage(item.selectedImage, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedIconColor, horizontal: self.horizontal, imageMode: true) + let (textImage, contentWidth) = tabBarItemImage(item.selectedImage, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered) + let (image, imageContentWidth) = tabBarItemImage(item.selectedImage, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered) node.textImageNode.image = textImage node.accessibilityLabel = item.title node.imageNode.image = image node.contentWidth = max(contentWidth, imageContentWidth) } else { - let (textImage, contentWidth) = tabBarItemImage(item.image, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarTextColor, horizontal: self.horizontal, imageMode: false) - let (image, imageContentWidth) = tabBarItemImage(item.image, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarIconColor, horizontal: self.horizontal, imageMode: true) + let (textImage, contentWidth) = tabBarItemImage(item.image, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered) + let (image, imageContentWidth) = tabBarItemImage(item.image, title: item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered) node.textImageNode.image = textImage node.accessibilityLabel = item.title node.imageNode.image = image @@ -423,15 +430,18 @@ class TabBarNode: ASDisplayNode { } if !container.badgeContainerNode.isHidden { - let hasSingleLetterValue = container.badgeTextNode.attributedText?.string.count == 1 + var hasSingleLetterValue: Bool = false + if let string = container.badgeTextNode.attributedText?.string { + hasSingleLetterValue = string.count == 1 + } let badgeSize = container.badgeTextNode.updateLayout(CGSize(width: 200.0, height: 100.0)) let backgroundSize = CGSize(width: hasSingleLetterValue ? 18.0 : max(18.0, badgeSize.width + 10.0 + 1.0), height: 18.0) let backgroundFrame: CGRect if horizontal { - backgroundFrame = CGRect(origin: CGPoint(x: originX + 10.0, y: 2.0), size: backgroundSize) + backgroundFrame = CGRect(origin: CGPoint(x: originX + 15.0, y: 3.0), size: backgroundSize) } else { - let contentWidth = node.contentWidth ?? node.frame.width - backgroundFrame = CGRect(origin: CGPoint(x: floor(originX + node.frame.width / 2.0) + contentWidth - backgroundSize.width - 5.0, y: 2.0), size: backgroundSize) + let contentWidth: CGFloat = 25.0 //node.contentWidth ?? node.frame.width + backgroundFrame = CGRect(origin: CGPoint(x: floor(originX + node.frame.width / 2.0) + contentWidth - backgroundSize.width - 5.0, y: self.centered ? 9.0 : 2.0), size: backgroundSize) } transition.updateFrame(node: container.badgeContainerNode, frame: backgroundFrame) container.badgeBackgroundNode.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size) diff --git a/submodules/Display/Source/TapLongTapOrDoubleTapGestureRecognizer.swift b/submodules/Display/Source/TapLongTapOrDoubleTapGestureRecognizer.swift index 62107686ed..35d1372542 100644 --- a/submodules/Display/Source/TapLongTapOrDoubleTapGestureRecognizer.swift +++ b/submodules/Display/Source/TapLongTapOrDoubleTapGestureRecognizer.swift @@ -68,6 +68,7 @@ public enum TapLongTapOrDoubleTapGestureRecognizerAction { case waitForSingleTap case waitForHold(timeout: Double, acceptTap: Bool) case fail + case keepWithSingleTap } public final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { @@ -206,6 +207,8 @@ public final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer, } switch tapAction { + case .keepWithSingleTap: + break case .waitForSingleTap, .waitForDoubleTap: self.timer?.invalidate() let timer = Timer(timeInterval: 0.3, target: TapLongTapOrDoubleTapGestureRecognizerTimerTarget(target: self), selector: #selector(TapLongTapOrDoubleTapGestureRecognizerTimerTarget.longTapEvent), userInfo: nil, repeats: false) @@ -284,7 +287,7 @@ public final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer, } switch tapAction { - case .waitForSingleTap: + case .waitForSingleTap, .keepWithSingleTap: if let (touchLocation, _) = self.touchLocationAndTimestamp { self.lastRecognizedGestureAndLocation = (.tap, touchLocation) } diff --git a/submodules/Display/Source/TextAlertController.swift b/submodules/Display/Source/TextAlertController.swift index ae2eb242a6..d94ee19393 100644 --- a/submodules/Display/Source/TextAlertController.swift +++ b/submodules/Display/Source/TextAlertController.swift @@ -73,7 +73,7 @@ public final class TextAlertContentActionNode: HighlightableButtonNode { } private func updateTitle() { - var font = Font.regular(17.0) + var font = Font.regular(theme.baseFontSize) var color: UIColor switch self.action.type { case .defaultAction, .genericAction: @@ -83,7 +83,7 @@ public final class TextAlertContentActionNode: HighlightableButtonNode { } switch self.action.type { case .defaultAction: - font = Font.semibold(17.0) + font = Font.semibold(theme.baseFontSize) case .destructiveAction, .genericAction: break } @@ -360,15 +360,15 @@ public func standardTextAlertController(theme: AlertControllerTheme, title: Stri var dismissImpl: (() -> Void)? let attributedText: NSAttributedString if parseMarkdown { - let font = title == nil ? Font.semibold(17.0) : Font.regular(13.0) - let boldFont = title == nil ? Font.bold(17.0) : Font.semibold(13.0) + let font = title == nil ? Font.semibold(theme.baseFontSize * 13.0 / 17.0) : Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)) + let boldFont = title == nil ? Font.bold(theme.baseFontSize * 13.0 / 17.0) : Font.semibold(floor(theme.baseFontSize * 13.0 / 17.0)) let body = MarkdownAttributeSet(font: font, textColor: theme.primaryColor) let bold = MarkdownAttributeSet(font: boldFont, textColor: theme.primaryColor) attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil }), textAlignment: .center) } else { - attributedText = NSAttributedString(string: text, font: title == nil ? Font.semibold(17.0) : Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) + attributedText = NSAttributedString(string: text, font: title == nil ? Font.semibold(theme.baseFontSize) : Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)), textColor: theme.primaryColor, paragraphAlignment: .center) } - let controller = AlertController(theme: theme, contentNode: TextAlertContentNode(theme: theme, title: title != nil ? NSAttributedString(string: title!, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) : nil, text: attributedText, actions: actions.map { action in + let controller = AlertController(theme: theme, contentNode: TextAlertContentNode(theme: theme, title: title != nil ? NSAttributedString(string: title!, font: Font.semibold(theme.baseFontSize), textColor: theme.primaryColor, paragraphAlignment: .center) : nil, text: attributedText, actions: actions.map { action in return TextAlertAction(type: action.type, title: action.title, action: { dismissImpl?() action.action() diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 718b750e72..2cbb4708ca 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -13,6 +13,18 @@ private final class TextNodeStrikethrough { } } +public struct TextRangeRectEdge: Equatable { + public var x: CGFloat + public var y: CGFloat + public var height: CGFloat + + public init(x: CGFloat, y: CGFloat, height: CGFloat) { + self.x = x + self.y = y + self.height = height + } +} + private final class TextNodeLine { let line: CTLine let frame: CGRect @@ -85,6 +97,7 @@ private func displayLineFrame(frame: CGRect, isRTL: Bool, boundingRect: CGRect, public final class TextNodeLayoutArguments { public let attributedString: NSAttributedString? public let backgroundColor: UIColor? + public let minimumNumberOfLines: Int public let maximumNumberOfLines: Int public let truncationType: CTLineTruncationType public let constrainedSize: CGSize @@ -94,10 +107,12 @@ public final class TextNodeLayoutArguments { public let insets: UIEdgeInsets public let lineColor: UIColor? public let textShadowColor: UIColor? + public let textStroke: (UIColor, CGFloat)? - public init(attributedString: NSAttributedString?, backgroundColor: UIColor? = nil, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment = .natural, lineSpacing: CGFloat = 0.12, cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets(), lineColor: UIColor? = nil, textShadowColor: UIColor? = nil) { + public init(attributedString: NSAttributedString?, backgroundColor: UIColor? = nil, minimumNumberOfLines: Int = 0, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment = .natural, lineSpacing: CGFloat = 0.12, cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets(), lineColor: UIColor? = nil, textShadowColor: UIColor? = nil, textStroke: (UIColor, CGFloat)? = nil) { self.attributedString = attributedString self.backgroundColor = backgroundColor + self.minimumNumberOfLines = minimumNumberOfLines self.maximumNumberOfLines = maximumNumberOfLines self.truncationType = truncationType self.constrainedSize = constrainedSize @@ -107,6 +122,7 @@ public final class TextNodeLayoutArguments { self.insets = insets self.lineColor = lineColor self.textShadowColor = textShadowColor + self.textStroke = textStroke } } @@ -128,9 +144,10 @@ public final class TextNodeLayout: NSObject { fileprivate let blockQuotes: [TextNodeBlockQuote] fileprivate let lineColor: UIColor? fileprivate let textShadowColor: UIColor? + fileprivate let textStroke: (UIColor, CGFloat)? public let hasRTL: Bool - fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, rawTextSize: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], blockQuotes: [TextNodeBlockQuote], backgroundColor: UIColor?, lineColor: UIColor?, textShadowColor: UIColor?) { + fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, rawTextSize: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], blockQuotes: [TextNodeBlockQuote], backgroundColor: UIColor?, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?) { self.attributedString = attributedString self.maximumNumberOfLines = maximumNumberOfLines self.truncationType = truncationType @@ -148,6 +165,7 @@ public final class TextNodeLayout: NSObject { self.backgroundColor = backgroundColor self.lineColor = lineColor self.textShadowColor = textShadowColor + self.textStroke = textStroke var hasRTL = false for line in lines { if line.isRTL { @@ -583,11 +601,13 @@ public final class TextNodeLayout: NSObject { return nil } - public func rangeRects(in range: NSRange) -> [CGRect]? { + public func rangeRects(in range: NSRange) -> (rects: [CGRect], start: TextRangeRectEdge, end: TextRangeRectEdge)? { guard let _ = self.attributedString, range.length != 0 else { return nil } var rects: [(CGRect, CGRect)] = [] + var startEdge: TextRangeRectEdge? + var endEdge: TextRangeRectEdge? for line in self.lines { let lineRange = NSIntersectionRange(range, line.range) if lineRange.length != 0 { @@ -610,11 +630,34 @@ public final class TextNodeLayout: NSObject { let width = max(0.0, abs(rightOffset - leftOffset)) + if line.range.contains(range.lowerBound) { + let offsetX = floor(CTLineGetOffsetForStringIndex(line.line, range.lowerBound, nil)) + startEdge = TextRangeRectEdge(x: lineFrame.minX + offsetX, y: lineFrame.minY, height: lineFrame.height) + } + if line.range.contains(range.upperBound - 1) { + let offsetX: CGFloat + if line.range.upperBound == range.upperBound { + offsetX = lineFrame.maxX + } else { + var secondaryOffset: CGFloat = 0.0 + let primaryOffset = floor(CTLineGetOffsetForStringIndex(line.line, range.upperBound - 1, &secondaryOffset)) + secondaryOffset = floor(secondaryOffset) + let nextOffet = floor(CTLineGetOffsetForStringIndex(line.line, range.upperBound, &secondaryOffset)) + + if primaryOffset != secondaryOffset { + offsetX = secondaryOffset + } else { + offsetX = nextOffet + } + } + endEdge = TextRangeRectEdge(x: lineFrame.minX + offsetX, y: lineFrame.minY, height: lineFrame.height) + } + rects.append((lineFrame, CGRect(origin: CGPoint(x: lineFrame.minX + min(leftOffset, rightOffset) + self.insets.left, y: lineFrame.minY + self.insets.top), size: CGSize(width: width, height: lineFrame.size.height)))) } } - if !rects.isEmpty { - return rects.map { $1 } + if !rects.isEmpty, let startEdge = startEdge, let endEdge = endEdge { + return (rects.map { $1 }, startEdge, endEdge) } return nil } @@ -728,7 +771,7 @@ public final class TextAccessibilityOverlayNode: ASDisplayNode { } public class TextNode: ASDisplayNode { - public private(set) var cachedLayout: TextNodeLayout? + public internal(set) var cachedLayout: TextNodeLayout? override public init() { super.init() @@ -762,7 +805,7 @@ public class TextNode: ASDisplayNode { } } - public func rangeRects(in range: NSRange) -> [CGRect]? { + public func rangeRects(in range: NSRange) -> (rects: [CGRect], start: TextRangeRectEdge, end: TextRangeRectEdge)? { if let cachedLayout = self.cachedLayout { return cachedLayout.rangeRects(in: range) } else { @@ -778,7 +821,7 @@ public class TextNode: ASDisplayNode { } } - private class func calculateLayout(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?) -> TextNodeLayout { + private class func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?) -> TextNodeLayout { if let attributedString = attributedString { let stringLength = attributedString.length @@ -804,7 +847,7 @@ public class TextNode: ASDisplayNode { var maybeTypesetter: CTTypesetter? maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString) if maybeTypesetter == nil { - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor) + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textStroke: textStroke) } let typesetter = maybeTypesetter! @@ -886,7 +929,12 @@ public class TextNode: ASDisplayNode { let coreTextLine: CTLine let originalLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 0.0) - if CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) < Double(constrainedSize.width) { + var lineConstrainedSize = constrainedSize + if bottomCutoutEnabled { + lineConstrainedSize.width -= bottomCutoutSize.width + } + + if CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) < Double(lineConstrainedSize.width) { coreTextLine = originalLine } else { var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:] @@ -896,7 +944,7 @@ public class TextNode: ASDisplayNode { let truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes) let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString) - coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(constrainedSize.width), truncationType, truncationToken) ?? truncationToken + coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(lineConstrainedSize.width), truncationType, truncationToken) ?? truncationToken truncated = true } @@ -913,7 +961,7 @@ public class TextNode: ASDisplayNode { } } - let lineWidth = min(constrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))) + let lineWidth = min(lineConstrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))) let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight) layoutSize.height += fontLineHeight + fontLineSpacing layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) @@ -989,7 +1037,7 @@ public class TextNode: ASDisplayNode { if !lines.isEmpty && bottomCutoutEnabled { let proposedWidth = lines[lines.count - 1].frame.width + bottomCutoutSize.width if proposedWidth > layoutSize.width { - if proposedWidth < constrainedSize.width { + if proposedWidth <= constrainedSize.width + .ulpOfOne { layoutSize.width = proposedWidth } else { layoutSize.height += bottomCutoutSize.height @@ -997,9 +1045,20 @@ public class TextNode: ASDisplayNode { } } - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), rawTextSize: CGSize(width: ceil(rawLayoutSize.width) + insets.left + insets.right, height: ceil(rawLayoutSize.height) + insets.top + insets.bottom), truncated: truncated, firstLineOffset: firstLineOffset, lines: lines, blockQuotes: blockQuotes, backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor) + if lines.count < minimumNumberOfLines { + var lineCount = lines.count + while lineCount < minimumNumberOfLines { + if lineCount != 0 { + layoutSize.height += fontLineSpacing + } + layoutSize.height += fontLineHeight + lineCount += 1 + } + } + + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), rawTextSize: CGSize(width: ceil(rawLayoutSize.width) + insets.left + insets.right, height: ceil(rawLayoutSize.height) + insets.top + insets.bottom), truncated: truncated, firstLineOffset: firstLineOffset, lines: lines, blockQuotes: blockQuotes, backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textStroke: textStroke) } else { - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor) + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textStroke: textStroke) } } @@ -1037,6 +1096,16 @@ public class TextNode: ASDisplayNode { context.setShadow(offset: CGSize(width: 0.0, height: 1.0), blur: 0.0, color: textShadowColor.cgColor) } + if let (textStrokeColor, textStrokeWidth) = layout.textStroke { + context.setBlendMode(.normal) + context.setLineCap(.round) + context.setLineJoin(.round) + context.setStrokeColor(textStrokeColor.cgColor) + context.setFillColor(textStrokeColor.cgColor) + context.setLineWidth(textStrokeWidth) + context.setTextDrawingMode(.fillStroke) + } + let textMatrix = context.textMatrix let textPosition = context.textPosition context.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0) @@ -1143,11 +1212,11 @@ public class TextNode: ASDisplayNode { if stringMatch { layout = existingLayout } else { - layout = TextNode.calculateLayout(attributedString: arguments.attributedString, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor) + layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke) updated = true } } else { - layout = TextNode.calculateLayout(attributedString: arguments.attributedString, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor) + layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke) updated = true } diff --git a/submodules/Display/Source/TooltipController.swift b/submodules/Display/Source/TooltipController.swift index 2b440b21b1..34c320883d 100644 --- a/submodules/Display/Source/TooltipController.swift +++ b/submodules/Display/Source/TooltipController.swift @@ -62,6 +62,7 @@ open class TooltipController: ViewController, StandalonePresentableController { } public private(set) var content: TooltipControllerContent + private let baseFontSize: CGFloat open func updateContent(_ content: TooltipControllerContent, animated: Bool, extendTimer: Bool, arrowOnBottom: Bool = true) { if self.content != content { @@ -89,8 +90,9 @@ open class TooltipController: ViewController, StandalonePresentableController { public var dismissed: ((Bool) -> Void)? - public init(content: TooltipControllerContent, timeout: Double = 2.0, dismissByTapOutside: Bool = false, dismissByTapOutsideSource: Bool = false, dismissImmediatelyOnLayoutUpdate: Bool = false, arrowOnBottom: Bool = true) { + public init(content: TooltipControllerContent, baseFontSize: CGFloat, timeout: Double = 2.0, dismissByTapOutside: Bool = false, dismissByTapOutsideSource: Bool = false, dismissImmediatelyOnLayoutUpdate: Bool = false, arrowOnBottom: Bool = true) { self.content = content + self.baseFontSize = baseFontSize self.timeout = timeout self.dismissByTapOutside = dismissByTapOutside self.dismissByTapOutsideSource = dismissByTapOutsideSource @@ -111,7 +113,7 @@ open class TooltipController: ViewController, StandalonePresentableController { } override open func loadDisplayNode() { - self.displayNode = TooltipControllerNode(content: self.content, dismiss: { [weak self] tappedInside in + self.displayNode = TooltipControllerNode(content: self.content, baseFontSize: self.baseFontSize, dismiss: { [weak self] tappedInside in self?.dismiss(tappedInside: tappedInside) }, dismissByTapOutside: self.dismissByTapOutside, dismissByTapOutsideSource: self.dismissByTapOutsideSource) self.controllerNode.arrowOnBottom = self.initialArrowOnBottom @@ -183,6 +185,7 @@ open class TooltipController: ViewController, StandalonePresentableController { open func dismissImmediately() { self.dismissed?(false) + self.controllerNode.hide() self.presentingViewController?.dismiss(animated: false) } } diff --git a/submodules/Display/Source/TooltipControllerNode.swift b/submodules/Display/Source/TooltipControllerNode.swift index 3b39885dc0..ba0650c6ee 100644 --- a/submodules/Display/Source/TooltipControllerNode.swift +++ b/submodules/Display/Source/TooltipControllerNode.swift @@ -3,6 +3,8 @@ import UIKit import AsyncDisplayKit final class TooltipControllerNode: ASDisplayNode { + private let baseFontSize: CGFloat + private let dismiss: (Bool) -> Void private var validLayout: ContainerViewLayout? @@ -19,7 +21,9 @@ final class TooltipControllerNode: ASDisplayNode { private var dismissedByTouchOutside = false private var dismissByTapOutsideSource = false - init(content: TooltipControllerContent, dismiss: @escaping (Bool) -> Void, dismissByTapOutside: Bool, dismissByTapOutsideSource: Bool) { + init(content: TooltipControllerContent, baseFontSize: CGFloat, dismiss: @escaping (Bool) -> Void, dismissByTapOutside: Bool, dismissByTapOutsideSource: Bool) { + self.baseFontSize = baseFontSize + self.dismissByTapOutside = dismissByTapOutside self.dismissByTapOutsideSource = dismissByTapOutsideSource @@ -33,7 +37,7 @@ final class TooltipControllerNode: ASDisplayNode { if case let .attributedText(text) = content { self.textNode.attributedText = text } else { - self.textNode.attributedText = NSAttributedString(string: content.text, font: Font.regular(14.0), textColor: .white, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: content.text, font: Font.regular(floor(baseFontSize * 14.0 / 17.0)), textColor: .white, paragraphAlignment: .center) } self.textNode.isUserInteractionEnabled = false self.textNode.displaysAsynchronously = false @@ -58,7 +62,7 @@ final class TooltipControllerNode: ASDisplayNode { }) self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) } - self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(floor(self.baseFontSize * 14.0 / 17.0)), textColor: .white, paragraphAlignment: .center) if let layout = self.validLayout { self.containerLayoutUpdated(layout, transition: transition) } @@ -122,6 +126,10 @@ final class TooltipControllerNode: ASDisplayNode { }) } + func hide() { + self.containerNode.alpha = 0.0 + } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let event = event { var eventIsPresses = false diff --git a/submodules/Display/Source/TransformImageArguments.swift b/submodules/Display/Source/TransformImageArguments.swift index a7b1f64d25..ae8051f6ea 100644 --- a/submodules/Display/Source/TransformImageArguments.swift +++ b/submodules/Display/Source/TransformImageArguments.swift @@ -7,6 +7,10 @@ public enum TransformImageResizeMode { case blurBackground } +public protocol TransformImageCustomArguments { + func serialized() -> NSArray +} + public struct TransformImageArguments: Equatable { public let corners: ImageCorners @@ -15,15 +19,17 @@ public struct TransformImageArguments: Equatable { public let intrinsicInsets: UIEdgeInsets public let resizeMode: TransformImageResizeMode public let emptyColor: UIColor? + public let custom: TransformImageCustomArguments? public let scale: CGFloat? - public init(corners: ImageCorners, imageSize: CGSize, boundingSize: CGSize, intrinsicInsets: UIEdgeInsets, resizeMode: TransformImageResizeMode = .fill(.black), emptyColor: UIColor? = nil, scale: CGFloat? = nil) { + public init(corners: ImageCorners, imageSize: CGSize, boundingSize: CGSize, intrinsicInsets: UIEdgeInsets, resizeMode: TransformImageResizeMode = .fill(.black), emptyColor: UIColor? = nil, custom: TransformImageCustomArguments? = nil, scale: CGFloat? = nil) { self.corners = corners self.imageSize = imageSize self.boundingSize = boundingSize self.intrinsicInsets = intrinsicInsets self.resizeMode = resizeMode self.emptyColor = emptyColor + self.custom = custom self.scale = scale } @@ -34,7 +40,12 @@ public struct TransformImageArguments: Equatable { public var drawingRect: CGRect { let cornersExtendedEdges = self.corners.extendedEdges - return CGRect(x: cornersExtendedEdges.left + self.intrinsicInsets.left, y: cornersExtendedEdges.top + self.intrinsicInsets.top, width: self.boundingSize.width, height: self.boundingSize.height); + return CGRect(x: cornersExtendedEdges.left + self.intrinsicInsets.left, y: cornersExtendedEdges.top + self.intrinsicInsets.top, width: self.boundingSize.width, height: self.boundingSize.height) + } + + public var imageRect: CGRect { + let drawingRect = self.drawingRect + return CGRect(x: drawingRect.minX + floor((drawingRect.width - self.imageSize.width) / 2.0), y: drawingRect.minX + floor((drawingRect.height - self.imageSize.height) / 2.0), width: self.imageSize.width, height: self.imageSize.height) } public var insets: UIEdgeInsets { @@ -43,6 +54,14 @@ public struct TransformImageArguments: Equatable { } public static func ==(lhs: TransformImageArguments, rhs: TransformImageArguments) -> Bool { - return lhs.imageSize == rhs.imageSize && lhs.boundingSize == rhs.boundingSize && lhs.corners == rhs.corners && lhs.emptyColor == rhs.emptyColor + var result = lhs.imageSize == rhs.imageSize && lhs.boundingSize == rhs.boundingSize && lhs.corners == rhs.corners && lhs.emptyColor == rhs.emptyColor + if result { + if let lhsCustom = lhs.custom, let rhsCustom = rhs.custom { + return lhsCustom.serialized().isEqual(rhsCustom.serialized()) + } else { + return (lhs.custom != nil) == (rhs.custom != nil) + } + } + return result } } diff --git a/submodules/Display/Source/TransformImageNode.swift b/submodules/Display/Source/TransformImageNode.swift index 36e72a6568..311ce62ee1 100644 --- a/submodules/Display/Source/TransformImageNode.swift +++ b/submodules/Display/Source/TransformImageNode.swift @@ -46,6 +46,13 @@ open class TransformImageNode: ASDisplayNode { } } + public func reset() { + self.disposable.set(nil) + self.currentArguments = nil + self.currentTransform = nil + self.contents = nil + } + public func setSignal(_ signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>, attemptSynchronously: Bool = false, dispatchOnDisplayLink: Bool = true) { let argumentsPromise = self.argumentsPromise @@ -74,7 +81,7 @@ open class TransformImageNode: ASDisplayNode { let apply: () -> Void = { if let strongSelf = self { if strongSelf.contents == nil { - if strongSelf.contentAnimations.contains(.firstUpdate) { + if strongSelf.contentAnimations.contains(.firstUpdate) && !attemptSynchronously { strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } } else if strongSelf.contentAnimations.contains(.subsequentUpdates) { diff --git a/submodules/Display/Source/UIKitUtils.swift b/submodules/Display/Source/UIKitUtils.swift index 884fbfc9f9..a8ad19277a 100644 --- a/submodules/Display/Source/UIKitUtils.swift +++ b/submodules/Display/Source/UIKitUtils.swift @@ -106,12 +106,12 @@ public extension UIColor { } } - var hsv: (CGFloat, CGFloat, CGFloat) { + var hsb: (CGFloat, CGFloat, CGFloat) { var hue: CGFloat = 0.0 var saturation: CGFloat = 0.0 - var value: CGFloat = 0.0 - if self.getHue(&hue, saturation: &saturation, brightness: &value, alpha: nil) { - return (hue, saturation, value) + var brightness: CGFloat = 0.0 + if self.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil) { + return (hue, saturation, brightness) } else { return (0.0, 0.0, 0.0) } @@ -318,6 +318,8 @@ private func makeSubtreeSnapshot(layer: CALayer, keepTransform: Bool = false) -> maskLayer.contentsScale = mask.contentsScale maskLayer.contentsCenter = mask.contentsCenter maskLayer.contentsGravity = mask.contentsGravity + maskLayer.frame = mask.frame + maskLayer.bounds = mask.bounds view.layer.mask = maskLayer } view.layer.cornerRadius = layer.cornerRadius diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index 00b59e1373..6aff11db49 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -93,6 +93,8 @@ public enum ViewControllerNavigationPresentation { } } + var blocksInteractionUntilReady: Bool = false + public final var isOpaqueWhenInOverlay: Bool = false public final var blocksBackgroundWhenInOverlay: Bool = false public final var automaticallyControlPresentationContextLayout: Bool = true @@ -131,6 +133,15 @@ public enum ViewControllerNavigationPresentation { public var tabBarItemDebugTapAction: (() -> Void)? + public private(set) var modalStyleOverlayTransitionFactor: CGFloat = 0.0 + public var modalStyleOverlayTransitionFactorUpdated: ((ContainedViewLayoutTransition) -> Void)? + public func updateModalStyleOverlayTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) { + if self.modalStyleOverlayTransitionFactor != value { + self.modalStyleOverlayTransitionFactor = value + self.modalStyleOverlayTransitionFactorUpdated?(transition) + } + } + private var _displayNode: ASDisplayNode? public final var displayNode: ASDisplayNode { get { @@ -445,8 +456,7 @@ public enum ViewControllerNavigationPresentation { } override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) { - super.present(viewControllerToPresent, animated: flag, completion: completion) - return + self.view.window?.rootViewController?.present(viewControllerToPresent, animated: flag, completion: completion) } override open func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { @@ -526,7 +536,11 @@ public enum ViewControllerNavigationPresentation { } open func dismiss(completion: (() -> Void)? = nil) { - (self.navigationController as? NavigationController)?.filterController(self, animated: true) + if let navigationController = self.navigationController as? NavigationController { + navigationController.filterController(self, animated: true) + } else { + self.presentingViewController?.dismiss(animated: false, completion: nil) + } } @available(iOSApplicationExtension 9.0, iOS 9.0, *) diff --git a/submodules/Display/Source/WallpaperBackgroundNode.swift b/submodules/Display/Source/WallpaperBackgroundNode.swift index cf8e9c8f79..203f3632de 100644 --- a/submodules/Display/Source/WallpaperBackgroundNode.swift +++ b/submodules/Display/Source/WallpaperBackgroundNode.swift @@ -27,17 +27,37 @@ public final class WallpaperBackgroundNode: ASDisplayNode { self.contentNode.view.removeMotionEffect(effect) } } - self.updateScale() + if !self.frame.isEmpty { + self.updateScale() + } } } } - + public var image: UIImage? { didSet { self.contentNode.contents = self.image?.cgImage } } + public var rotation: CGFloat = 0.0 { + didSet { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) + var fromValue: CGFloat = 0.0 + if let value = (self.layer.value(forKeyPath: "transform.rotation.z") as? NSNumber)?.floatValue { + fromValue = CGFloat(value) + } + self.contentNode.layer.transform = CATransform3DMakeRotation(self.rotation, 0.0, 0.0, 1.0) + self.contentNode.layer.animateRotation(from: fromValue, to: self.rotation, duration: 0.3) + } + } + + public var imageContentMode: UIView.ContentMode { + didSet { + self.contentNode.contentMode = self.imageContentMode + } + } + func updateScale() { if self.motionEnabled { let scale = (self.frame.width + motionAmount * 2.0) / self.frame.width @@ -48,8 +68,10 @@ public final class WallpaperBackgroundNode: ASDisplayNode { } public override init() { + self.imageContentMode = .scaleAspectFill + self.contentNode = ASDisplayNode() - self.contentNode.contentMode = .scaleAspectFill + self.contentNode.contentMode = self.imageContentMode super.init() @@ -58,10 +80,13 @@ public final class WallpaperBackgroundNode: ASDisplayNode { self.addSubnode(self.contentNode) } - override public func layout() { - super.layout() - self.contentNode.bounds = self.bounds - self.contentNode.position = CGPoint(x: self.bounds.midX, y: self.bounds.midY) - self.updateScale() + public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + let isFirstLayout = self.contentNode.frame.isEmpty + transition.updatePosition(node: self.contentNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + transition.updateBounds(node: self.contentNode, bounds: CGRect(origin: CGPoint(), size: size)) + + if isFirstLayout && !self.frame.isEmpty { + self.updateScale() + } } } diff --git a/submodules/Display/Source/WindowContent.swift b/submodules/Display/Source/WindowContent.swift index be9568d986..ba17a06b9a 100644 --- a/submodules/Display/Source/WindowContent.swift +++ b/submodules/Display/Source/WindowContent.swift @@ -125,8 +125,8 @@ private func encodeText(_ string: String, _ key: Int) -> String { return result } -public func doesViewTreeDisableInteractiveTransitionGestureRecognizer(_ view: UIView) -> Bool { - if view.disablesInteractiveTransitionGestureRecognizer { +public func doesViewTreeDisableInteractiveTransitionGestureRecognizer(_ view: UIView, keyboardOnly: Bool = false) -> Bool { + if view.disablesInteractiveTransitionGestureRecognizer && !keyboardOnly { return true } if view.disablesInteractiveKeyboardGestureRecognizer { @@ -136,7 +136,7 @@ public func doesViewTreeDisableInteractiveTransitionGestureRecognizer(_ view: UI return true } if let superview = view.superview { - return doesViewTreeDisableInteractiveTransitionGestureRecognizer(superview) + return doesViewTreeDisableInteractiveTransitionGestureRecognizer(superview, keyboardOnly: keyboardOnly) } return false } @@ -267,6 +267,8 @@ public class Window1 { public var previewThemeAccentColor: UIColor = .blue public var previewThemeDarkBlur: Bool = false + private var shouldNotAnimateLikelyKeyboardAutocorrectionSwitch: Bool = false + public private(set) var forceInCallStatusBarText: String? = nil public var inCallNavigate: (() -> Void)? { didSet { @@ -330,8 +332,17 @@ public class Window1 { self?.isInteractionBlocked = value } + let updateOpaqueOverlays: () -> Void = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf._rootController?.displayNode.accessibilityElementsHidden = strongSelf.presentationContext.hasOpaqueOverlay || strongSelf.topPresentationContext.hasOpaqueOverlay + } self.presentationContext.updateHasOpaqueOverlay = { [weak self] value in - self?._rootController?.displayNode.accessibilityElementsHidden = value + updateOpaqueOverlays() + } + self.topPresentationContext.updateHasOpaqueOverlay = { [weak self] value in + updateOpaqueOverlays() } self.hostView.present = { [weak self] controller, level, blockInteraction, completion in @@ -513,7 +524,15 @@ public class Window1 { transitionCurve = .easeInOut } - strongSelf.updateLayout { $0.update(inputHeight: keyboardHeight.isLessThanOrEqualTo(0.0) ? nil : keyboardHeight, transition: .animated(duration: duration, curve: transitionCurve), overrideTransition: false) } + var transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: transitionCurve) + + if strongSelf.shouldNotAnimateLikelyKeyboardAutocorrectionSwitch, let inputHeight = strongSelf.windowLayout.inputHeight { + if abs(inputHeight - keyboardHeight) <= 44.1 { + transition = .immediate + } + } + + strongSelf.updateLayout { $0.update(inputHeight: keyboardHeight.isLessThanOrEqualTo(0.0) ? nil : keyboardHeight, transition: transition, overrideTransition: false) } } }) @@ -706,6 +725,36 @@ public class Window1 { if let rootController = self._rootController { if let rootController = rootController as? NavigationController { rootController.statusBarHost = self.statusBarHost + rootController.updateSupportedOrientations = { [weak self] in + guard let strongSelf = self else { + return + } + + var supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all) + let orientationToLock: UIInterfaceOrientationMask + if strongSelf.windowLayout.size.width < strongSelf.windowLayout.size.height { + orientationToLock = .portrait + } else { + orientationToLock = .landscape + } + if let _rootController = strongSelf._rootController { + supportedOrientations = supportedOrientations.intersection(_rootController.combinedSupportedOrientations(currentOrientationToLock: orientationToLock)) + } + supportedOrientations = supportedOrientations.intersection(strongSelf.presentationContext.combinedSupportedOrientations(currentOrientationToLock: orientationToLock)) + supportedOrientations = supportedOrientations.intersection(strongSelf.overlayPresentationContext.combinedSupportedOrientations(currentOrientationToLock: orientationToLock)) + + var resolvedOrientations: UIInterfaceOrientationMask + switch strongSelf.windowLayout.metrics.widthClass { + case .regular: + resolvedOrientations = supportedOrientations.regularSize + case .compact: + resolvedOrientations = supportedOrientations.compactSize + } + if resolvedOrientations.isEmpty { + resolvedOrientations = [.portrait] + } + strongSelf.hostView.updateSupportedInterfaceOrientations(resolvedOrientations) + } rootController.keyboardViewManager = self.keyboardViewManager rootController.inCallNavigate = { [weak self] in self?.inCallNavigate?() @@ -1056,7 +1105,7 @@ public class Window1 { if let inputHeight = self.windowLayout.inputHeight, !inputHeight.isZero, keyboardGestureBeginLocation.y < self.windowLayout.size.height - inputHeight - (accessoryHeight ?? 0.0) { var enableGesture = true if let view = self.hostView.containerView.hitTest(location, with: nil) { - if doesViewTreeDisableInteractiveTransitionGestureRecognizer(view) { + if doesViewTreeDisableInteractiveTransitionGestureRecognizer(view, keyboardOnly: true) { enableGesture = false } } @@ -1173,9 +1222,16 @@ public class Window1 { return hidden } - public func forEachViewController(_ f: (ContainableController) -> Bool) { - if let navigationController = self._rootController as? NavigationController, let controller = navigationController.topOverlayController { - !f(controller) + public func forEachViewController(_ f: (ContainableController) -> Bool, excludeNavigationSubControllers: Bool = false) { + if let navigationController = self._rootController as? NavigationController { + if !excludeNavigationSubControllers { + for case let controller as ContainableController in navigationController.viewControllers { + !f(controller) + } + } + if let controller = navigationController.topOverlayController { + !f(controller) + } } for (controller, _) in self.presentationContext.controllers { if !f(controller) { @@ -1194,4 +1250,11 @@ public class Window1 { } } } + + public func doNotAnimateLikelyKeyboardAutocorrectionSwitch() { + self.shouldNotAnimateLikelyKeyboardAutocorrectionSwitch = true + DispatchQueue.main.async { + self.shouldNotAnimateLikelyKeyboardAutocorrectionSwitch = false + } + } } diff --git a/submodules/Emoji/Sources/EmojiUtils.swift b/submodules/Emoji/Sources/EmojiUtils.swift index dc8a1fa997..da916e7492 100644 --- a/submodules/Emoji/Sources/EmojiUtils.swift +++ b/submodules/Emoji/Sources/EmojiUtils.swift @@ -7,7 +7,7 @@ public extension UnicodeScalar { switch self.value { case 0x1F600...0x1F64F, 0x1F300...0x1F5FF, 0x1F680...0x1F6FF, 0x1F1E6...0x1F1FF, 0xE0020...0xE007F, 0xFE00...0xFE0F, 0x1F900...0x1F9FF, 0x1F018...0x1F0F5, 0x1F200...0x1F270, 65024...65039, 9100...9300, 8400...8447, 0x1F004, 0x1F18E, 0x1F191...0x1F19A, 0x1F5E8: return true - case 0x265F, 0x267E, 0x2692, 0x26C8, 0x26CE, 0x26CF, 0x26D1...0x26D3, 0x26E9, 0x26F0...0x26F9, 0x2705, 0x270A, 0x270B, 0x2728, 0x274E, 0x2753...0x2755, 0x274C, 0x2795...0x2797, 0x27B0, 0x27BF: + case 0x2603, 0x265F, 0x267E, 0x2692, 0x26C4, 0x26C8, 0x26CE, 0x26CF, 0x26D1...0x26D3, 0x26E9, 0x26F0...0x26F9, 0x2705, 0x270A, 0x270B, 0x2728, 0x274E, 0x2753...0x2755, 0x274C, 0x2795...0x2797, 0x27B0, 0x27BF: return true default: return false @@ -142,4 +142,14 @@ public extension String { } return (string, fitzModifier) } + + var strippedEmoji: (String) { + var string = "" + for scalar in self.unicodeScalars { + if scalar.value != 0xfe0f { + string.unicodeScalars.append(scalar) + } + } + return string + } } diff --git a/submodules/GalleryUI/BUCK b/submodules/GalleryUI/BUCK index 168d8bb330..7b4130c6ab 100644 --- a/submodules/GalleryUI/BUCK +++ b/submodules/GalleryUI/BUCK @@ -23,6 +23,9 @@ static_library( "//submodules/SwipeToDismissGesture:SwipeToDismissGesture", "//submodules/CheckNode:CheckNode", "//submodules/AppBundle:AppBundle", + "//submodules/StickerPackPreviewUI:StickerPackPreviewUI", + "//submodules/OverlayStatusController:OverlayStatusController", + "//submodules/PresentationDataUtils:PresentationDataUtils", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 0c8ccdb12d..2a31c310a1 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -106,6 +106,7 @@ class CaptionScrollWrapperNode: ASDisplayNode { final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScrollViewDelegate { private let context: AccountContext + private var presentationData: PresentationData private var theme: PresentationTheme private var strings: PresentationStrings private var nameOrder: PresentationPersonNameOrder @@ -167,7 +168,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.dateNode.isHidden = true self.backwardButton.isHidden = true self.forwardButton.isHidden = true - self.playbackControlButton.isHidden = true + if status == .Local { + self.playbackControlButton.isHidden = false + self.playbackControlButton.setImage(playImage, for: []) + } else { + self.playbackControlButton.isHidden = true + } self.statusButtonNode.isHidden = false self.statusNode.isHidden = false @@ -243,6 +249,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll init(context: AccountContext, presentationData: PresentationData) { self.context = context + self.presentationData = presentationData self.theme = presentationData.theme self.strings = presentationData.strings self.nameOrder = presentationData.nameDisplayOrder @@ -726,7 +733,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } var generalMessageContentKind: MessageContentKind? for message in messages { - let currentKind = messageContentKind(message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: strongSelf.context.account.peerId) + let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: strongSelf.context.account.peerId) if generalMessageContentKind == nil || generalMessageContentKind == currentKind { generalMessageContentKind = currentKind } else { @@ -757,7 +764,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) let items: [ActionSheetItem] = [ ActionSheetButtonItem(title: singleText, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -772,7 +779,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll actionSheet.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) @@ -787,7 +794,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll private func commitDeleteMessages(_ messages: [Message], ask: Bool) { self.messageContextDisposable.set((self.context.sharedContext.chatAvailableMessageActions(postbox: self.context.account.postbox, accountPeerId: self.context.account.peerId, messageIds: Set(messages.map { $0.id })) |> deliverOnMainQueue).start(next: { [weak self] actions in if let strongSelf = self, let controllerInteration = strongSelf.controllerInteraction, !actions.options.isEmpty { - let actionSheet = ActionSheetController(presentationTheme: strongSelf.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] var personalPeerName: String? var isChannel = false @@ -810,7 +817,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: messages.map { $0.id }, type: .forEveryone).start() + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messages.map { $0.id }, type: .forEveryone).start() strongSelf.controllerInteraction?.dismissController() } })) @@ -825,17 +832,17 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: messages.map { $0.id }, type: .forLocalPeer).start() + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messages.map { $0.id }, type: .forLocalPeer).start() strongSelf.controllerInteraction?.dismissController() } })) } if !ask && items.count == 1 { - let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: messages.map { $0.id }, type: .forEveryone).start() + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messages.map { $0.id }, type: .forEveryone).start() strongSelf.controllerInteraction?.dismissController() } else if !items.isEmpty { actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -854,7 +861,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } var generalMessageContentKind: MessageContentKind? for message in messages { - let currentKind = messageContentKind(message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: strongSelf.context.account.peerId) + let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: strongSelf.context.account.peerId) if generalMessageContentKind == nil || generalMessageContentKind == currentKind { generalMessageContentKind = currentKind } else { @@ -947,7 +954,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) let items: [ActionSheetItem] = [ ActionSheetButtonItem(title: singleText, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -961,7 +968,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 0f688f18dd..6d5d0a7067 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -222,10 +222,10 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese } public final class GalleryTransitionArguments { - public let transitionNode: (ASDisplayNode, () -> (UIView?, UIView?)) + public let transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)) public let addToTransitionSurface: (UIView) -> Void - public init(transitionNode: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: @escaping (UIView) -> Void) { + public init(transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: @escaping (UIView) -> Void) { self.transitionNode = transitionNode self.addToTransitionSurface = addToTransitionSurface } @@ -253,6 +253,33 @@ private enum GalleryMessageHistoryView { return [entry] } } + + var tagMask: MessageTags? { + switch self { + case .single: + return nil + case let .view(view): + return view.tagMask + } + } + + var hasEarlier: Bool { + switch self { + case .single: + return false + case let .view(view): + return view.earlierId != nil + } + } + + var hasLater: Bool { + switch self { + case .single: + return false + case let .view(view): + return view.laterId != nil + } + } } public enum GalleryControllerItemSource { @@ -304,6 +331,7 @@ public class GalleryController: ViewController, StandalonePresentableController private let context: AccountContext private var presentationData: PresentationData private let source: GalleryControllerItemSource + private let invertItemOrder: Bool private let streamVideos: Bool @@ -324,6 +352,9 @@ public class GalleryController: ViewController, StandalonePresentableController private let disposable = MetaDisposable() private var entries: [MessageHistoryEntry] = [] + private var hasLeftEntries: Bool = false + private var hasRightEntries: Bool = false + private var tagMask: MessageTags? private var centralEntryStableId: UInt32? private var configuration: GalleryConfiguration? @@ -332,7 +363,7 @@ public class GalleryController: ViewController, StandalonePresentableController private let centralItemRightBarButtonItem = Promise() private let centralItemRightBarButtonItems = Promise<[UIBarButtonItem]?>(nil) private let centralItemNavigationStyle = Promise() - private let centralItemFooterContentNode = Promise() + private let centralItemFooterContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>() private let centralItemAttributesDisposable = DisposableSet(); private let _hiddenMedia = Promise<(MessageId, Media)?>(nil) @@ -346,9 +377,12 @@ public class GalleryController: ViewController, StandalonePresentableController private var performAction: (GalleryControllerInteractionTapAction) -> Void private var openActionOptions: (GalleryControllerInteractionTapAction) -> Void + private let updateVisibleDisposable = MetaDisposable() + public init(context: AccountContext, source: GalleryControllerItemSource, invertItemOrder: Bool = false, streamSingleVideo: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, synchronousLoad: Bool = false, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, baseNavigationController: NavigationController?, actionInteraction: GalleryControllerActionInteraction? = nil) { self.context = context self.source = source + self.invertItemOrder = invertItemOrder self.replaceRootController = replaceRootController self.baseNavigationController = baseNavigationController self.actionInteraction = actionInteraction @@ -396,7 +430,7 @@ public class GalleryController: ViewController, StandalonePresentableController } else { namespaces = .not(Namespaces.Message.allScheduled) } - return context.account.postbox.aroundMessageHistoryViewForLocation(.peer(message!.id.peerId), anchor: .index(message!.index), count: 50, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tags, namespaces: namespaces, orderStatistics: [.combinedLocation]) + return context.account.postbox.aroundMessageHistoryViewForLocation(.peer(message!.id.peerId), anchor: .index(message!.index), count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tags, namespaces: namespaces, orderStatistics: [.combinedLocation]) |> mapToSignal { (view, _, _) -> Signal in let mapped = GalleryMessageHistoryView.view(view) return .single(mapped) @@ -444,13 +478,19 @@ public class GalleryController: ViewController, StandalonePresentableController } } + strongSelf.tagMask = view.tagMask + if invertItemOrder { strongSelf.entries = entries.reversed() + strongSelf.hasLeftEntries = view.hasLater + strongSelf.hasRightEntries = view.hasEarlier if let centralEntryStableId = centralEntryStableId { strongSelf.centralEntryStableId = centralEntryStableId } } else { strongSelf.entries = entries + strongSelf.hasLeftEntries = view.hasEarlier + strongSelf.hasRightEntries = view.hasLater strongSelf.centralEntryStableId = centralEntryStableId } if strongSelf.isViewLoaded { @@ -531,9 +571,9 @@ public class GalleryController: ViewController, StandalonePresentableController } })) - self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode in + self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode, overlayContentNode in self?.galleryNode.updatePresentationState({ - $0.withUpdatedFooterContentNode(footerContentNode) + $0.withUpdatedFooterContentNode(footerContentNode).withUpdatedOverlayContentNode(overlayContentNode) }, transition: .immediate) })) @@ -609,7 +649,7 @@ public class GalleryController: ViewController, StandalonePresentableController } else if canOpenIn { openText = strongSelf.presentationData.strings.Conversation_FileOpenIn } - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: cleanUrl)) @@ -646,13 +686,13 @@ public class GalleryController: ViewController, StandalonePresentableController })) } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.present(actionSheet, in: .window(.root)) case let .peerMention(peerId, mention): - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] if !mention.isEmpty { items.append(ActionSheetTextItem(title: mention)) @@ -671,13 +711,13 @@ public class GalleryController: ViewController, StandalonePresentableController })) } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.present(actionSheet, in: .window(.root)) case let .textMention(mention): - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: mention), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in @@ -692,13 +732,13 @@ public class GalleryController: ViewController, StandalonePresentableController UIPasteboard.general.string = mention }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.present(actionSheet, in: .window(.root)) case let .botCommand(command): - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: command)) items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in @@ -706,13 +746,13 @@ public class GalleryController: ViewController, StandalonePresentableController UIPasteboard.general.string = command })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.present(actionSheet, in: .window(.root)) case let .hashtag(peerName, hashtag): - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: hashtag), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in @@ -727,14 +767,14 @@ public class GalleryController: ViewController, StandalonePresentableController UIPasteboard.general.string = hashtag }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) ]) strongSelf.present(actionSheet, in: .window(.root)) case let .timecode(timecode, text): - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: text), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in @@ -749,7 +789,7 @@ public class GalleryController: ViewController, StandalonePresentableController UIPasteboard.general.string = text }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) @@ -774,6 +814,7 @@ public class GalleryController: ViewController, StandalonePresentableController if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex { self.context.sharedContext.mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex) } + self.updateVisibleDisposable.dispose() } @objc private func donePressed() { @@ -898,6 +939,7 @@ public class GalleryController: ViewController, StandalonePresentableController var hiddenItem: (MessageId, Media)? if let index = index { let message = strongSelf.entries[index].message + strongSelf.centralEntryStableId = message.stableId if let (media, _) = mediaForMessage(message: message) { hiddenItem = (message.id, media) } @@ -910,6 +952,69 @@ public class GalleryController: ViewController, StandalonePresentableController strongSelf.centralItemNavigationStyle.set(node.navigationStyle()) strongSelf.centralItemFooterContentNode.set(node.footerContent()) } + + switch strongSelf.source { + case let .peerMessagesAtId(initialMessageId): + var reloadAroundIndex: MessageIndex? + if index <= 2 && strongSelf.hasLeftEntries { + reloadAroundIndex = strongSelf.entries.first?.index + } else if index >= strongSelf.entries.count - 3 && strongSelf.hasRightEntries { + reloadAroundIndex = strongSelf.entries.last?.index + } + if let reloadAroundIndex = reloadAroundIndex, let tagMask = strongSelf.tagMask { + let namespaces: MessageIdNamespaces + if Namespaces.Message.allScheduled.contains(message.id.namespace) { + namespaces = .just(Namespaces.Message.allScheduled) + } else { + namespaces = .not(Namespaces.Message.allScheduled) + } + let signal = strongSelf.context.account.postbox.aroundMessageHistoryViewForLocation(.peer(initialMessageId.peerId), anchor: .index(reloadAroundIndex), count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: [.combinedLocation]) + |> mapToSignal { (view, _, _) -> Signal in + let mapped = GalleryMessageHistoryView.view(view) + return .single(mapped) + } + |> take(1) + + strongSelf.updateVisibleDisposable.set((signal + |> deliverOnMainQueue).start(next: { view in + guard let strongSelf = self, let view = view else { + return + } + + let entries = view.entries + + if strongSelf.invertItemOrder { + strongSelf.entries = entries.reversed() + strongSelf.hasLeftEntries = view.hasLater + strongSelf.hasRightEntries = view.hasEarlier + } else { + strongSelf.entries = entries + strongSelf.hasLeftEntries = view.hasEarlier + strongSelf.hasRightEntries = view.hasLater + } + if strongSelf.isViewLoaded { + var items: [GalleryItem] = [] + var centralItemIndex: Int? + for entry in strongSelf.entries { + var isCentral = false + if entry.message.stableId == strongSelf.centralEntryStableId { + isCentral = true + } + if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }) { + if isCentral { + centralItemIndex = items.count + } + items.append(item) + } + } + + strongSelf.galleryNode.pager.replaceItems(items, centralItemIndex: centralItemIndex) + } + })) + } + default: + break + } } if strongSelf.didSetReady { strongSelf._hiddenMedia.set(.single(hiddenItem)) diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index 31a920962c..5409b07a5b 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -11,7 +11,7 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture public let footerNode: GalleryFooterNode public var currentThumbnailContainerNode: GalleryThumbnailContainerNode? public var overlayNode: ASDisplayNode? - public var transitionDataForCentralItem: (() -> ((ASDisplayNode, () -> (UIView?, UIView?))?, (UIView) -> Void)?)? + public var transitionDataForCentralItem: (() -> ((ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, (UIView) -> Void)?)? public var dismiss: (() -> Void)? public var containerLayout: (CGFloat, ContainerViewLayout)? @@ -250,8 +250,8 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture self.updateThumbnailContainerNodeAlpha(transition) } - self.footerNode.updateLayout(layout, footerContentNode: self.presentationState.footerContentNode, thumbnailPanelHeight: thumbnailPanelHeight, transition: transition) - + self.footerNode.updateLayout(layout, footerContentNode: self.presentationState.footerContentNode, overlayContentNode: self.presentationState.overlayContentNode, thumbnailPanelHeight: thumbnailPanelHeight, transition: transition) + let previousContentHeight = self.scrollView.contentSize.height let previousVerticalOffset = self.scrollView.contentOffset.y @@ -276,14 +276,14 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture let alpha: CGFloat = self.areControlsHidden ? 0.0 : 1.0 self.navigationBar?.alpha = alpha self.statusBar?.updateAlpha(alpha, transition: .animated(duration: 0.3, curve: .easeInOut)) - self.footerNode.alpha = alpha + self.footerNode.setVisibilityAlpha(alpha) self.updateThumbnailContainerNodeAlpha(.immediate) }) } else { let alpha: CGFloat = self.areControlsHidden ? 0.0 : 1.0 self.navigationBar?.alpha = alpha self.statusBar?.updateAlpha(alpha, transition: .immediate) - self.footerNode.alpha = alpha + self.footerNode.setVisibilityAlpha(alpha) self.updateThumbnailContainerNodeAlpha(.immediate) } } diff --git a/submodules/GalleryUI/Sources/GalleryControllerPresentationState.swift b/submodules/GalleryUI/Sources/GalleryControllerPresentationState.swift index 7fd9620cb5..4881b7744e 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerPresentationState.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerPresentationState.swift @@ -2,16 +2,23 @@ import Foundation public final class GalleryControllerPresentationState { public let footerContentNode: GalleryFooterContentNode? + public let overlayContentNode: GalleryOverlayContentNode? public init() { self.footerContentNode = nil + self.overlayContentNode = nil } - public init(footerContentNode: GalleryFooterContentNode?) { + public init(footerContentNode: GalleryFooterContentNode?, overlayContentNode: GalleryOverlayContentNode?) { self.footerContentNode = footerContentNode + self.overlayContentNode = overlayContentNode } public func withUpdatedFooterContentNode(_ footerContentNode: GalleryFooterContentNode?) -> GalleryControllerPresentationState { - return GalleryControllerPresentationState(footerContentNode: footerContentNode) + return GalleryControllerPresentationState(footerContentNode: footerContentNode, overlayContentNode: self.overlayContentNode) + } + + public func withUpdatedOverlayContentNode(_ overlayContentNode: GalleryOverlayContentNode?) -> GalleryControllerPresentationState { + return GalleryControllerPresentationState(footerContentNode: self.footerContentNode, overlayContentNode: overlayContentNode) } } diff --git a/submodules/GalleryUI/Sources/GalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/GalleryFooterContentNode.swift index 9fe0dadf41..1d9126eec0 100644 --- a/submodules/GalleryUI/Sources/GalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/GalleryFooterContentNode.swift @@ -31,3 +31,20 @@ open class GalleryFooterContentNode: ASDisplayNode { completion() } } + +open class GalleryOverlayContentNode: ASDisplayNode { + var visibilityAlpha: CGFloat = 1.0 + open func setVisibilityAlpha(_ alpha: CGFloat) { + self.visibilityAlpha = alpha + } + + open func updateLayout(size: CGSize, metrics: LayoutMetrics, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + } + + open func animateIn(previousContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition) { + } + + open func animateOut(nextContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + completion() + } +} diff --git a/submodules/GalleryUI/Sources/GalleryFooterNode.swift b/submodules/GalleryUI/Sources/GalleryFooterNode.swift index 7537360dce..027ea8a682 100644 --- a/submodules/GalleryUI/Sources/GalleryFooterNode.swift +++ b/submodules/GalleryUI/Sources/GalleryFooterNode.swift @@ -7,6 +7,7 @@ public final class GalleryFooterNode: ASDisplayNode { private let backgroundNode: ASDisplayNode private var currentFooterContentNode: GalleryFooterContentNode? + private var currentOverlayContentNode: GalleryOverlayContentNode? private var currentLayout: (ContainerViewLayout, CGFloat)? private let controllerInteraction: GalleryControllerInteraction @@ -22,38 +23,59 @@ public final class GalleryFooterNode: ASDisplayNode { self.addSubnode(self.backgroundNode) } - public func updateLayout(_ layout: ContainerViewLayout, footerContentNode: GalleryFooterContentNode?, thumbnailPanelHeight: CGFloat, transition: ContainedViewLayoutTransition) { + private var visibilityAlpha: CGFloat = 1.0 + public func setVisibilityAlpha(_ alpha: CGFloat) { + self.visibilityAlpha = alpha + self.backgroundNode.alpha = alpha + self.currentFooterContentNode?.alpha = alpha + self.currentOverlayContentNode?.setVisibilityAlpha(alpha) + } + + public func updateLayout(_ layout: ContainerViewLayout, footerContentNode: GalleryFooterContentNode?, overlayContentNode: GalleryOverlayContentNode?, thumbnailPanelHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.currentLayout = (layout, thumbnailPanelHeight) let cleanInsets = layout.insets(options: []) - var removeCurrentFooterContentNode: GalleryFooterContentNode? + var dismissedCurrentFooterContentNode: GalleryFooterContentNode? if self.currentFooterContentNode !== footerContentNode { if let currentFooterContentNode = self.currentFooterContentNode { currentFooterContentNode.requestLayout = nil - removeCurrentFooterContentNode = currentFooterContentNode + dismissedCurrentFooterContentNode = currentFooterContentNode } self.currentFooterContentNode = footerContentNode if let footerContentNode = footerContentNode { + footerContentNode.alpha = self.visibilityAlpha footerContentNode.controllerInteraction = self.controllerInteraction footerContentNode.requestLayout = { [weak self] transition in if let strongSelf = self, let (currentLayout, currentThumbnailPanelHeight) = strongSelf.currentLayout { - strongSelf.updateLayout(currentLayout, footerContentNode: strongSelf.currentFooterContentNode, thumbnailPanelHeight: currentThumbnailPanelHeight, transition: transition) + strongSelf.updateLayout(currentLayout, footerContentNode: strongSelf.currentFooterContentNode, overlayContentNode: strongSelf.currentOverlayContentNode, thumbnailPanelHeight: currentThumbnailPanelHeight, transition: transition) } } self.addSubnode(footerContentNode) } } + var dismissedCurrentOverlayContentNode: GalleryOverlayContentNode? + if self.currentOverlayContentNode !== overlayContentNode { + if let currentOverlayContentNode = self.currentOverlayContentNode { + dismissedCurrentOverlayContentNode = currentOverlayContentNode + } + self.currentOverlayContentNode = overlayContentNode + if let overlayContentNode = overlayContentNode { + overlayContentNode.setVisibilityAlpha(self.visibilityAlpha) + self.addSubnode(overlayContentNode) + } + } + var backgroundHeight: CGFloat = 0.0 if let footerContentNode = self.currentFooterContentNode { backgroundHeight = footerContentNode.updateLayout(size: layout.size, metrics: layout.metrics, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, contentInset: thumbnailPanelHeight, transition: transition) transition.updateFrame(node: footerContentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight), size: CGSize(width: layout.size.width, height: backgroundHeight))) - if let removeCurrentFooterContentNode = removeCurrentFooterContentNode { + if let dismissedCurrentFooterContentNode = dismissedCurrentFooterContentNode { let contentTransition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) - footerContentNode.animateIn(fromHeight: removeCurrentFooterContentNode.bounds.height, previousContentNode: removeCurrentFooterContentNode, transition: contentTransition) - removeCurrentFooterContentNode.animateOut(toHeight: backgroundHeight, nextContentNode: footerContentNode, transition: contentTransition, completion: { [weak self, weak removeCurrentFooterContentNode] in - if let strongSelf = self, let removeCurrentFooterContentNode = removeCurrentFooterContentNode, removeCurrentFooterContentNode !== strongSelf.currentFooterContentNode { - removeCurrentFooterContentNode.removeFromSupernode() + footerContentNode.animateIn(fromHeight: dismissedCurrentFooterContentNode.bounds.height, previousContentNode: dismissedCurrentFooterContentNode, transition: contentTransition) + dismissedCurrentFooterContentNode.animateOut(toHeight: backgroundHeight, nextContentNode: footerContentNode, transition: contentTransition, completion: { [weak self, weak dismissedCurrentFooterContentNode] in + if let strongSelf = self, let dismissedCurrentFooterContentNode = dismissedCurrentFooterContentNode, dismissedCurrentFooterContentNode !== strongSelf.currentFooterContentNode { + dismissedCurrentFooterContentNode.removeFromSupernode() } }) contentTransition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight), size: CGSize(width: layout.size.width, height: backgroundHeight))) @@ -61,16 +83,42 @@ public final class GalleryFooterNode: ASDisplayNode { transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight), size: CGSize(width: layout.size.width, height: backgroundHeight))) } } else { - if let removeCurrentFooterContentNode = removeCurrentFooterContentNode { - removeCurrentFooterContentNode.removeFromSupernode() + if let dismissedCurrentFooterContentNode = dismissedCurrentFooterContentNode { + dismissedCurrentFooterContentNode.removeFromSupernode() } transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight), size: CGSize(width: layout.size.width, height: backgroundHeight))) } + + let contentTransition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + if let overlayContentNode = self.currentOverlayContentNode { + overlayContentNode.updateLayout(size: layout.size, metrics: layout.metrics, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: backgroundHeight, transition: transition) + transition.updateFrame(node: overlayContentNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + overlayContentNode.animateIn(previousContentNode: dismissedCurrentOverlayContentNode, transition: contentTransition) + if let dismissedCurrentOverlayContentNode = dismissedCurrentOverlayContentNode { + dismissedCurrentOverlayContentNode.animateOut(nextContentNode: overlayContentNode, transition: contentTransition, completion: { [weak self, weak dismissedCurrentOverlayContentNode] in + if let strongSelf = self, let dismissedCurrentOverlayContentNode = dismissedCurrentOverlayContentNode, dismissedCurrentOverlayContentNode !== strongSelf.currentOverlayContentNode { + dismissedCurrentOverlayContentNode.removeFromSupernode() + } + }) + } + } else { + if let dismissedCurrentOverlayContentNode = dismissedCurrentOverlayContentNode { + dismissedCurrentOverlayContentNode.animateOut(nextContentNode: overlayContentNode, transition: contentTransition, completion: { [weak self, weak dismissedCurrentOverlayContentNode] in + if let strongSelf = self, let dismissedCurrentOverlayContentNode = dismissedCurrentOverlayContentNode, dismissedCurrentOverlayContentNode !== strongSelf.currentOverlayContentNode { + dismissedCurrentOverlayContentNode.removeFromSupernode() + } + }) + } + } } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.backgroundNode.frame.contains(point) { + if let overlayResult = self.currentOverlayContentNode?.hitTest(point, with: event) { + return overlayResult + } + if !self.backgroundNode.frame.contains(point) || self.visibilityAlpha < 1.0 { return nil } let result = super.hitTest(point, with: event) diff --git a/submodules/GalleryUI/Sources/GalleryItem.swift b/submodules/GalleryUI/Sources/GalleryItem.swift index 89cfa918e5..c0b41d8194 100644 --- a/submodules/GalleryUI/Sources/GalleryItem.swift +++ b/submodules/GalleryUI/Sources/GalleryItem.swift @@ -21,6 +21,8 @@ public struct GalleryItemIndexData: Equatable { } public protocol GalleryItem { + var id: AnyHashable { get } + func node() -> GalleryItemNode func updateNode(node: GalleryItemNode) func thumbnailItem() -> (Int64, GalleryThumbnailItem)? diff --git a/submodules/GalleryUI/Sources/GalleryItemNode.swift b/submodules/GalleryUI/Sources/GalleryItemNode.swift index 7a91e6988a..b2c513654f 100644 --- a/submodules/GalleryUI/Sources/GalleryItemNode.swift +++ b/submodules/GalleryUI/Sources/GalleryItemNode.swift @@ -21,6 +21,10 @@ open class GalleryItemNode: ASDisplayNode { } public var toggleControlsVisibility: () -> Void = { } + public var goToPreviousItem: () -> Void = { } + public var goToNextItem: () -> Void = { } + public var canGoToPreviousItem: () -> Bool = { return false } + public var canGoToNextItem: () -> Bool = { return false } public var dismiss: () -> Void = { } public var beginCustomDismiss: () -> Void = { } public var completeCustomDismiss: () -> Void = { } @@ -54,8 +58,8 @@ open class GalleryItemNode: ASDisplayNode { return .single(nil) } - open func footerContent() -> Signal { - return .single(nil) + open func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { + return .single((nil, nil)) } open func navigationStyle() -> Signal { @@ -80,10 +84,10 @@ open class GalleryItemNode: ASDisplayNode { open func visibilityUpdated(isVisible: Bool) { } - open func animateIn(from node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { + open func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { } - open func animateOut(to node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + open func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { } open func contentSize() -> CGSize? { diff --git a/submodules/GalleryUI/Sources/GalleryPagerNode.swift b/submodules/GalleryUI/Sources/GalleryPagerNode.swift index fea7e407af..d0bba72d76 100644 --- a/submodules/GalleryUI/Sources/GalleryPagerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryPagerNode.swift @@ -152,16 +152,27 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { } public func replaceItems(_ items: [GalleryItem], centralItemIndex: Int?, keepFirst: Bool = false) { + var items = items + if keepFirst && !self.items.isEmpty && !items.isEmpty { + items[0] = self.items[0] + } + var updateItems: [GalleryPagerUpdateItem] = [] - let deleteItems: [Int] = [] + var deleteItems: [Int] = [] var insertItems: [GalleryPagerInsertItem] = [] - for i in 0 ..< items.count { - if i == 0 && keepFirst { - updateItems.append(GalleryPagerUpdateItem(index: 0, previousIndex: 0, item: items[i])) - } else { - insertItems.append(GalleryPagerInsertItem(index: i, item: items[i], previousIndex: nil)) + var previousIndexById: [AnyHashable: Int] = [:] + var validIds = Set(items.map { $0.id }) + + for i in 0 ..< self.items.count { + previousIndexById[self.items[i].id] = i + if !validIds.contains(self.items[i].id) { + deleteItems.append(i) } } + + for i in 0 ..< items.count { + insertItems.append(GalleryPagerInsertItem(index: i, item: items[i], previousIndex: previousIndexById[items[i].id])) + } self.transaction(GalleryPagerTransaction(deleteItems: deleteItems, insertItems: insertItems, updateItems: updateItems, focusOnItem: centralItemIndex)) } @@ -169,6 +180,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { for updatedItem in transaction.updateItems { self.items[updatedItem.previousIndex] = updatedItem.item if let itemNode = self.visibleItemNode(at: updatedItem.previousIndex) { + //print("update visible node at \(updatedItem.previousIndex)") updatedItem.item.updateNode(node: itemNode) } } @@ -180,55 +192,52 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { self.items.remove(at: deleteItemIndex) for i in 0 ..< self.itemNodes.count { if self.itemNodes[i].index == deleteItemIndex { + //print("delete visible node at \(deleteItemIndex)") self.removeVisibleItemNode(internalIndex: i) break } } } - for itemNode in self.itemNodes { - var indexOffset = 0 - for deleteIndex in deleteItems { - if deleteIndex < itemNode.index { - indexOffset += 1 - } else { - break - } - } - - itemNode.index = itemNode.index - indexOffset - } - let insertItems = transaction.insertItems.sorted(by: { $0.index < $1.index }) - if self.items.count == 0 && !insertItems.isEmpty { - if insertItems[0].index != 0 { - fatalError("transaction: invalid insert into empty list") - } + + if transaction.updateItems.isEmpty && !insertItems.isEmpty { + self.items.removeAll() } for insertedItem in insertItems { - self.items.insert(insertedItem.item, at: insertedItem.index) + self.items.append(insertedItem.item) + //self.items.insert(insertedItem.item, at: insertedItem.index) } - let sortedInsertItems = transaction.insertItems.sorted(by: { $0.index < $1.index }) + let visibleIndices: [Int] = self.itemNodes.map { $0.index } + + var remapIndices: [Int: Int] = [:] + for i in 0 ..< insertItems.count { + if let previousIndex = insertItems[i].previousIndex, visibleIndices.contains(previousIndex) { + remapIndices[previousIndex] = i + } + } for itemNode in self.itemNodes { - var indexOffset = 0 - for insertedItem in sortedInsertItems { - if insertedItem.index <= itemNode.index + indexOffset { - indexOffset += 1 - } + if let remappedIndex = remapIndices[itemNode.index] { + //print("remap visible node \(itemNode.index) -> \(remappedIndex)") + itemNode.index = remappedIndex } - - itemNode.index = itemNode.index + indexOffset } + self.itemNodes.sort(by: { $0.index < $1.index }) + + //print("visible indices before update \(self.itemNodes.map { $0.index })") + self.invalidatedItems = true if let focusOnItem = transaction.focusOnItem { self.centralItemIndex = focusOnItem } self.updateItemNodes(transition: .immediate) + + //print("visible indices after update \(self.itemNodes.map { $0.index })") } else if let focusOnItem = transaction.focusOnItem { self.ignoreCentralItemIndexUpdate = true @@ -241,6 +250,32 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { private func makeNodeForItem(at index: Int) -> GalleryItemNode { let node = self.items[index].node() node.toggleControlsVisibility = self.toggleControlsVisibility + node.goToPreviousItem = { [weak self] in + if let strongSelf = self { + if let index = strongSelf.centralItemIndex, index > 0 { + strongSelf.transaction(GalleryPagerTransaction(deleteItems: [], insertItems: [], updateItems: [], focusOnItem: index - 1)) + } + } + } + node.goToNextItem = { [weak self] in + if let strongSelf = self { + if let index = strongSelf.centralItemIndex, index < strongSelf.items.count - 1 { + strongSelf.transaction(GalleryPagerTransaction(deleteItems: [], insertItems: [], updateItems: [], focusOnItem: index + 1)) + } + } + } + node.canGoToPreviousItem = { [weak self] in + if let strongSelf = self, let index = strongSelf.centralItemIndex, index > 0 { + return true + } + return false + } + node.canGoToNextItem = { [weak self] in + if let strongSelf = self, let index = strongSelf.centralItemIndex, index < strongSelf.items.count - 1 { + return true + } + return false + } node.dismiss = self.dismiss node.beginCustomDismiss = self.beginCustomDismiss node.completeCustomDismiss = self.completeCustomDismiss @@ -314,7 +349,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { } } - if centralItemIndex != items.count - 1 { + if centralItemIndex != self.items.count - 1 { if self.shouldLoadItems(force: forceLoad) && self.visibleItemNode(at: centralItemIndex + 1) == nil { let node = self.makeNodeForItem(at: centralItemIndex + 1) node.frame = centralItemNode.frame.offsetBy(dx: centralItemNode.frame.size.width + self.pageGap, dy: 0.0) diff --git a/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift index b7b82d1d3d..6c5288df31 100644 --- a/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift @@ -15,6 +15,10 @@ import StickerResources import AppBundle class ChatAnimationGalleryItem: GalleryItem { + var id: AnyHashable { + return self.message.stableId + } + let context: AccountContext let presentationData: PresentationData let message: Message @@ -238,7 +242,7 @@ final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode { })) } - override func animateIn(from node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { + override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view) let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view.superview) @@ -254,7 +258,7 @@ final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode { self.statusNodeContainer.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) } - override func animateOut(to node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view) let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view.superview) let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) @@ -264,7 +268,7 @@ final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode { var boundsCompleted = false var copyCompleted = false - let (maybeCopyView, copyViewBackgrond) = node.1() + let (maybeCopyView, copyViewBackgrond) = node.2() copyViewBackgrond?.alpha = 0.0 let copyView = maybeCopyView! @@ -325,8 +329,8 @@ final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode { return self._rightBarButtonItems.get() } - override func footerContent() -> Signal { - return .single(self.footerContentNode) + override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { + return .single((self.footerContentNode, nil)) } @objc func statusPressed() { diff --git a/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift index 2bc400d803..cb04f53db0 100644 --- a/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift @@ -12,6 +12,10 @@ import AccountContext import RadialStatusNode class ChatDocumentGalleryItem: GalleryItem { + var id: AnyHashable { + return self.message.stableId + } + let context: AccountContext let presentationData: PresentationData let message: Message @@ -307,7 +311,7 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD return self._title.get() } - override func animateIn(from node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { + override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.webView) let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.webView.superview) @@ -323,7 +327,7 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD self.statusNodeContainer.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) } - override func animateOut(to node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.webView) let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.webView.superview) let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) @@ -333,7 +337,7 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD var boundsCompleted = false var copyCompleted = false - let (maybeCopyView, copyViewBackgrond) = node.1() + let (maybeCopyView, copyViewBackgrond) = node.2() copyViewBackgrond?.alpha = 0.0 let copyView = maybeCopyView! @@ -375,8 +379,8 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD self.statusNodeContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false) } - override func footerContent() -> Signal { - return .single(self.footerContentNode) + override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { + return .single((self.footerContentNode, nil)) } @objc func statusPressed() { diff --git a/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift index 78099a5279..bfcdf0fb42 100644 --- a/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift @@ -13,6 +13,10 @@ import RadialStatusNode import ShareController class ChatExternalFileGalleryItem: GalleryItem { + var id: AnyHashable { + return self.message.stableId + } + let context: AccountContext let presentationData: PresentationData let message: Message @@ -242,7 +246,7 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode { return self._title.get() } - override func animateIn(from node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { + override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view) let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view.superview) @@ -258,7 +262,7 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode { self.statusNodeContainer.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) } - override func animateOut(to node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view) let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view.superview) let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) @@ -268,7 +272,7 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode { var boundsCompleted = false var copyCompleted = false - let (maybeCopyView, copyViewBackgrond) = node.1() + let (maybeCopyView, copyViewBackgrond) = node.2() copyViewBackgrond?.alpha = 0.0 let copyView = maybeCopyView! @@ -310,8 +314,8 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode { self.statusNodeContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false) } - override func footerContent() -> Signal { - return .single(self.footerContentNode) + override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { + return .single((self.footerContentNode, nil)) } @objc func statusPressed() { diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 25c7ca919c..23c5b815cb 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -10,6 +10,10 @@ import TelegramPresentationData import AccountContext import RadialStatusNode import PhotoResources +import AppBundle +import StickerPackPreviewUI +import OverlayStatusController +import PresentationDataUtils enum ChatMediaGalleryThumbnail: Equatable { case image(ImageMediaReference) @@ -75,6 +79,10 @@ final class ChatMediaGalleryThumbnailItem: GalleryThumbnailItem { } class ChatImageGalleryItem: GalleryItem { + var id: AnyHashable { + return self.message.stableId + } + let context: AccountContext let presentationData: PresentationData let message: Message @@ -156,6 +164,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { private let imageNode: TransformImageNode fileprivate let _ready = Promise() fileprivate let _title = Promise() + fileprivate let _rightBarButtonItem = Promise() private let statusNodeContainer: HighlightableButtonNode private let statusNode: RadialStatusNode private let footerContentNode: ChatItemGalleryFooterContentNode @@ -230,10 +239,53 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } else { self._ready.set(.single(Void())) } + if imageReference.media.flags.contains(.hasStickers) { + let rightBarButtonItem = UIBarButtonItem(image: UIImage(bundleImageName: "Media Gallery/Stickers"), style: .plain, target: self, action: #selector(self.openStickersButtonPressed)) + self._rightBarButtonItem.set(.single(rightBarButtonItem)) + } } self.contextAndMedia = (self.context, imageReference.abstract) } + @objc func openStickersButtonPressed() { + guard let (context, media) = self.contextAndMedia else { + return + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let progressSignal = Signal { [weak self] subscriber in + guard let strongSelf = self else { + return EmptyDisposable + } + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + (strongSelf.baseNavigationController()?.topViewController as? ViewController)?.present(controller, in: .window(.root), with: 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 signal = stickerPacksAttachedToMedia(account: context.account, media: media) + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + let _ = (signal + |> deliverOnMainQueue).start(next: { [weak self] packs in + guard let strongSelf = self, !packs.isEmpty else { + return + } + let baseNavigationController = strongSelf.baseNavigationController() + baseNavigationController?.view.endEditing(true) + let controller = StickerPackScreen(context: context, mainStickerPack: packs[0], stickerPacks: packs, sendSticker: nil) + (baseNavigationController?.topViewController as? ViewController)?.present(controller, in: .window(.root), with: nil) + }) + } + func setFile(context: AccountContext, fileReference: FileMediaReference) { if self.contextAndMedia == nil || !self.contextAndMedia!.1.media.isEqual(to: fileReference.media) { if var largestSize = fileReference.media.dimensions { @@ -302,14 +354,20 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { })) } - override func animateIn(from node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { + override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view) let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view.superview) let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) - let transformedCopyViewFinalFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view) - let (maybeSurfaceCopyView, _) = node.1() - let (maybeCopyView, copyViewBackgrond) = node.1() + /*let projectedScale = CGPoint(x: self.imageNode.view.bounds.width / node.1.width, y: self.imageNode.view.bounds.height / node.1.height) + let scaledLocalImageViewBounds = CGRect(x: -node.1.minX * projectedScale.x, y: -node.1.minY * projectedScale.y, width: node.0.bounds.width * projectedScale.x, height: node.0.bounds.height * projectedScale.y)*/ + + let scaledLocalImageViewBounds = self.imageNode.view.bounds + + let transformedCopyViewFinalFrame = self.imageNode.view.convert(scaledLocalImageViewBounds, to: self.view) + + let (maybeSurfaceCopyView, _) = node.2() + let (maybeCopyView, copyViewBackgrond) = node.2() copyViewBackgrond?.alpha = 0.0 let surfaceCopyView = maybeSurfaceCopyView! let copyView = maybeCopyView! @@ -320,7 +378,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { var transformedSurfaceFinalFrame: CGRect? if let contentSurface = surfaceCopyView.superview { transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface) - transformedSurfaceFinalFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: contentSurface) + transformedSurfaceFinalFrame = self.imageNode.view.convert(scaledLocalImageViewBounds, to: contentSurface) } if let transformedSurfaceFrame = transformedSurfaceFrame { @@ -330,7 +388,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame - copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false) + copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) surfaceCopyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) @@ -361,7 +419,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self.statusNodeContainer.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) } - override func animateOut(to node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { self.fetchDisposable.set(nil) var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view) @@ -373,8 +431,8 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { var boundsCompleted = false var copyCompleted = false - let (maybeSurfaceCopyView, _) = node.1() - let (maybeCopyView, copyViewBackgrond) = node.1() + let (maybeSurfaceCopyView, _) = node.2() + let (maybeCopyView, copyViewBackgrond) = node.2() copyViewBackgrond?.alpha = 0.0 let surfaceCopyView = maybeSurfaceCopyView! let copyView = maybeCopyView! @@ -447,8 +505,12 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { return self._title.get() } - override func footerContent() -> Signal { - return .single(self.footerContentNode) + override func rightBarButtonItem() -> Signal { + return self._rightBarButtonItem.get() + } + + override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { + return .single((self.footerContentNode, nil)) } @objc func statusPressed() { diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 253455e154..ba3a583887 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -19,6 +19,10 @@ public enum UniversalVideoGalleryItemContentInfo { } public class UniversalVideoGalleryItem: GalleryItem { + public var id: AnyHashable { + return self.content.id + } + let context: AccountContext let presentationData: PresentationData let content: UniversalVideoContent @@ -145,6 +149,87 @@ private final class UniversalVideoGalleryItemPictureInPictureNode: ASDisplayNode } } +private let soundOnImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/SoundOn"), color: .white) +private let soundOffImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/SoundOff"), color: .white) +private var roundButtonBackgroundImage = { + return generateImage(CGSize(width: 42.0, height: 42), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + context.setFillColor(UIColor(white: 0.0, alpha: 0.5).cgColor) + context.fillEllipse(in: bounds) + }) +}() + +private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentNode { + private let soundButtonNode: HighlightableButtonNode + private var validLayout: (CGSize, LayoutMetrics, CGFloat, CGFloat, CGFloat)? + + override init() { + self.soundButtonNode = HighlightableButtonNode() + self.soundButtonNode.alpha = 0.0 + self.soundButtonNode.setBackgroundImage(roundButtonBackgroundImage, for: .normal) + self.soundButtonNode.setImage(soundOffImage, for: .normal) + self.soundButtonNode.setImage(soundOnImage, for: .selected) + self.soundButtonNode.setImage(soundOnImage, for: [.selected, .highlighted]) + + super.init() + + self.soundButtonNode.addTarget(self, action: #selector(self.soundButtonPressed), forControlEvents: .touchUpInside) + self.addSubnode(self.soundButtonNode) + } + + func hide() { + self.soundButtonNode.isHidden = true + } + + override func updateLayout(size: CGSize, metrics: LayoutMetrics, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, metrics, leftInset, rightInset, bottomInset) + + let soundButtonDiameter: CGFloat = 42.0 + let inset: CGFloat = 12.0 + let effectiveBottomInset = self.visibilityAlpha < 1.0 ? 0.0 : bottomInset + let soundButtonFrame = CGRect(origin: CGPoint(x: size.width - soundButtonDiameter - inset - rightInset, y: size.height - soundButtonDiameter - inset - effectiveBottomInset), size: CGSize(width: soundButtonDiameter, height: soundButtonDiameter)) + transition.updateFrame(node: self.soundButtonNode, frame: soundButtonFrame) + } + + override func animateIn(previousContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition) { + transition.updateAlpha(node: self.soundButtonNode, alpha: 1.0) + } + + override func animateOut(nextContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + transition.updateAlpha(node: self.soundButtonNode, alpha: 0.0) + } + + override func setVisibilityAlpha(_ alpha: CGFloat) { + super.setVisibilityAlpha(alpha) + self.updateSoundButtonVisibility() + } + + func updateSoundButtonVisibility() { + if self.soundButtonNode.isSelected { + self.soundButtonNode.alpha = self.visibilityAlpha + } else { + self.soundButtonNode.alpha = 1.0 + } + + if let validLayout = self.validLayout { + self.updateLayout(size: validLayout.0, metrics: validLayout.1, leftInset: validLayout.2, rightInset: validLayout.3, bottomInset: validLayout.4, transition: .animated(duration: 0.3, curve: .easeInOut)) + } + } + + @objc func soundButtonPressed() { + self.soundButtonNode.isSelected = !self.soundButtonNode.isSelected + self.updateSoundButtonVisibility() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.soundButtonNode.frame.contains(point) { + return nil + } + return super.hitTest(point, with: event) + } +} + private struct FetchControls { let fetch: () -> Void let cancel: () -> Void @@ -161,6 +246,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let scrubberView: ChatVideoGalleryItemScrubberView private let footerContentNode: ChatItemGalleryFooterContentNode + private let overlayContentNode: UniversalVideoGalleryItemOverlayNode private var videoNode: UniversalVideoNode? private var videoFramePreview: MediaPlayerFramePreview? @@ -173,6 +259,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var _isVisible: Bool? private var initiallyActivated = false private var hideStatusNodeUntilCentrality = false + private var playOnContentOwnership = false + private var skipInitialPause = false private var validLayout: (ContainerViewLayout, CGFloat)? private var didPause = false private var isPaused = true @@ -206,6 +294,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.footerContentNode.performAction = performAction self.footerContentNode.openActionOptions = openActionOptions + self.overlayContentNode = UniversalVideoGalleryItemOverlayNode() + self.statusButtonNode = HighlightableButtonNode() self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) self.statusNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0)) @@ -238,7 +328,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } self.statusButtonNode.addSubnode(self.statusNode) - self.statusButtonNode.addTarget(self, action: #selector(statusButtonPressed), forControlEvents: .touchUpInside) + self.statusButtonNode.addTarget(self, action: #selector(self.statusButtonPressed), forControlEvents: .touchUpInside) self.addSubnode(self.statusButtonNode) @@ -391,6 +481,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { videoNode.ownsContentNodeUpdated = { [weak self] value in if let strongSelf = self { strongSelf.updateDisplayPlaceholder(!value) + + if strongSelf.playOnContentOwnership { + strongSelf.playOnContentOwnership = false + strongSelf.initiallyActivated = true + strongSelf.skipInitialPause = true + strongSelf.videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: .stop) + } } } self.videoNode = videoNode @@ -398,6 +495,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { videoNode.backgroundColor = videoNode.ownsContentNode ? UIColor.black : UIColor(rgb: 0x333335) if item.fromPlayingVideo { videoNode.canAttachContent = false + self.overlayContentNode.hide() } else { self.updateDisplayPlaceholder(!videoNode.ownsContentNode) } @@ -420,11 +518,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } else { let throttledSignal = videoNode.status |> mapToThrottled { next -> Signal in - return .single(next) |> then(.complete() |> delay(4.0, queue: Queue.concurrentDefaultQueue())) + return .single(next) |> then(.complete() |> delay(2.0, queue: Queue.concurrentDefaultQueue())) } self.mediaPlaybackStateDisposable.set(throttledSignal.start(next: { status in - if let status = status, status.duration > 60.0 * 20.0 { + if let status = status, status.duration >= 60.0 * 20.0 { var timestamp: Double? if status.timestamp > 5.0 && status.timestamp < status.duration - 5.0 { timestamp = status.timestamp @@ -625,7 +723,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } private func shouldAutoplayOnCentrality() -> Bool { - if let item = self.item, let content = item.content as? NativeVideoContent, !self.initiallyActivated { +// !self.initiallyActivated + if let item = self.item, let content = item.content as? NativeVideoContent { var isLocal = false if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { isLocal = true @@ -658,15 +757,19 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.hideStatusNodeUntilCentrality = false self.statusButtonNode.isHidden = self.hideStatusNodeUntilCentrality || self.statusNodeShouldBeHidden + if videoNode.ownsContentNode { if isAnimated { videoNode.seek(0.0) videoNode.play() - } - else if self.shouldAutoplayOnCentrality() { + } else if self.shouldAutoplayOnCentrality() { self.initiallyActivated = true videoNode.playOnceWithSound(playAndRecord: false, actionAtEnd: .stop) } + } else { + if self.shouldAutoplayOnCentrality() { + self.playOnContentOwnership = true + } } } else { self.dismissOnOrientationChange = false @@ -689,8 +792,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if hadPreviousValue { videoNode.canAttachContent = isVisible if isVisible { - videoNode.pause() - videoNode.seek(0.0) + if self.skipInitialPause { + self.skipInitialPause = false + } else { + videoNode.pause() + videoNode.seek(0.0) + } } else { videoNode.continuePlayingWithoutSound() } @@ -747,7 +854,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - override func animateIn(from node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { + override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { guard let videoNode = self.videoNode else { return } @@ -773,8 +880,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) let transformedCopyViewFinalFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) - let (maybeSurfaceCopyView, _) = node.1() - let (maybeCopyView, copyViewBackgrond) = node.1() + let (maybeSurfaceCopyView, _) = node.2() + let (maybeCopyView, copyViewBackgrond) = node.2() copyViewBackgrond?.alpha = 0.0 let surfaceCopyView = maybeSurfaceCopyView! let copyView = maybeCopyView! @@ -818,7 +925,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false) if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedSurfaceFinalFrame = transformedSurfaceFinalFrame { - surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak surfaceCopyView] _ in + surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), to: CGPoint(x: transformedSurfaceFinalFrame.midX, y: transformedSurfaceFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak surfaceCopyView] _ in surfaceCopyView?.removeFromSuperview() }) let scale = CGSize(width: transformedSurfaceFinalFrame.size.width / transformedSurfaceFrame.size.width, height: transformedSurfaceFinalFrame.size.height / transformedSurfaceFrame.size.height) @@ -859,7 +966,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - override func animateOut(to node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { guard let videoNode = self.videoNode else { completion() return @@ -875,8 +982,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var boundsCompleted = true var copyCompleted = false - let (maybeSurfaceCopyView, _) = node.1() - let (maybeCopyView, copyViewBackgrond) = node.1() + let (maybeSurfaceCopyView, _) = node.2() + let (maybeCopyView, copyViewBackgrond) = node.2() copyViewBackgrond?.alpha = 0.0 let surfaceCopyView = maybeSurfaceCopyView! let copyView = maybeCopyView! @@ -982,7 +1089,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { transformedSuperFrame = transformedSuperFrame.offsetBy(dx: videoNode.position.x - previousFrame.center.x, dy: videoNode.position.y - previousFrame.center.y) } - let initialScale: CGFloat = 1.0 //min(videoNode.layer.bounds.width / node.0.view.bounds.width, videoNode.layer.bounds.height / node.0.view.bounds.height) + let initialScale: CGFloat = 1.0 let targetScale = max(transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height) videoNode.backgroundColor = .clear @@ -1169,14 +1276,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { (baseNavigationController?.topViewController as? ViewController)?.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { id, media in if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode { - return GalleryTransitionArguments(transitionNode: (overlayNode, { [weak overlayNode] in + return GalleryTransitionArguments(transitionNode: (overlayNode, overlayNode.bounds, { [weak overlayNode] in return (overlayNode?.view.snapshotContentTree(), nil) }), addToTransitionSurface: { [weak overlaySupernode, weak overlayNode] view in overlaySupernode?.view.addSubview(view) overlayNode?.canAttachContent = false }) } else if let info = context.sharedContext.mediaManager.galleryHiddenMediaManager.findTarget(messageId: id, media: media) { - return GalleryTransitionArguments(transitionNode: (info.1, { + return GalleryTransitionArguments(transitionNode: (info.1, info.1.bounds, { return info.2() }), addToTransitionSurface: info.0) } @@ -1197,7 +1304,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - override func footerContent() -> Signal { - return .single(self.footerContentNode) + override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { + return .single((self.footerContentNode, nil)) } } diff --git a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift index 4a2df1e638..3c36aeaa18 100644 --- a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift +++ b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift @@ -122,6 +122,7 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode { public final class SecretMediaPreviewController: ViewController { private let context: AccountContext + private let messageId: MessageId private let _ready = Promise() override public var ready: Promise { @@ -150,6 +151,7 @@ public final class SecretMediaPreviewController: ViewController { public init(context: AccountContext, messageId: MessageId) { self.context = context + self.messageId = messageId self.presentationData = context.sharedContext.currentPresentationData.with { $0 } super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: GalleryController.darkNavigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) @@ -159,8 +161,6 @@ public final class SecretMediaPreviewController: ViewController { self.statusBar.statusBarStyle = .White - - self.disposable.set((context.account.postbox.messageView(messageId) |> deliverOnMainQueue).start(next: { [weak self] view in if let strongSelf = self { strongSelf.messageView = view @@ -178,17 +178,6 @@ public final class SecretMediaPreviewController: ViewController { return nil } }) - - self.screenCaptureEventsDisposable = (screenCaptureEvents() - |> deliverOnMainQueue).start(next: { [weak self] _ in - if let strongSelf = self, strongSelf.traceVisibility() { - if messageId.peerId.namespace == Namespaces.Peer.CloudUser { - let _ = enqueueMessages(account: context.account, peerId: messageId.peerId, messages: [.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.historyScreenshot)), replyToMessageId: nil, localGroupingKey: nil)]).start() - } else if messageId.peerId.namespace == Namespaces.Peer.SecretChat { - let _ = addSecretChatMessageScreenshot(account: context.account, peerId: messageId.peerId).start() - } - } - }) } required public init(coder aDecoder: NSCoder) { @@ -348,6 +337,19 @@ public final class SecretMediaPreviewController: ViewController { override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + if self.screenCaptureEventsDisposable == nil { + self.screenCaptureEventsDisposable = (screenCaptureEvents() + |> deliverOnMainQueue).start(next: { [weak self] _ in + if let strongSelf = self, strongSelf.traceVisibility() { + if strongSelf.messageId.peerId.namespace == Namespaces.Peer.CloudUser { + let _ = enqueueMessages(account: strongSelf.context.account, peerId: strongSelf.messageId.peerId, messages: [.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.historyScreenshot)), replyToMessageId: nil, localGroupingKey: nil)]).start() + } else if strongSelf.messageId.peerId.namespace == Namespaces.Peer.SecretChat { + let _ = addSecretChatMessageScreenshot(account: strongSelf.context.account, peerId: strongSelf.messageId.peerId).start() + } + } + }) + } + var nodeAnimatesItself = false if let centralItemNode = self.controllerNode.pager.centralItemNode(), let message = self.messageView?.message { diff --git a/submodules/GalleryUI/Sources/ZoomableContentGalleryItemNode.swift b/submodules/GalleryUI/Sources/ZoomableContentGalleryItemNode.swift index 7c7851232a..f65769e863 100644 --- a/submodules/GalleryUI/Sources/ZoomableContentGalleryItemNode.swift +++ b/submodules/GalleryUI/Sources/ZoomableContentGalleryItemNode.swift @@ -3,8 +3,36 @@ import UIKit import Display import AsyncDisplayKit +private let leftFadeImage = generateImage(CGSize(width: 64.0, height: 1.0), opaque: false, rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let gradientColors = [UIColor.black.withAlphaComponent(0.35).cgColor, UIColor.black.withAlphaComponent(0.0).cgColor] as CFArray + + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 64.0, y: 0.0), options: CGGradientDrawingOptions()) +}) + +private let rightFadeImage = generateImage(CGSize(width: 64.0, height: 1.0), opaque: false, rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.35).cgColor] as CFArray + + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 64.0, y: 0.0), options: CGGradientDrawingOptions()) +}) + open class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { public let scrollNode: ASScrollNode + private let leftFadeNode: ASImageNode + private let rightFadeNode: ASImageNode private var containerLayout: ContainerViewLayout? @@ -32,6 +60,16 @@ open class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate self.scrollNode.view.contentInsetAdjustmentBehavior = .never } + self.leftFadeNode = ASImageNode() + self.leftFadeNode.contentMode = .scaleToFill + self.leftFadeNode.image = leftFadeImage + self.leftFadeNode.alpha = 0.0 + + self.rightFadeNode = ASImageNode() + self.rightFadeNode.contentMode = .scaleToFill + self.rightFadeNode.image = rightFadeImage + self.rightFadeNode.alpha = 0.0 + super.init() self.scrollNode.view.delegate = self @@ -41,42 +79,75 @@ open class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate self.scrollNode.view.scrollsToTop = false self.scrollNode.view.delaysContentTouches = false + let edgeWidth: CGFloat = 44.0 + let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.contentTap(_:))) - tapRecognizer.tapActionAtPoint = { _ in + tapRecognizer.tapActionAtPoint = { [weak self] location in + if let strongSelf = self { + let pointInNode = strongSelf.scrollNode.view.convert(location, to: strongSelf.view) + if pointInNode.x < edgeWidth || pointInNode.x > strongSelf.frame.width - edgeWidth { + return .waitForSingleTap + } + } return .waitForDoubleTap } + tapRecognizer.highlight = { [weak self] location in + if let strongSelf = self { + let pointInNode = location.flatMap { strongSelf.scrollNode.view.convert($0, to: strongSelf.view) } + let transition: ContainedViewLayoutTransition = .animated(duration: 0.07, curve: .easeInOut) + if let location = pointInNode, location.x < edgeWidth && strongSelf.canGoToPreviousItem() { + transition.updateAlpha(node: strongSelf.leftFadeNode, alpha: 1.0) + } else { + transition.updateAlpha(node: strongSelf.leftFadeNode, alpha: 0.0) + } + if let location = pointInNode, location.x > strongSelf.frame.width - edgeWidth && strongSelf.canGoToNextItem() { + transition.updateAlpha(node: strongSelf.rightFadeNode, alpha: 1.0) + } else { + transition.updateAlpha(node: strongSelf.rightFadeNode, alpha: 0.0) + } + } + } self.scrollNode.view.addGestureRecognizer(tapRecognizer) - + self.addSubnode(self.scrollNode) + self.addSubnode(self.leftFadeNode) + self.addSubnode(self.rightFadeNode) } @objc open func contentTap(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { if recognizer.state == .ended { if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { - switch gesture { - case .tap: - self.toggleControlsVisibility() - case .doubleTap: - if let contentView = self.zoomableContent?.1.view, self.scrollNode.view.zoomScale.isLessThanOrEqualTo(self.scrollNode.view.minimumZoomScale) { - let pointInView = self.scrollNode.view.convert(location, to: contentView) - - let newZoomScale = self.scrollNode.view.maximumZoomScale - let scrollViewSize = self.scrollNode.view.bounds.size - - let w = scrollViewSize.width / newZoomScale - let h = scrollViewSize.height / newZoomScale - let x = pointInView.x - (w / 2.0) - let y = pointInView.y - (h / 2.0) - - let rectToZoomTo = CGRect(x: x, y: y, width: w, height: h) - - self.scrollNode.view.zoom(to: rectToZoomTo, animated: true) - } else { - self.scrollNode.view.setZoomScale(self.scrollNode.view.minimumZoomScale, animated: true) - } - default: - break + let pointInNode = self.scrollNode.view.convert(location, to: self.view) + if pointInNode.x < 44.0 { + self.goToPreviousItem() + } else if pointInNode.x > self.frame.width - 44.0 { + self.goToNextItem() + } else { + switch gesture { + case .tap: + self.toggleControlsVisibility() + case .doubleTap: + if let contentView = self.zoomableContent?.1.view, self.scrollNode.view.zoomScale.isLessThanOrEqualTo(self.scrollNode.view.minimumZoomScale) { + let pointInView = self.scrollNode.view.convert(location, to: contentView) + + let newZoomScale = self.scrollNode.view.maximumZoomScale + let scrollViewSize = self.scrollNode.view.bounds.size + + let w = scrollViewSize.width / newZoomScale + let h = scrollViewSize.height / newZoomScale + let x = pointInView.x - (w / 2.0) + let y = pointInView.y - (h / 2.0) + + let rectToZoomTo = CGRect(x: x, y: y, width: w, height: h) + + self.scrollNode.view.zoom(to: rectToZoomTo, animated: true) + } else { + self.scrollNode.view.setZoomScale(self.scrollNode.view.minimumZoomScale, animated: true) + } + default: + break + } } } } @@ -93,6 +164,10 @@ open class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate } self.containerLayout = layout + let fadeWidth = min(72.0, layout.size.width * 0.2) + self.leftFadeNode.frame = CGRect(x: 0.0, y: 0.0, width: fadeWidth, height: layout.size.height) + self.rightFadeNode.frame = CGRect(x: layout.size.width - fadeWidth, y: 0.0, width: fadeWidth, height: layout.size.height) + if shouldResetContents { var previousFrame: CGRect? var previousScale: CGFloat? diff --git a/submodules/Geocoding/Sources/Geocoding.swift b/submodules/Geocoding/Sources/Geocoding.swift index 967fe6169a..a391e26b3c 100644 --- a/submodules/Geocoding/Sources/Geocoding.swift +++ b/submodules/Geocoding/Sources/Geocoding.swift @@ -2,6 +2,19 @@ import Foundation import CoreLocation import SwiftSignalKit +public func geocodeLocation(address: String) -> Signal<[CLPlacemark]?, NoError> { + return Signal { subscriber in + let geocoder = CLGeocoder() + geocoder.geocodeAddressString(address) { (placemarks, _) in + subscriber.putNext(placemarks) + subscriber.putCompletion() + } + return ActionDisposable { + geocoder.cancelGeocode() + } + } +} + public func geocodeLocation(dictionary: [String: String]) -> Signal<(Double, Double)?, NoError> { return Signal { subscriber in let geocoder = CLGeocoder() @@ -24,6 +37,19 @@ public struct ReverseGeocodedPlacemark { public let city: String? public let country: String? + public var compactDisplayAddress: String? { + if let street = self.street { + return street + } + if let city = self.city { + return city + } + if let country = self.country { + return country + } + return nil + } + public var fullAddress: String { var components: [String] = [] if let street = self.street { diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index b353e21f39..a6e14bb87e 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -39,7 +39,7 @@ public final class HashtagSearchController: TelegramBaseController { self.title = query self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) - let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations) + let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations) let location: SearchMessagesLocation = .general let search = searchMessages(account: context.account, location: location, query: query, state: nil) @@ -49,6 +49,7 @@ public final class HashtagSearchController: TelegramBaseController { } let interaction = ChatListNodeInteraction(activateSearch: { }, peerSelected: { peer in + }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, messageSelected: { [weak self] peer, message, _ in if let strongSelf = self { @@ -79,7 +80,9 @@ public final class HashtagSearchController: TelegramBaseController { let previousEntries = previousSearchItems.swap(entries) let firstTime = previousEntries == nil - let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, context: strongSelf.context, enableHeaders: false, filter: [], interaction: interaction, peerContextAction: nil) + let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, context: strongSelf.context, presentationData: strongSelf.presentationData, enableHeaders: false, filter: [], interaction: interaction, peerContextAction: nil, toggleExpandLocalResults: { + }, toggleExpandGlobalResults: { + }) strongSelf.controllerNode.enqueueTransition(transition, firstTime: firstTime) } }) diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift index 866abaa0a0..ef47b6d8ea 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift @@ -136,29 +136,8 @@ final class HashtagSearchControllerNode: ASDisplayNode { self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) diff --git a/submodules/HorizontalPeerItem/BUCK b/submodules/HorizontalPeerItem/BUCK index fe74c3eda8..20a7708257 100644 --- a/submodules/HorizontalPeerItem/BUCK +++ b/submodules/HorizontalPeerItem/BUCK @@ -17,6 +17,7 @@ static_library( "//submodules/PeerOnlineMarkerNode:PeerOnlineMarkerNode", "//submodules/TelegramStringFormatting:TelegramStringFormatting", "//submodules/ContextUI:ContextUI", + "//submodules/AccountContext:AccountContext", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift b/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift index a3ee0ac522..bbff42ee43 100644 --- a/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift +++ b/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift @@ -11,6 +11,7 @@ import TelegramStringFormatting import PeerOnlineMarkerNode import SelectablePeerNode import ContextUI +import AccountContext public enum HorizontalPeerItemMode { case list @@ -23,7 +24,7 @@ public final class HorizontalPeerItem: ListViewItem { let theme: PresentationTheme let strings: PresentationStrings let mode: HorizontalPeerItemMode - let account: Account + let context: AccountContext public let peer: Peer let action: (Peer) -> Void let contextAction: (Peer, ASDisplayNode, ContextGesture?) -> Void @@ -32,11 +33,11 @@ public final class HorizontalPeerItem: ListViewItem { let presence: PeerPresence? let unreadBadge: (Int32, Bool)? - public init(theme: PresentationTheme, strings: PresentationStrings, mode: HorizontalPeerItemMode, account: Account, peer: Peer, presence: PeerPresence?, unreadBadge: (Int32, Bool)?, action: @escaping (Peer) -> Void, contextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, isPeerSelected: @escaping (PeerId) -> Bool, customWidth: CGFloat?) { + public init(theme: PresentationTheme, strings: PresentationStrings, mode: HorizontalPeerItemMode, context: AccountContext, peer: Peer, presence: PeerPresence?, unreadBadge: (Int32, Bool)?, action: @escaping (Peer) -> Void, contextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, isPeerSelected: @escaping (PeerId) -> Bool, customWidth: CGFloat?) { self.theme = theme self.strings = strings self.mode = mode - self.account = account + self.context = context self.peer = peer self.action = action self.contextAction = contextAction @@ -147,10 +148,10 @@ public final class HorizontalPeerItemNode: ListViewItemNode { let badgeTextColor: UIColor let (unreadCount, isMuted) = unreadBadge if isMuted { - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.theme) + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.theme, diameter: 20.0) badgeTextColor = item.theme.chatList.unreadBadgeInactiveTextColor } else { - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.theme) + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.theme, diameter: 20.0) badgeTextColor = item.theme.chatList.unreadBadgeActiveTextColor } badgeAttributedString = NSAttributedString(string: unreadCount > 0 ? "\(unreadCount)" : " ", font: badgeFont, textColor: badgeTextColor) @@ -187,7 +188,7 @@ public final class HorizontalPeerItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item strongSelf.peerNode.theme = itemTheme - strongSelf.peerNode.setup(account: item.account, theme: item.theme, strings: item.strings, peer: RenderedPeer(peer: item.peer), numberOfLines: 1, synchronousLoad: false) + strongSelf.peerNode.setup(context: item.context, theme: item.theme, strings: item.strings, peer: RenderedPeer(peer: item.peer), numberOfLines: 1, synchronousLoad: false) strongSelf.peerNode.frame = CGRect(origin: CGPoint(), size: itemLayout.size) strongSelf.peerNode.updateSelection(selected: item.isPeerSelected(item.peer.id), animated: false) diff --git a/submodules/InstantPageUI/BUCK b/submodules/InstantPageUI/BUCK index 4c6a52fdde..52ef2dc662 100644 --- a/submodules/InstantPageUI/BUCK +++ b/submodules/InstantPageUI/BUCK @@ -20,6 +20,7 @@ static_library( "//submodules/MosaicLayout:MosaicLayout", "//submodules/LocationUI:LocationUI", "//submodules/AppBundle:AppBundle", + "//submodules/LocationResources:LocationResources", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/InstantPageUI/Sources/InstantImageGalleryItem.swift b/submodules/InstantPageUI/Sources/InstantImageGalleryItem.swift index 64034b701b..087b35790e 100644 --- a/submodules/InstantPageUI/Sources/InstantImageGalleryItem.swift +++ b/submodules/InstantPageUI/Sources/InstantImageGalleryItem.swift @@ -35,6 +35,12 @@ private struct InstantImageGalleryThumbnailItem: GalleryThumbnailItem { } class InstantImageGalleryItem: GalleryItem { + var id: AnyHashable { + return self.itemId + } + + let itemId: AnyHashable + let context: AccountContext let presentationData: PresentationData let imageReference: ImageMediaReference @@ -44,7 +50,8 @@ class InstantImageGalleryItem: GalleryItem { let openUrl: (InstantPageUrlItem) -> Void let openUrlOptions: (InstantPageUrlItem) -> Void - init(context: AccountContext, presentationData: PresentationData, imageReference: ImageMediaReference, caption: NSAttributedString, credit: NSAttributedString, location: InstantPageGalleryEntryLocation?, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlOptions: @escaping (InstantPageUrlItem) -> Void) { + init(context: AccountContext, presentationData: PresentationData, itemId: AnyHashable, imageReference: ImageMediaReference, caption: NSAttributedString, credit: NSAttributedString, location: InstantPageGalleryEntryLocation?, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlOptions: @escaping (InstantPageUrlItem) -> Void) { + self.itemId = itemId self.context = context self.presentationData = presentationData self.imageReference = imageReference @@ -161,14 +168,14 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { self.footerContentNode.setShareMedia(fileReference.abstract) } - override func animateIn(from node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { + override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view) let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view.superview) let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) let transformedCopyViewFinalFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view) - let surfaceCopyView = node.1().0! - let copyView = node.1().0! + let surfaceCopyView = node.2().0! + let copyView = node.2().0! addToTransitionSurface(surfaceCopyView) @@ -217,7 +224,7 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { self.statusNodeContainer.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)*/ } - override func animateOut(to node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { self.fetchDisposable.set(nil) var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view) @@ -229,8 +236,8 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { var boundsCompleted = false var copyCompleted = false - let copyView = node.1().0! - let surfaceCopyView = node.1().0! + let copyView = node.2().0! + let surfaceCopyView = node.2().0! addToTransitionSurface(surfaceCopyView) @@ -301,7 +308,7 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { return self._title.get() } - override func footerContent() -> Signal { - return .single(self.footerContentNode) + override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { + return .single((self.footerContentNode, nil)) } } diff --git a/submodules/InstantPageUI/Sources/InstantPageAnchorItem.swift b/submodules/InstantPageUI/Sources/InstantPageAnchorItem.swift index 9c2d810467..7ff05d9c24 100644 --- a/submodules/InstantPageUI/Sources/InstantPageAnchorItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageAnchorItem.swift @@ -28,7 +28,7 @@ final class InstantPageAnchorItem: InstantPageItem { func drawInTile(context: CGContext) { } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return nil } diff --git a/submodules/InstantPageUI/Sources/InstantPageArticleItem.swift b/submodules/InstantPageUI/Sources/InstantPageArticleItem.swift index 0a2eb5f941..6d9d7435be 100644 --- a/submodules/InstantPageUI/Sources/InstantPageArticleItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageArticleItem.swift @@ -33,7 +33,7 @@ final class InstantPageArticleItem: InstantPageItem { self.rtl = rtl } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return InstantPageArticleNode(context: context, item: self, webPage: self.webPage, strings: strings, theme: theme, contentItems: self.contentItems, contentSize: self.contentSize, cover: self.cover, url: self.url, webpageId: self.webpageId, rtl: self.rtl, openUrl: openUrl) } diff --git a/submodules/InstantPageUI/Sources/InstantPageArticleNode.swift b/submodules/InstantPageUI/Sources/InstantPageArticleNode.swift index a493fa8efa..e1387d4885 100644 --- a/submodules/InstantPageUI/Sources/InstantPageArticleNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageArticleNode.swift @@ -125,7 +125,7 @@ final class InstantPageArticleNode: ASDisplayNode, InstantPageNode { func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { } - func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil } diff --git a/submodules/InstantPageUI/Sources/InstantPageAudioItem.swift b/submodules/InstantPageUI/Sources/InstantPageAudioItem.swift index 5fcd695a29..8516284d54 100644 --- a/submodules/InstantPageUI/Sources/InstantPageAudioItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageAudioItem.swift @@ -24,7 +24,7 @@ final class InstantPageAudioItem: InstantPageItem { self.medias = [media] } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return InstantPageAudioNode(context: context, strings: strings, theme: theme, webPage: self.webpage, media: self.media, openMedia: openMedia) } diff --git a/submodules/InstantPageUI/Sources/InstantPageAudioNode.swift b/submodules/InstantPageUI/Sources/InstantPageAudioNode.swift index 6503ccb479..ce5b312d17 100644 --- a/submodules/InstantPageUI/Sources/InstantPageAudioNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageAudioNode.swift @@ -35,13 +35,13 @@ private func generatePauseButton(color: UIColor) -> UIImage? { }) } -private func titleString(media: InstantPageMedia, theme: InstantPageTheme) -> NSAttributedString { +private func titleString(media: InstantPageMedia, theme: InstantPageTheme, strings: PresentationStrings) -> NSAttributedString { let string = NSMutableAttributedString() if let file = media.media as? TelegramMediaFile { loop: for attribute in file.attributes { if case let .Audio(isVoice, _, title, performer, _) = attribute, !isVoice { - let titleText: String = title ?? "Unknown Track" - let subtitleText: String = performer ?? "Unknown Artist" + let titleText: String = title ?? strings.MediaPlayer_UnknownTrack + let subtitleText: String = performer ?? strings.MediaPlayer_UnknownArtist let titleString = NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: theme.textCategories.paragraph.color) let subtitleString = NSAttributedString(string: " — \(subtitleText)", font: Font.regular(17.0), textColor: theme.textCategories.paragraph.color) @@ -111,7 +111,7 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode { super.init() - self.titleNode.attributedText = titleString(media: media, theme: theme) + self.titleNode.attributedText = titleString(media: media, theme: theme, strings: strings) self.addSubnode(self.statusNode) self.addSubnode(self.buttonNode) @@ -226,7 +226,7 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode { self.playImage = generatePlayButton(color: theme.textCategories.paragraph.color)! self.pauseImage = generatePauseButton(color: theme.textCategories.paragraph.color)! - self.titleNode.attributedText = titleString(media: self.media, theme: theme) + self.titleNode.attributedText = titleString(media: self.media, theme: theme, strings: strings) var brightness: CGFloat = 0.0 theme.textCategories.paragraph.color.getHue(nil, saturation: nil, brightness: &brightness, alpha: nil) @@ -236,7 +236,7 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode { } } - func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil } diff --git a/submodules/InstantPageUI/Sources/InstantPageContentNode.swift b/submodules/InstantPageUI/Sources/InstantPageContentNode.swift index d8e9ef8b55..be14889554 100644 --- a/submodules/InstantPageUI/Sources/InstantPageContentNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageContentNode.swift @@ -14,6 +14,7 @@ final class InstantPageContentNode : ASDisplayNode { private let context: AccountContext private let strings: PresentationStrings private let nameDisplayOrder: PresentationPersonNameOrder + private let sourcePeerType: MediaAutoDownloadPeerType private let theme: InstantPageTheme private let openMedia: (InstantPageMedia) -> Void @@ -40,10 +41,11 @@ final class InstantPageContentNode : ASDisplayNode { private var previousVisibleBounds: CGRect? - init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, items: [InstantPageItem], contentSize: CGSize, inOverlayPanel: Bool = false, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void) { + init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, items: [InstantPageItem], contentSize: CGSize, inOverlayPanel: Bool = false, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void) { self.context = context self.strings = strings self.nameDisplayOrder = nameDisplayOrder + self.sourcePeerType = sourcePeerType self.theme = theme self.openMedia = openMedia @@ -187,7 +189,7 @@ final class InstantPageContentNode : ASDisplayNode { if itemNode == nil { let itemIndex = itemIndex let detailsIndex = detailsIndex - if let newNode = item.node(context: self.context, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, theme: theme, openMedia: { [weak self] media in + if let newNode = item.node(context: self.context, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, theme: theme, sourcePeerType: self.sourcePeerType, openMedia: { [weak self] media in self?.openMedia(media) }, longPressMedia: { [weak self] media in self?.longPressMedia(media) @@ -315,7 +317,7 @@ final class InstantPageContentNode : ASDisplayNode { self.requestLayoutUpdate?(animated) } - func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { for (_, itemNode) in self.visibleItemsWithNodes { if let transitionNode = itemNode.transitionNode(media: media) { return transitionNode diff --git a/submodules/InstantPageUI/Sources/InstantPageController.swift b/submodules/InstantPageUI/Sources/InstantPageController.swift index 8691aa57a8..b8d3fb4cfe 100644 --- a/submodules/InstantPageUI/Sources/InstantPageController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageController.swift @@ -75,7 +75,7 @@ public final class InstantPageController: ViewController { strongSelf.settings = settings strongSelf.themeSettings = themeSettings if strongSelf.isNodeLoaded { - strongSelf.controllerNode.update(settings: settings, strings: strongSelf.presentationData.strings) + strongSelf.controllerNode.update(settings: settings, themeSettings: themeSettings, strings: strongSelf.presentationData.strings) } } }) @@ -96,7 +96,7 @@ public final class InstantPageController: ViewController { } override public func loadDisplayNode() { - self.displayNode = InstantPageControllerNode(context: self.context, settings: self.settings, themeSettings: self.themeSettings, presentationTheme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, statusBar: self.statusBar, sourcePeerType: self.sourcePeerType, getNavigationController: { [weak self] in + self.displayNode = InstantPageControllerNode(context: self.context, settings: self.settings, themeSettings: self.themeSettings, presentationTheme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, autoNightModeTriggered: self.presentationData.autoNightModeTriggered, statusBar: self.statusBar, sourcePeerType: self.sourcePeerType, getNavigationController: { [weak self] in return self?.navigationController as? NavigationController }, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a, blockInteraction: true) diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index 3d661e0949..1360bc8ac7 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -23,6 +23,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { private var presentationTheme: PresentationTheme private var strings: PresentationStrings private var nameDisplayOrder: PresentationPersonNameOrder + private let autoNightModeTriggered: Bool private var dateTimeFormat: PresentationDateTimeFormat private var theme: InstantPageTheme? private let sourcePeerType: MediaAutoDownloadPeerType @@ -87,17 +88,18 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { return InstantPageStoredState(contentOffset: Double(self.scrollNode.view.contentOffset.y), details: details) } - init(context: AccountContext, settings: InstantPagePresentationSettings?, themeSettings: PresentationThemeSettings?, presentationTheme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, statusBar: StatusBar, sourcePeerType: MediaAutoDownloadPeerType, getNavigationController: @escaping () -> NavigationController?, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, openPeer: @escaping (PeerId) -> Void, navigateBack: @escaping () -> Void) { + init(context: AccountContext, settings: InstantPagePresentationSettings?, themeSettings: PresentationThemeSettings?, presentationTheme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, autoNightModeTriggered: Bool, statusBar: StatusBar, sourcePeerType: MediaAutoDownloadPeerType, getNavigationController: @escaping () -> NavigationController?, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, openPeer: @escaping (PeerId) -> Void, navigateBack: @escaping () -> Void) { self.context = context self.presentationTheme = presentationTheme self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder + self.autoNightModeTriggered = autoNightModeTriggered self.strings = strings self.settings = settings let themeReferenceDate = Date() self.themeReferenceDate = themeReferenceDate self.theme = settings.flatMap { settings in - return instantPageThemeForType(instantPageThemeTypeForSettingsAndTime(themeSettings: themeSettings, settings: settings, time: themeReferenceDate).0, settings: settings) + return instantPageThemeForType(instantPageThemeTypeForSettingsAndTime(themeSettings: themeSettings, settings: settings, time: themeReferenceDate, forceDarkTheme: autoNightModeTriggered).0, settings: settings) } self.sourcePeerType = sourcePeerType self.statusBar = statusBar @@ -162,7 +164,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { self.loadProgressDisposable.dispose() } - func update(settings: InstantPagePresentationSettings, strings: PresentationStrings) { + func update(settings: InstantPagePresentationSettings, themeSettings: PresentationThemeSettings?, strings: PresentationStrings) { if self.settings != settings || self.strings !== strings { let previousSettings = self.settings var updateLayout = previousSettings == nil @@ -174,7 +176,8 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } self.settings = settings - let themeType = instantPageThemeTypeForSettingsAndTime(themeSettings: self.themeSettings, settings: settings, time: self.themeReferenceDate) + self.themeSettings = themeSettings + let themeType = instantPageThemeTypeForSettingsAndTime(themeSettings: self.themeSettings, settings: settings, time: self.themeReferenceDate, forceDarkTheme: self.autoNightModeTriggered) let theme = instantPageThemeForType(themeType.0, settings: settings) self.theme = theme self.strings = strings @@ -495,6 +498,9 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { if item is InstantPageWebEmbedItem { embedIndex += 1 } + if let imageItem = item as? InstantPageImageItem, imageItem.media.media is TelegramMediaWebpage { + embedIndex += 1 + } if item is InstantPageDetailsItem { detailsIndex += 1 } @@ -535,7 +541,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { let itemIndex = itemIndex let embedIndex = embedIndex let detailsIndex = detailsIndex - if let newNode = item.node(context: self.context, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, theme: theme, openMedia: { [weak self] media in + if let newNode = item.node(context: self.context, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, theme: theme, sourcePeerType: self.sourcePeerType, openMedia: { [weak self] media in self?.openMedia(media) }, longPressMedia: { [weak self] media in self?.longPressMedia(media) @@ -869,12 +875,12 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { private func longPressMedia(_ media: InstantPageMedia) { let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in if let strongSelf = self, let image = media.media as? TelegramMediaImage { - let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil) + let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) let _ = copyToPasteboard(context: strongSelf.context, postbox: strongSelf.context.account.postbox, mediaReference: .standalone(media: media)).start() } }), ContextMenuAction(content: .text(title: self.strings.Conversation_LinkDialogSave, accessibilityLabel: self.strings.Conversation_LinkDialogSave), action: { [weak self] in if let strongSelf = self, let image = media.media as? TelegramMediaImage { - let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil) + let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) let _ = saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, mediaReference: .standalone(media: media)).start() } }), ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in @@ -885,7 +891,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { self.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in if let strongSelf = self { for (_, itemNode) in strongSelf.visibleItemsWithNodes { - if let (node, _) = itemNode.transitionNode(media: media) { + if let (node, _, _) = itemNode.transitionNode(media: media) { return (strongSelf.scrollNode, node.convert(node.bounds, to: strongSelf.scrollNode), strongSelf, strongSelf.bounds) } } @@ -931,7 +937,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: self.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -1045,7 +1051,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { return } - let controller = InstantPageReferenceController(context: self.context, theme: theme, webPage: webPage, anchorText: anchorText, openUrl: { [weak self] url in + let controller = InstantPageReferenceController(context: self.context, sourcePeerType: self.sourcePeerType, theme: theme, webPage: webPage, anchorText: anchorText, openUrl: { [weak self] url in self?.openUrl(url) }, openUrlIn: { [weak self] url in self?.openUrlIn(url) @@ -1174,7 +1180,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { let _ = (strongSelf.context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { peer in if let strongSelf = self { - if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { strongSelf.getNavigationController()?.pushViewController(controller) } } @@ -1188,7 +1194,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { self?.present(c, a) }, dismissInput: { self?.view.endEditing(true) - }) + }, contentContext: nil) } } })) @@ -1332,9 +1338,9 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { return } if self.settingsNode == nil { - let settingsNode = InstantPageSettingsNode(strings: self.strings, settings: settings, currentThemeType: instantPageThemeTypeForSettingsAndTime(themeSettings: self.themeSettings, settings: settings, time: self.themeReferenceDate), applySettings: { [weak self] settings in + let settingsNode = InstantPageSettingsNode(strings: self.strings, settings: settings, currentThemeType: instantPageThemeTypeForSettingsAndTime(themeSettings: self.themeSettings, settings: settings, time: self.themeReferenceDate, forceDarkTheme: self.autoNightModeTriggered), applySettings: { [weak self] settings in if let strongSelf = self { - strongSelf.update(settings: settings, strings: strongSelf.strings) + strongSelf.update(settings: settings, themeSettings: strongSelf.themeSettings, strings: strongSelf.strings) let _ = updateInstantPagePresentationSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { _ in return settings }).start() diff --git a/submodules/InstantPageUI/Sources/InstantPageDetailsItem.swift b/submodules/InstantPageUI/Sources/InstantPageDetailsItem.swift index 403bc21adc..ef2087425d 100644 --- a/submodules/InstantPageUI/Sources/InstantPageDetailsItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageDetailsItem.swift @@ -34,12 +34,12 @@ final class InstantPageDetailsItem: InstantPageItem { self.index = index } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { var expanded: Bool? if let expandedDetails = currentExpandedDetails, let currentlyExpanded = expandedDetails[self.index] { expanded = currentlyExpanded } - return InstantPageDetailsNode(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, item: self, openMedia: openMedia, longPressMedia: longPressMedia, openPeer: openPeer, openUrl: openUrl, currentlyExpanded: expanded, updateDetailsExpanded: updateDetailsExpanded) + return InstantPageDetailsNode(context: context, sourcePeerType: sourcePeerType, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, item: self, openMedia: openMedia, longPressMedia: longPressMedia, openPeer: openPeer, openUrl: openUrl, currentlyExpanded: expanded, updateDetailsExpanded: updateDetailsExpanded) } func matchesAnchor(_ anchor: String) -> Bool { diff --git a/submodules/InstantPageUI/Sources/InstantPageDetailsNode.swift b/submodules/InstantPageUI/Sources/InstantPageDetailsNode.swift index 168e061e2c..740e8c3f38 100644 --- a/submodules/InstantPageUI/Sources/InstantPageDetailsNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageDetailsNode.swift @@ -36,7 +36,7 @@ final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode { var requestLayoutUpdate: ((Bool) -> Void)? - init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, item: InstantPageDetailsItem, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, currentlyExpanded: Bool?, updateDetailsExpanded: @escaping (Bool) -> Void) { + init(context: AccountContext, sourcePeerType: MediaAutoDownloadPeerType, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, item: InstantPageDetailsItem, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, currentlyExpanded: Bool?, updateDetailsExpanded: @escaping (Bool) -> Void) { self.context = context self.strings = strings self.nameDisplayOrder = nameDisplayOrder @@ -66,7 +66,7 @@ final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode { self.arrowNode = InstantPageDetailsArrowNode(color: theme.controlColor, open: self.expanded) self.separatorNode = ASDisplayNode() - self.contentNode = InstantPageContentNode(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, items: item.items, contentSize: CGSize(width: item.frame.width, height: item.frame.height - item.titleHeight), openMedia: openMedia, longPressMedia: longPressMedia, openPeer: openPeer, openUrl: openUrl) + self.contentNode = InstantPageContentNode(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, sourcePeerType: sourcePeerType, theme: theme, items: item.items, contentSize: CGSize(width: item.frame.width, height: item.frame.height - item.titleHeight), openMedia: openMedia, longPressMedia: longPressMedia, openPeer: openPeer, openUrl: openUrl) super.init() @@ -143,7 +143,7 @@ final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode { } - func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return self.contentNode.transitionNode(media: media) } diff --git a/submodules/InstantPageUI/Sources/InstantPageFeedbackItem.swift b/submodules/InstantPageUI/Sources/InstantPageFeedbackItem.swift index 5e499e33e1..a67941ecbb 100644 --- a/submodules/InstantPageUI/Sources/InstantPageFeedbackItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageFeedbackItem.swift @@ -21,7 +21,7 @@ final class InstantPageFeedbackItem: InstantPageItem { self.webPage = webPage } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return InstantPageFeedbackNode(context: context, strings: strings, theme: theme, webPage: self.webPage, openUrl: openUrl) } diff --git a/submodules/InstantPageUI/Sources/InstantPageFeedbackNode.swift b/submodules/InstantPageUI/Sources/InstantPageFeedbackNode.swift index 352782f4a0..0cbba86791 100644 --- a/submodules/InstantPageUI/Sources/InstantPageFeedbackNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageFeedbackNode.swift @@ -90,7 +90,7 @@ final class InstantPageFeedbackNode: ASDisplayNode, InstantPageNode { func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { } - func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil } diff --git a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift index 62a5f53e3f..6c7ef95f88 100644 --- a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift @@ -98,7 +98,7 @@ public struct InstantPageGalleryEntry: Equatable { } if let image = self.media.media as? TelegramMediaImage { - return InstantImageGalleryItem(context: context, presentationData: presentationData, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions) + return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions) } else if let file = self.media.media as? TelegramMediaFile { if file.isVideo { var indexData: GalleryItemIndexData? @@ -120,8 +120,8 @@ public struct InstantPageGalleryEntry: Equatable { if let dimensions = file.dimensions { representations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: file.resource)) } - let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil) - return InstantImageGalleryItem(context: context, presentationData: presentationData, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions) + let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) + return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions) } } else if let embedWebpage = self.media.media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = embedWebpage.content { if let content = WebEmbedVideoContent(webPage: embedWebpage, webpageContent: webpageContent) { @@ -170,7 +170,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable private let centralItemTitleView = Promise() private let centralItemRightBarButtonItem = Promise() private let centralItemNavigationStyle = Promise() - private let centralItemFooterContentNode = Promise() + private let centralItemFooterContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>() private let centralItemAttributesDisposable = DisposableSet(); private let _hiddenMedia = Promise(nil) @@ -243,7 +243,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable self?.navigationItem.rightBarButtonItem = rightBarButtonItem })) - self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode in + self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode, _ in self?.galleryNode.updatePresentationState({ $0.withUpdatedFooterContentNode(footerContentNode) }, transition: .immediate) @@ -260,7 +260,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable if let strongSelf = self { let canOpenIn = availableOpenInOptions(context: context, item: .url(url: url.url)).count > 1 let openText = canOpenIn ? strongSelf.presentationData.strings.Conversation_FileOpenIn : strongSelf.presentationData.strings.Conversation_LinkDialogOpen - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: url.url), ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in @@ -278,7 +278,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable } }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) diff --git a/submodules/InstantPageUI/Sources/InstantPageImageItem.swift b/submodules/InstantPageUI/Sources/InstantPageImageItem.swift index 0b8839d178..6bed10a920 100644 --- a/submodules/InstantPageUI/Sources/InstantPageImageItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageImageItem.swift @@ -45,8 +45,8 @@ final class InstantPageImageItem: InstantPageItem { self.fit = fit } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { - return InstantPageImageNode(context: context, theme: theme, webPage: self.webPage, media: self.media, attributes: self.attributes, interactive: self.interactive, roundCorners: self.roundCorners, fit: self.fit, openMedia: openMedia, longPressMedia: longPressMedia) + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + return InstantPageImageNode(context: context, sourcePeerType: sourcePeerType, theme: theme, webPage: self.webPage, media: self.media, attributes: self.attributes, interactive: self.interactive, roundCorners: self.roundCorners, fit: self.fit, openMedia: openMedia, longPressMedia: longPressMedia) } func matchesAnchor(_ anchor: String) -> Bool { diff --git a/submodules/InstantPageUI/Sources/InstantPageImageNode.swift b/submodules/InstantPageUI/Sources/InstantPageImageNode.swift index df7375c3db..0060020d55 100644 --- a/submodules/InstantPageUI/Sources/InstantPageImageNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageImageNode.swift @@ -11,8 +11,10 @@ import AccountContext import RadialStatusNode import PhotoResources import MediaResources +import LocationResources import LiveLocationPositionNode import AppBundle +import TelegramUIPreferences private struct FetchControls { let fetch: (Bool) -> Void @@ -46,7 +48,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { private var themeUpdated: Bool = false - init(context: AccountContext, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute], interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void) { + init(context: AccountContext, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute], interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void) { self.context = context self.theme = theme self.webPage = webPage @@ -71,7 +73,9 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image) self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, photoReference: imageReference)) - self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, storeToDownloadsPeerType: nil).start()) + if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings.with { $0 }, peerType: sourcePeerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: image) { + self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, storeToDownloadsPeerType: nil).start()) + } self.fetchControls = FetchControls(fetch: { [weak self] manual in if let strongSelf = self { @@ -101,7 +105,9 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { } else if let file = media.media as? TelegramMediaFile { let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file) if file.mimeType.hasPrefix("image/") { - _ = freeMediaFileInteractiveFetched(account: context.account, fileReference: fileReference).start() + if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings.with { $0 }, peerType: sourcePeerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: file) { + _ = freeMediaFileInteractiveFetched(account: context.account, fileReference: fileReference).start() + } self.imageNode.setSignal(instantPageImageFile(account: context.account, fileReference: fileReference, fetched: true)) } else { self.imageNode.setSignal(chatMessageVideo(postbox: context.account.postbox, videoReference: fileReference)) @@ -230,7 +236,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { let makePinLayout = self.pinNode.asyncLayout() let theme = self.context.sharedContext.currentPresentationData.with { $0 }.theme - let (pinSize, pinApply) = makePinLayout(self.context.account, theme, nil, false) + let (pinSize, pinApply) = makePinLayout(self.context, theme, .location(nil)) self.pinNode.frame = CGRect(origin: CGPoint(x: floor((size.width - pinSize.width) / 2.0), y: floor(size.height * 0.5 - 10.0 - pinSize.height / 2.0)), size: pinSize) pinApply() } else if let webPage = media.media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let image = content.image, let largest = largestImageRepresentation(image.representations) { @@ -244,10 +250,10 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { } } - func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if media == self.media { let imageNode = self.imageNode - return (self.imageNode, { [weak imageNode] in + return (self.imageNode, self.imageNode.bounds, { [weak imageNode] in return (imageNode?.view.snapshotContentTree(unhide: true), nil) }) } else { diff --git a/submodules/InstantPageUI/Sources/InstantPageItem.swift b/submodules/InstantPageUI/Sources/InstantPageItem.swift index f7a9d25a9c..c60bd39cf0 100644 --- a/submodules/InstantPageUI/Sources/InstantPageItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageItem.swift @@ -16,7 +16,7 @@ protocol InstantPageItem { func matchesAnchor(_ anchor: String) -> Bool func drawInTile(context: CGContext) - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? func matchesNode(_ node: InstantPageNode) -> Bool func linkSelectionRects(at point: CGPoint) -> [CGRect] diff --git a/submodules/InstantPageUI/Sources/InstantPageLayout.swift b/submodules/InstantPageUI/Sources/InstantPageLayout.swift index ddd2d2eb6a..65fe599451 100644 --- a/submodules/InstantPageUI/Sources/InstantPageLayout.swift +++ b/submodules/InstantPageUI/Sources/InstantPageLayout.swift @@ -628,7 +628,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: Ins let frame = CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size) let item: InstantPageItem if let url = url, let coverId = coverId, let image = media[coverId] as? TelegramMediaImage { - let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, image: image, file: nil, files: nil, instantPage: nil) + let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, image: image, file: nil, attributes: [], instantPage: nil) let content = TelegramMediaWebpageContent.Loaded(loadedContent) item = InstantPageImageItem(frame: frame, webPage: webpage, media: InstantPageMedia(index: embedIndex, media: TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: -1), content: content), url: nil, caption: nil, credit: nil), attributes: [], interactive: true, roundCorners: false, fit: false) diff --git a/submodules/InstantPageUI/Sources/InstantPageNode.swift b/submodules/InstantPageUI/Sources/InstantPageNode.swift index 6d1745963b..3eb643e1b4 100644 --- a/submodules/InstantPageUI/Sources/InstantPageNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageNode.swift @@ -7,7 +7,7 @@ import TelegramPresentationData protocol InstantPageNode { func updateIsVisible(_ isVisible: Bool) - func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> (UIView?, UIView?))? + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? func updateHiddenMedia(media: InstantPageMedia?) func update(strings: PresentationStrings, theme: InstantPageTheme) diff --git a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceItem.swift b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceItem.swift index 6400461344..3b5e71ccc4 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceItem.swift @@ -27,7 +27,7 @@ final class InstantPagePeerReferenceItem: InstantPageItem { self.rtl = rtl } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return InstantPagePeerReferenceNode(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, initialPeer: self.initialPeer, safeInset: self.safeInset, transparent: self.transparent, rtl: self.rtl, openPeer: openPeer) } diff --git a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift index 410488dc4c..ffad4284c0 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift @@ -290,7 +290,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { } } - func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil } diff --git a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift index a4ad1c2237..d43d84cc0c 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift @@ -29,7 +29,7 @@ final class InstantPagePlayableVideoItem: InstantPageItem { self.interactive = interactive } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return InstantPagePlayableVideoNode(context: context, webPage: self.webPage, theme: theme, media: self.media, interactive: self.interactive, openMedia: openMedia) } diff --git a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift index 944c9c8f90..12150b846d 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift @@ -47,7 +47,7 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler var imageReference: ImageMediaReference? if let file = media.media as? TelegramMediaFile, let presentation = smallestImageRepresentation(file.previewRepresentations) { - let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [presentation], immediateThumbnailData: nil, reference: nil, partialReference: nil) + let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [presentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image) } @@ -149,9 +149,9 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler } } - func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if media == self.media { - return (self, { [weak self] in + return (self, self.bounds, { [weak self] in return (self?.view.snapshotContentTree(unhide: true), nil) }) } else { diff --git a/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift b/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift index a6eaf0d51d..4768118441 100644 --- a/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift @@ -7,6 +7,7 @@ import TelegramCore import SyncCore import SwiftSignalKit import AccountContext +import TelegramUIPreferences final class InstantPageReferenceController: ViewController { private var controllerNode: InstantPageReferenceControllerNode { @@ -16,6 +17,7 @@ final class InstantPageReferenceController: ViewController { private var animatedIn = false private let context: AccountContext + private let sourcePeerType: MediaAutoDownloadPeerType private let theme: InstantPageTheme private let webPage: TelegramMediaWebpage private let anchorText: NSAttributedString @@ -23,8 +25,9 @@ final class InstantPageReferenceController: ViewController { private let openUrlIn: (InstantPageUrlItem) -> Void private let present: (ViewController, Any?) -> Void - init(context: AccountContext, theme: InstantPageTheme, webPage: TelegramMediaWebpage, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) { + init(context: AccountContext, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, webPage: TelegramMediaWebpage, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context + self.sourcePeerType = sourcePeerType self.theme = theme self.webPage = webPage self.anchorText = anchorText @@ -42,7 +45,7 @@ final class InstantPageReferenceController: ViewController { } override public func loadDisplayNode() { - self.displayNode = InstantPageReferenceControllerNode(context: self.context, theme: self.theme, webPage: self.webPage, anchorText: self.anchorText, openUrl: self.openUrl, openUrlIn: self.openUrlIn, present: self.present) + self.displayNode = InstantPageReferenceControllerNode(context: self.context, sourcePeerType: self.sourcePeerType, theme: self.theme, webPage: self.webPage, anchorText: self.anchorText, openUrl: self.openUrl, openUrlIn: self.openUrlIn, present: self.present) self.controllerNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) } diff --git a/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift index a13d6c5c51..02d1007769 100644 --- a/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift @@ -10,9 +10,11 @@ import TelegramPresentationData import AccountContext import ShareController import OpenInExternalAppUI +import TelegramUIPreferences class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private let context: AccountContext + private let sourcePeerType: MediaAutoDownloadPeerType private let theme: InstantPageTheme private var presentationData: PresentationData private let webPage: TelegramMediaWebpage @@ -38,8 +40,9 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie var dismiss: (() -> Void)? var close: (() -> Void)? - init(context: AccountContext, theme: InstantPageTheme, webPage: TelegramMediaWebpage, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) { + init(context: AccountContext, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, webPage: TelegramMediaWebpage, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context + self.sourcePeerType = sourcePeerType self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.theme = theme self.webPage = webPage @@ -202,7 +205,7 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie let sideInset: CGFloat = 16.0 let (_, items, contentSize) = layoutTextItemWithString(self.anchorText, boundingWidth: width - sideInset * 2.0, offset: CGPoint(x: sideInset, y: sideInset), media: media, webpage: self.webPage) - let contentNode = InstantPageContentNode(context: self.context, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, theme: self.theme, items: items, contentSize: CGSize(width: width, height: contentSize.height), inOverlayPanel: true, openMedia: { _ in }, longPressMedia: { _ in }, openPeer: { _ in }, openUrl: { _ in }) + let contentNode = InstantPageContentNode(context: self.context, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, sourcePeerType: self.sourcePeerType, theme: self.theme, items: items, contentSize: CGSize(width: width, height: contentSize.height), inOverlayPanel: true, openMedia: { _ in }, longPressMedia: { _ in }, openPeer: { _ in }, openUrl: { _ in }) transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: titleAreaHeight), size: CGSize(width: width, height: contentSize.height))) self.contentContainerNode.insertSubnode(contentNode, at: 0) self.contentNode = contentNode @@ -368,7 +371,7 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie } }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) diff --git a/submodules/InstantPageUI/Sources/InstantPageScrollableNode.swift b/submodules/InstantPageUI/Sources/InstantPageScrollableNode.swift index 33f5a4aa84..547ae8fbf7 100644 --- a/submodules/InstantPageUI/Sources/InstantPageScrollableNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageScrollableNode.swift @@ -98,7 +98,7 @@ final class InstantPageScrollableNode: ASScrollNode, InstantPageNode { func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { } - func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil } diff --git a/submodules/InstantPageUI/Sources/InstantPageShapeItem.swift b/submodules/InstantPageUI/Sources/InstantPageShapeItem.swift index bf5fbcf629..3d22f56dd6 100644 --- a/submodules/InstantPageUI/Sources/InstantPageShapeItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageShapeItem.swift @@ -62,7 +62,7 @@ final class InstantPageShapeItem: InstantPageItem { return false } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return nil } diff --git a/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift b/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift index 561f744a00..9ce77fc9a6 100644 --- a/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift @@ -21,8 +21,8 @@ final class InstantPageSlideshowItem: InstantPageItem { self.medias = medias } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { - return InstantPageSlideshowNode(context: context, theme: theme, webPage: webPage, medias: self.medias, openMedia: openMedia, longPressMedia: longPressMedia) + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + return InstantPageSlideshowNode(context: context, sourcePeerType: sourcePeerType, theme: theme, webPage: webPage, medias: self.medias, openMedia: openMedia, longPressMedia: longPressMedia) } func matchesAnchor(_ anchor: String) -> Bool { diff --git a/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift b/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift index ca2a03bdba..950907e2c8 100644 --- a/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift @@ -6,6 +6,7 @@ import AsyncDisplayKit import Display import TelegramPresentationData import AccountContext +import TelegramUIPreferences private final class InstantPageSlideshowItemNode: ASDisplayNode { private var _index: Int? @@ -54,7 +55,7 @@ private final class InstantPageSlideshowItemNode: ASDisplayNode { } } - func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if let node = self.contentNode as? InstantPageNode { return node.transitionNode(media: media) } @@ -64,6 +65,7 @@ private final class InstantPageSlideshowItemNode: ASDisplayNode { private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDelegate { private let context: AccountContext + private let sourcePeerType: MediaAutoDownloadPeerType private let theme: InstantPageTheme private let webPage: TelegramMediaWebpage private let openMedia: (InstantPageMedia) -> Void @@ -97,8 +99,9 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDe } } - init(context: AccountContext, theme: InstantPageTheme, webPage: TelegramMediaWebpage, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, pageGap: CGFloat = 0.0) { + init(context: AccountContext, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, webPage: TelegramMediaWebpage, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, pageGap: CGFloat = 0.0) { self.context = context + self.sourcePeerType = sourcePeerType self.theme = theme self.webPage = webPage self.openMedia = openMedia @@ -180,7 +183,7 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDe let media = self.items[index] let contentNode: ASDisplayNode if let _ = media.media as? TelegramMediaImage { - contentNode = InstantPageImageNode(context: self.context, theme: self.theme, webPage: self.webPage, media: media, attributes: [], interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia, longPressMedia: self.longPressMedia) + contentNode = InstantPageImageNode(context: self.context, sourcePeerType: self.sourcePeerType, theme: self.theme, webPage: self.webPage, media: media, attributes: [], interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia, longPressMedia: self.longPressMedia) } else if let file = media.media as? TelegramMediaFile { contentNode = ASDisplayNode() } else { @@ -363,7 +366,7 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDe } } - func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { for node in self.itemNodes { if let transitionNode = node.transitionNode(media: media) { return transitionNode @@ -379,10 +382,10 @@ final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode { private let pagerNode: InstantPageSlideshowPagerNode private let pageControlNode: PageControlNode - init(context: AccountContext, theme: InstantPageTheme, webPage: TelegramMediaWebpage, medias: [InstantPageMedia], openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void) { + init(context: AccountContext, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, webPage: TelegramMediaWebpage, medias: [InstantPageMedia], openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void) { self.medias = medias - self.pagerNode = InstantPageSlideshowPagerNode(context: context, theme: theme, webPage: webPage, openMedia: openMedia, longPressMedia: longPressMedia) + self.pagerNode = InstantPageSlideshowPagerNode(context: context, sourcePeerType: sourcePeerType, theme: theme, webPage: webPage, openMedia: openMedia, longPressMedia: longPressMedia) self.pagerNode.replaceItems(medias, centralItemIndex: nil) self.pageControlNode = PageControlNode(dotColor: .white, inactiveDotColor: UIColor(white: 1.0, alpha: 0.5)) @@ -422,7 +425,7 @@ final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode { } } - func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return self.pagerNode.transitionNode(media: media) } diff --git a/submodules/InstantPageUI/Sources/InstantPageTableItem.swift b/submodules/InstantPageUI/Sources/InstantPageTableItem.swift index 92bce80e35..b496136023 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTableItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTableItem.swift @@ -200,12 +200,12 @@ final class InstantPageTableItem: InstantPageScrollableItem { return false } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { var additionalNodes: [InstantPageNode] = [] for cell in self.cells { for item in cell.additionalItems { if item.wantsNode { - if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, openMedia: { _ in }, longPressMedia: { _ in }, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) { + if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourcePeerType: sourcePeerType, openMedia: { _ in }, longPressMedia: { _ in }, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) { node.frame = item.frame.offsetBy(dx: cell.frame.minX, dy: cell.frame.minY) additionalNodes.append(node) } diff --git a/submodules/InstantPageUI/Sources/InstantPageTextItem.swift b/submodules/InstantPageUI/Sources/InstantPageTextItem.swift index bf828c6898..4dfdfc0d67 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTextItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTextItem.swift @@ -342,7 +342,7 @@ final class InstantPageTextItem: InstantPageItem { return false } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return nil } @@ -391,11 +391,11 @@ final class InstantPageScrollableTextItem: InstantPageScrollableItem { context.restoreGState() } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { var additionalNodes: [InstantPageNode] = [] for item in additionalItems { if item.wantsNode { - if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, openMedia: { _ in }, longPressMedia: { _ in }, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) { + if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourcePeerType: sourcePeerType, openMedia: { _ in }, longPressMedia: { _ in }, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) { node.frame = item.frame additionalNodes.append(node) } diff --git a/submodules/InstantPageUI/Sources/InstantPageTheme.swift b/submodules/InstantPageUI/Sources/InstantPageTheme.swift index 495378975b..a8d6c925b9 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTheme.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTheme.swift @@ -277,7 +277,7 @@ private func fontSizeMultiplierForVariant(_ variant: InstantPagePresentationFont } } -func instantPageThemeTypeForSettingsAndTime(themeSettings: PresentationThemeSettings?, settings: InstantPagePresentationSettings, time: Date?) -> (InstantPageThemeType, Bool) { +func instantPageThemeTypeForSettingsAndTime(themeSettings: PresentationThemeSettings?, settings: InstantPagePresentationSettings, time: Date?, forceDarkTheme: Bool) -> (InstantPageThemeType, Bool) { if settings.autoNightMode { switch settings.themeType { case .light, .sepia, .gray: @@ -288,7 +288,7 @@ func instantPageThemeTypeForSettingsAndTime(themeSettings: PresentationThemeSett if case .explicitNone = themeSettings.automaticThemeSwitchSetting.trigger { } else { fallback = false - useDarkTheme = automaticThemeShouldSwitchNow(settings: themeSettings.automaticThemeSwitchSetting, systemUserInterfaceStyle: .light) + useDarkTheme = forceDarkTheme } } if fallback, let time = time { @@ -323,7 +323,7 @@ func instantPageThemeForType(_ type: InstantPageThemeType, settings: InstantPage extension ActionSheetControllerTheme { convenience init(instantPageTheme: InstantPageTheme) { - self.init(dimColor: UIColor(white: 0.0, alpha: 0.4), backgroundType: instantPageTheme.type != .dark ? .light : .dark, itemBackgroundColor: instantPageTheme.overlayPanelColor, itemHighlightedBackgroundColor: instantPageTheme.panelHighlightedBackgroundColor, standardActionTextColor: instantPageTheme.panelAccentColor, destructiveActionTextColor: instantPageTheme.panelAccentColor, disabledActionTextColor: instantPageTheme.panelAccentColor, primaryTextColor: instantPageTheme.textCategories.paragraph.color, secondaryTextColor: instantPageTheme.textCategories.caption.color, controlAccentColor: instantPageTheme.panelAccentColor, controlColor: instantPageTheme.tableBorderColor, switchFrameColor: .white, switchContentColor: .white, switchHandleColor: .white) + self.init(dimColor: UIColor(white: 0.0, alpha: 0.4), backgroundType: instantPageTheme.type != .dark ? .light : .dark, itemBackgroundColor: instantPageTheme.overlayPanelColor, itemHighlightedBackgroundColor: instantPageTheme.panelHighlightedBackgroundColor, standardActionTextColor: instantPageTheme.panelAccentColor, destructiveActionTextColor: instantPageTheme.panelAccentColor, disabledActionTextColor: instantPageTheme.panelAccentColor, primaryTextColor: instantPageTheme.textCategories.paragraph.color, secondaryTextColor: instantPageTheme.textCategories.caption.color, controlAccentColor: instantPageTheme.panelAccentColor, controlColor: instantPageTheme.tableBorderColor, switchFrameColor: .white, switchContentColor: .white, switchHandleColor: .white, baseFontSize: 17.0) } } diff --git a/submodules/InstantPageUI/Sources/InstantPageWebEmbedItem.swift b/submodules/InstantPageUI/Sources/InstantPageWebEmbedItem.swift index de516b6e37..f432f660ba 100644 --- a/submodules/InstantPageUI/Sources/InstantPageWebEmbedItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageWebEmbedItem.swift @@ -25,7 +25,7 @@ final class InstantPageWebEmbedItem: InstantPageItem { self.enableScrolling = enableScrolling } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? { return InstantPageWebEmbedNode(frame: self.frame, url: self.url, html: self.html, enableScrolling: self.enableScrolling, updateWebEmbedHeight: updateWebEmbedHeight) } diff --git a/submodules/InstantPageUI/Sources/InstantPageWebEmbedNode.swift b/submodules/InstantPageUI/Sources/InstantPageWebEmbedNode.swift index b6e0575550..31de3d5bdd 100644 --- a/submodules/InstantPageUI/Sources/InstantPageWebEmbedNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageWebEmbedNode.swift @@ -113,7 +113,7 @@ final class InstantPageWebEmbedNode: ASDisplayNode, InstantPageNode { self.webView?.frame = self.bounds } - func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil } diff --git a/submodules/ItemListAddressItem/Sources/ItemListAddressItem.swift b/submodules/ItemListAddressItem/Sources/ItemListAddressItem.swift index 9ad3ec1d6d..929a0d82ca 100644 --- a/submodules/ItemListAddressItem/Sources/ItemListAddressItem.swift +++ b/submodules/ItemListAddressItem/Sources/ItemListAddressItem.swift @@ -18,13 +18,14 @@ public final class ItemListAddressItem: ListViewItem, ItemListItem { let selected: Bool? public let sectionId: ItemListSectionId let style: ItemListStyle + let displayDecorations: Bool let action: (() -> Void)? let longTapAction: (() -> Void)? let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? public let tag: Any? - public init(theme: PresentationTheme, label: String, text: String, imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?, selected: Bool? = nil, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) { + public init(theme: PresentationTheme, label: String, text: String, imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?, selected: Bool? = nil, sectionId: ItemListSectionId, style: ItemListStyle, displayDecorations: Bool = true, action: (() -> Void)?, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) { self.theme = theme self.label = label self.text = text @@ -32,6 +33,7 @@ public final class ItemListAddressItem: ListViewItem, ItemListItem { self.selected = selected self.sectionId = sectionId self.style = style + self.displayDecorations = displayDecorations self.action = action self.longTapAction = longTapAction self.linkItemAction = linkItemAction @@ -157,7 +159,7 @@ public class ItemListAddressItemNode: ListViewItemNode { updatedTheme = item.theme } - let insets: UIEdgeInsets + var insets: UIEdgeInsets let leftInset: CGFloat = 16.0 + params.leftInset let rightInset: CGFloat = 8.0 + params.rightInset let separatorHeight = UIScreenPixel @@ -175,6 +177,10 @@ public class ItemListAddressItemNode: ListViewItemNode { insets = itemListNeighborsGroupedInsets(neighbors) } + if !item.displayDecorations { + insets = UIEdgeInsets() + } + var leftOffset: CGFloat = 0.0 var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)? if let selected = item.selected { @@ -226,6 +232,11 @@ public class ItemListAddressItemNode: ListViewItemNode { strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor } + strongSelf.topStripeNode.isHidden = !item.displayDecorations + strongSelf.bottomStripeNode.isHidden = !item.displayDecorations + strongSelf.backgroundNode.isHidden = !item.displayDecorations + strongSelf.highlightedBackgroundNode.isHidden = !item.displayDecorations + let _ = labelApply() let _ = textApply() let _ = imageApply() @@ -293,7 +304,7 @@ public class ItemListAddressItemNode: ListViewItemNode { case .sameSection(false): strongSelf.topStripeNode.isHidden = true default: - strongSelf.topStripeNode.isHidden = false + strongSelf.topStripeNode.isHidden = !item.displayDecorations } let bottomStripeInset: CGFloat let bottomStripeOffset: CGFloat diff --git a/submodules/ItemListAvatarAndNameInfoItem/BUCK b/submodules/ItemListAvatarAndNameInfoItem/BUCK index 209680328a..6dadb44c97 100644 --- a/submodules/ItemListAvatarAndNameInfoItem/BUCK +++ b/submodules/ItemListAvatarAndNameInfoItem/BUCK @@ -21,6 +21,7 @@ static_library( "//submodules/AppBundle:AppBundle", "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/PhoneNumberFormat:PhoneNumberFormat", + "//submodules/AccountContext:AccountContext", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift b/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift index 1cd6563cae..d0cebfc0fe 100644 --- a/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift +++ b/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift @@ -15,6 +15,7 @@ import TelegramStringFormatting import PeerPresenceStatusManager import AppBundle import PhoneNumberFormat +import AccountContext private let updatingAvatarOverlayImage = generateFilledCircleImage(diameter: 66.0, color: UIColor(white: 0.0, alpha: 0.4), backgroundColor: nil) @@ -138,9 +139,8 @@ public enum ItemListAvatarAndNameInfoItemMode { } public class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem { - let account: Account - let theme: PresentationTheme - let strings: PresentationStrings + let accountContext: AccountContext + let presentationData: ItemListPresentationData let dateTimeFormat: PresentationDateTimeFormat let mode: ItemListAvatarAndNameInfoItemMode let peer: Peer? @@ -162,10 +162,9 @@ public class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem { public let selectable: Bool - public init(account: Account, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, mode: ItemListAvatarAndNameInfoItemMode, peer: Peer?, presence: PeerPresence?, label: String? = nil, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, sectionId: ItemListSectionId, style: ItemListAvatarAndNameInfoItemStyle, editingNameUpdated: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, editingNameCompleted: @escaping () -> Void = {}, avatarTapped: @escaping () -> Void, context: ItemListAvatarAndNameInfoItemContext? = nil, updatingImage: ItemListAvatarAndNameInfoItemUpdatingAvatar? = nil, call: (() -> Void)? = nil, action: (() -> Void)? = nil, longTapAction: (() -> Void)? = nil, tag: ItemListItemTag? = nil) { - self.account = account - self.theme = theme - self.strings = strings + public init(accountContext: AccountContext, presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, mode: ItemListAvatarAndNameInfoItemMode, peer: Peer?, presence: PeerPresence?, label: String? = nil, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, sectionId: ItemListSectionId, style: ItemListAvatarAndNameInfoItemStyle, editingNameUpdated: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, editingNameCompleted: @escaping () -> Void = {}, avatarTapped: @escaping () -> Void, context: ItemListAvatarAndNameInfoItemContext? = nil, updatingImage: ItemListAvatarAndNameInfoItemUpdatingAvatar? = nil, call: (() -> Void)? = nil, action: (() -> Void)? = nil, longTapAction: (() -> Void)? = nil, tag: ItemListItemTag? = nil) { + self.accountContext = accountContext + self.presentationData = presentationData self.dateTimeFormat = dateTimeFormat self.mode = mode self.peer = peer @@ -234,8 +233,6 @@ public class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem { } private let avatarFont = avatarPlaceholderFont(size: 28.0) -private let nameFont = Font.medium(19.0) -private let statusFont = Font.regular(15.0) public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, ItemListItemFocusableNode, UITextFieldDelegate { private let backgroundNode: ASDisplayNode @@ -362,20 +359,23 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo return { item, params, neighbors in let baseWidth = params.width - params.leftInset - params.rightInset + let nameFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 19.0 / 17.0)) + let statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } var credibilityIconImage: UIImage? var credibilityIconOffset: CGFloat = 4.0 if let peer = item.peer { if peer.isScam { - credibilityIconImage = PresentationResourcesChatList.scamIcon(item.theme, type: .regular) + credibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, type: .regular) credibilityIconOffset = 6.0 } else if peer.isVerified { - credibilityIconImage = PresentationResourcesItemList.verifiedPeerIcon(item.theme) + credibilityIconImage = PresentationResourcesItemList.verifiedPeerIcon(item.presentationData.theme) } } @@ -398,7 +398,7 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo nameMaximumNumberOfLines = 2 } - let (nameNodeLayout, nameNodeApply) = layoutNameNode(TextNodeLayoutArguments(attributedString: NSAttributedString(string: displayTitle.composedDisplayTitle(strings: item.strings), font: nameFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: nameMaximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: baseWidth - 20 - 94.0 - (item.call != nil ? 36.0 : 0.0) - additionalTitleInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (nameNodeLayout, nameNodeApply) = layoutNameNode(TextNodeLayoutArguments(attributedString: NSAttributedString(string: displayTitle.composedDisplayTitle(strings: item.presentationData.strings), font: nameFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: nameMaximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: baseWidth - 20 - 94.0 - (item.call != nil ? 36.0 : 0.0) - additionalTitleInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var statusText: String = "" let statusColor: UIColor @@ -415,64 +415,64 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo } statusText += "@\(username)" } - statusColor = item.theme.list.itemSecondaryTextColor + statusColor = item.presentationData.theme.list.itemSecondaryTextColor case .generic, .contact, .editSettings: if let label = item.label { statusText = label - statusColor = item.theme.list.itemSecondaryTextColor + statusColor = item.presentationData.theme.list.itemSecondaryTextColor } else if peer.flags.contains(.isSupport), !servicePeer { - statusText = item.strings.Bot_GenericSupportStatus - statusColor = item.theme.list.itemSecondaryTextColor + statusText = item.presentationData.strings.Bot_GenericSupportStatus + statusColor = item.presentationData.theme.list.itemSecondaryTextColor } else if let _ = peer.botInfo { - statusText = item.strings.Bot_GenericBotStatus - statusColor = item.theme.list.itemSecondaryTextColor + statusText = item.presentationData.strings.Bot_GenericBotStatus + statusColor = item.presentationData.theme.list.itemSecondaryTextColor } else if case .generic = item.mode, !servicePeer { let presence = (item.presence as? TelegramUserPresence) ?? TelegramUserPresence(status: .none, lastActivity: 0) let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, dateTimeFormat: item.dateTimeFormat, presence: presence, relativeTo: Int32(timestamp), expanded: true) + let (string, activity) = stringAndActivityForUserPresence(strings: item.presentationData.strings, dateTimeFormat: item.dateTimeFormat, presence: presence, relativeTo: Int32(timestamp), expanded: true) statusText = string if activity { - statusColor = item.theme.list.itemAccentColor + statusColor = item.presentationData.theme.list.itemAccentColor } else { - statusColor = item.theme.list.itemSecondaryTextColor + statusColor = item.presentationData.theme.list.itemSecondaryTextColor } } else { statusText = "" - statusColor = item.theme.list.itemPrimaryTextColor + statusColor = item.presentationData.theme.list.itemPrimaryTextColor } } } else if let channel = item.peer as? TelegramChannel { if let cachedChannelData = item.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { if case .group = channel.info { if memberCount == 0 { - statusText = item.strings.Group_Status + statusText = item.presentationData.strings.Group_Status } else { - statusText = item.strings.Conversation_StatusMembers(memberCount) + statusText = item.presentationData.strings.Conversation_StatusMembers(memberCount) } } else { if memberCount == 0 { - statusText = item.strings.Channel_Status + statusText = item.presentationData.strings.Channel_Status } else { - statusText = item.strings.Conversation_StatusSubscribers(memberCount) + statusText = item.presentationData.strings.Conversation_StatusSubscribers(memberCount) } } - statusColor = item.theme.list.itemSecondaryTextColor + statusColor = item.presentationData.theme.list.itemSecondaryTextColor } else { switch channel.info { case .broadcast: - statusText = item.strings.Channel_Status - statusColor = item.theme.list.itemSecondaryTextColor + statusText = item.presentationData.strings.Channel_Status + statusColor = item.presentationData.theme.list.itemSecondaryTextColor case .group: - statusText = item.strings.Group_Status - statusColor = item.theme.list.itemSecondaryTextColor + statusText = item.presentationData.strings.Group_Status + statusColor = item.presentationData.theme.list.itemSecondaryTextColor } } } else if let group = item.peer as? TelegramGroup { - statusText = item.strings.GroupInfo_ParticipantCount(Int32(group.participantCount)) - statusColor = item.theme.list.itemSecondaryTextColor + statusText = item.presentationData.strings.GroupInfo_ParticipantCount(Int32(group.participantCount)) + statusColor = item.presentationData.theme.list.itemSecondaryTextColor } else { statusText = "" - statusColor = item.theme.list.itemPrimaryTextColor + statusColor = item.presentationData.theme.list.itemPrimaryTextColor } var availableStatusWidth = baseWidth - 20 @@ -484,36 +484,42 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo let separatorHeight = UIScreenPixel + let nameSpacing: CGFloat = 3.0 + let hasCorners = itemListHasRoundedBlockLayout(params) let contentSize: CGSize var insets: UIEdgeInsets let itemBackgroundColor: UIColor let itemSeparatorColor: UIColor switch item.style { - case .plain: - itemBackgroundColor = item.theme.list.plainBackgroundColor - itemSeparatorColor = item.theme.list.itemPlainSeparatorColor - contentSize = CGSize(width: params.width, height: 96.0) - insets = itemListNeighborsPlainInsets(neighbors) - case let .blocks(withTopInset, withExtendedBottomInset): - itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor - itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor - contentSize = CGSize(width: params.width, height: 92.0) - if withTopInset || hasCorners { - insets = itemListNeighborsGroupedInsets(neighbors) - } else { - let topInset: CGFloat - switch neighbors.top { - case .sameSection, .none: - topInset = 0.0 - case .otherSection: - topInset = separatorHeight + 35.0 - } - insets = UIEdgeInsets(top: topInset, left: 0.0, bottom: separatorHeight, right: 0.0) - if withExtendedBottomInset { - insets.bottom += 12.0 - } + case .plain: + let verticalInset: CGFloat = 15.0 + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + let baseHeight = nameNodeLayout.size.height + nameSpacing + statusNodeLayout.size.height + 40.0 + contentSize = CGSize(width: params.width, height: max(baseHeight, verticalInset * 2.0 + 66.0)) + insets = itemListNeighborsPlainInsets(neighbors) + case let .blocks(withTopInset, withExtendedBottomInset): + let verticalInset: CGFloat = 13.0 + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + let baseHeight = nameNodeLayout.size.height + nameSpacing + statusNodeLayout.size.height + 30.0 + contentSize = CGSize(width: params.width, height: max(baseHeight, verticalInset * 2.0 + 66.0)) + if withTopInset || hasCorners { + insets = itemListNeighborsGroupedInsets(neighbors) + } else { + let topInset: CGFloat + switch neighbors.top { + case .sameSection, .none: + topInset = 0.0 + case .otherSection: + topInset = separatorHeight + 35.0 } + insets = UIEdgeInsets(top: topInset, left: 0.0, bottom: separatorHeight, right: 0.0) + if withExtendedBottomInset { + insets.bottom += 12.0 + } + } } var updateAvatarOverlayImage: UIImage? @@ -538,15 +544,15 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo strongSelf.topStripeNode.backgroundColor = itemSeparatorColor strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor strongSelf.inputSeparator?.backgroundColor = itemSeparatorColor - strongSelf.callButton.setImage(PresentationResourcesChat.chatInfoCallButtonImage(item.theme), for: []) + strongSelf.callButton.setImage(PresentationResourcesChat.chatInfoCallButtonImage(item.presentationData.theme), for: []) - strongSelf.inputFirstClearButton?.setImage(generateClearIcon(color: item.theme.list.inputClearButtonColor), for: []) - strongSelf.inputSecondClearButton?.setImage(generateClearIcon(color: item.theme.list.inputClearButtonColor), for: []) + strongSelf.inputFirstClearButton?.setImage(generateClearIcon(color: item.presentationData.theme.list.inputClearButtonColor), for: []) + strongSelf.inputSecondClearButton?.setImage(generateClearIcon(color: item.presentationData.theme.list.inputClearButtonColor), for: []) - updatedArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.theme) + updatedArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) } if item.updatingImage != nil { @@ -584,11 +590,8 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo strongSelf.callButton.removeFromSupernode() } - let avatarOriginY: CGFloat switch item.style { case .plain: - avatarOriginY = 15.0 - if strongSelf.backgroundNode.supernode != nil { strongSelf.backgroundNode.removeFromSupernode() } @@ -602,8 +605,6 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo strongSelf.maskNode.removeFromSupernode() } case .blocks: - avatarOriginY = 13.0 - if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) } @@ -637,7 +638,7 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height)) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) @@ -668,10 +669,10 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo overrideImage = .deletedIcon } - strongSelf.avatarNode.setPeer(account: item.account, theme: item.theme, peer: peer, overrideImage: overrideImage, emptyColor: ignoreEmpty ? nil : item.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads) + strongSelf.avatarNode.setPeer(context: item.accountContext, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: ignoreEmpty ? nil : item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads) } - let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: avatarOriginY), size: CGSize(width: 66.0, height: 66.0)) + let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: floor((layout.contentSize.height - 66.0) / 2.0)), size: CGSize(width: 66.0, height: 66.0)) strongSelf.avatarNode.frame = avatarFrame strongSelf.updatingAvatarOverlay.frame = avatarFrame @@ -711,7 +712,7 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo if strongSelf.inputSeparator == nil { animateIn = true } - let keyboardAppearance = item.theme.rootController.keyboardColor.keyboardAppearance + let keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance switch editingName { case let .personName(firstName, lastName, _): if strongSelf.inputSeparator == nil { @@ -725,10 +726,10 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo if strongSelf.inputFirstField == nil { let inputFirstField = TextFieldNodeView() inputFirstField.delegate = self - inputFirstField.font = Font.regular(17.0) + inputFirstField.font = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) inputFirstField.autocorrectionType = .no inputFirstField.returnKeyType = .next - inputFirstField.attributedText = NSAttributedString(string: firstName, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor) + inputFirstField.attributedText = NSAttributedString(string: firstName, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPrimaryTextColor) strongSelf.inputFirstField = inputFirstField strongSelf.view.addSubview(inputFirstField) inputFirstField.addTarget(self, action: #selector(strongSelf.textFieldDidChange(_:)), for: .editingChanged) @@ -736,8 +737,8 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo strongSelf.inputFirstField?.text = firstName } - strongSelf.inputFirstField?.textColor = item.theme.list.itemPrimaryTextColor - strongSelf.inputFirstField?.attributedPlaceholder = NSAttributedString(string: item.strings.UserInfo_FirstNamePlaceholder, font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor) + strongSelf.inputFirstField?.textColor = item.presentationData.theme.list.itemPrimaryTextColor + strongSelf.inputFirstField?.attributedPlaceholder = NSAttributedString(string: item.presentationData.strings.UserInfo_FirstNamePlaceholder, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPlaceholderTextColor) if strongSelf.inputFirstField?.keyboardAppearance != keyboardAppearance { strongSelf.inputFirstField?.keyboardAppearance = keyboardAppearance } @@ -747,7 +748,7 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo strongSelf.inputFirstClearButton?.imageNode.displaysAsynchronously = false strongSelf.inputFirstClearButton?.imageNode.displayWithoutProcessing = true strongSelf.inputFirstClearButton?.displaysAsynchronously = false - strongSelf.inputFirstClearButton?.setImage(generateClearIcon(color: item.theme.list.inputClearButtonColor), for: []) + strongSelf.inputFirstClearButton?.setImage(generateClearIcon(color: item.presentationData.theme.list.inputClearButtonColor), for: []) strongSelf.inputFirstClearButton?.addTarget(strongSelf, action: #selector(strongSelf.firstClearPressed), forControlEvents: .touchUpInside) strongSelf.inputFirstClearButton?.isHidden = true strongSelf.addSubnode(strongSelf.inputFirstClearButton!) @@ -756,10 +757,10 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo if strongSelf.inputSecondField == nil { let inputSecondField = TextFieldNodeView() inputSecondField.delegate = self - inputSecondField.font = Font.regular(17.0) + inputSecondField.font = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) inputSecondField.autocorrectionType = .no inputSecondField.returnKeyType = .done - inputSecondField.attributedText = NSAttributedString(string: lastName, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor) + inputSecondField.attributedText = NSAttributedString(string: lastName, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPrimaryTextColor) strongSelf.inputSecondField = inputSecondField strongSelf.view.addSubview(inputSecondField) inputSecondField.addTarget(self, action: #selector(strongSelf.textFieldDidChange(_:)), for: .editingChanged) @@ -767,8 +768,8 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo strongSelf.inputSecondField?.text = lastName } - strongSelf.inputSecondField?.textColor = item.theme.list.itemPrimaryTextColor - strongSelf.inputSecondField?.attributedPlaceholder = NSAttributedString(string: item.strings.UserInfo_LastNamePlaceholder, font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor) + strongSelf.inputSecondField?.textColor = item.presentationData.theme.list.itemPrimaryTextColor + strongSelf.inputSecondField?.attributedPlaceholder = NSAttributedString(string: item.presentationData.strings.UserInfo_LastNamePlaceholder, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPlaceholderTextColor) if strongSelf.inputSecondField?.keyboardAppearance != keyboardAppearance { strongSelf.inputSecondField?.keyboardAppearance = keyboardAppearance } @@ -778,7 +779,7 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo strongSelf.inputSecondClearButton?.imageNode.displaysAsynchronously = false strongSelf.inputSecondClearButton?.imageNode.displayWithoutProcessing = true strongSelf.inputSecondClearButton?.displaysAsynchronously = false - strongSelf.inputSecondClearButton?.setImage(generateClearIcon(color: item.theme.list.inputClearButtonColor), for: []) + strongSelf.inputSecondClearButton?.setImage(generateClearIcon(color: item.presentationData.theme.list.inputClearButtonColor), for: []) strongSelf.inputSecondClearButton?.addTarget(strongSelf, action: #selector(strongSelf.secondClearPressed), forControlEvents: .touchUpInside) strongSelf.inputSecondClearButton?.isHidden = true strongSelf.addSubnode(strongSelf.inputSecondClearButton!) @@ -807,29 +808,29 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo strongSelf.addSubnode(inputSeparator) strongSelf.inputSeparator = inputSeparator } - strongSelf.inputSeparator?.backgroundColor = itemSeparatorColor + strongSelf.inputSeparator?.backgroundColor = .clear if strongSelf.inputFirstField == nil { let inputFirstField = TextFieldNodeView() inputFirstField.delegate = self - inputFirstField.font = Font.regular(17.0) + inputFirstField.font = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 19.0 / 17.0)) inputFirstField.autocorrectionType = .no - inputFirstField.attributedText = NSAttributedString(string: title, font: Font.regular(19.0), textColor: item.theme.list.itemPrimaryTextColor) + inputFirstField.attributedText = NSAttributedString(string: title, font: Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 19.0 / 17.0)), textColor: item.presentationData.theme.list.itemPrimaryTextColor) strongSelf.inputFirstField = inputFirstField strongSelf.view.addSubview(inputFirstField) inputFirstField.addTarget(self, action: #selector(strongSelf.textFieldDidChange(_:)), for: .editingChanged) } else if strongSelf.inputFirstField?.text != title { strongSelf.inputFirstField?.text = title } - strongSelf.inputFirstField?.textColor = item.theme.list.itemPrimaryTextColor + strongSelf.inputFirstField?.textColor = item.presentationData.theme.list.itemPrimaryTextColor let placeholder: String switch type { case .group: - placeholder = item.strings.GroupInfo_GroupNamePlaceholder + placeholder = item.presentationData.strings.GroupInfo_GroupNamePlaceholder case .channel: - placeholder = item.strings.GroupInfo_ChannelListNamePlaceholder + placeholder = item.presentationData.strings.GroupInfo_ChannelListNamePlaceholder } - strongSelf.inputFirstField?.attributedPlaceholder = NSAttributedString(string: placeholder, font: Font.regular(19.0), textColor: item.theme.list.itemPlaceholderTextColor) + strongSelf.inputFirstField?.attributedPlaceholder = NSAttributedString(string: placeholder, font: Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 19.0 / 17.0)), textColor: item.presentationData.theme.list.itemPlaceholderTextColor) if strongSelf.inputFirstField?.keyboardAppearance != keyboardAppearance { strongSelf.inputFirstField?.keyboardAppearance = keyboardAppearance } @@ -839,14 +840,14 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo strongSelf.inputFirstClearButton?.imageNode.displaysAsynchronously = false strongSelf.inputFirstClearButton?.imageNode.displayWithoutProcessing = true strongSelf.inputFirstClearButton?.displaysAsynchronously = false - strongSelf.inputFirstClearButton?.setImage(generateClearIcon(color: item.theme.list.inputClearButtonColor), for: []) + strongSelf.inputFirstClearButton?.setImage(generateClearIcon(color: item.presentationData.theme.list.inputClearButtonColor), for: []) strongSelf.inputFirstClearButton?.addTarget(strongSelf, action: #selector(strongSelf.firstClearPressed), forControlEvents: .touchUpInside) strongSelf.inputFirstClearButton?.isHidden = true strongSelf.addSubnode(strongSelf.inputFirstClearButton!) } - strongSelf.inputSeparator?.frame = CGRect(origin: CGPoint(x: params.leftInset + 100.0, y: 62.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 100.0, height: separatorHeight)) - strongSelf.inputFirstField?.frame = CGRect(origin: CGPoint(x: params.leftInset + 111.0, y: 26.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 111.0 - 36.0, height: 35.0)) + strongSelf.inputSeparator?.frame = CGRect(origin: CGPoint(x: params.leftInset + 100.0, y: 64.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 100.0, height: separatorHeight)) + strongSelf.inputFirstField?.frame = CGRect(origin: CGPoint(x: params.leftInset + 111.0, y: 28.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 111.0 - 36.0, height: 35.0)) if let image = strongSelf.inputFirstClearButton?.image(for: []), let inputFieldFrame = strongSelf.inputFirstField?.frame { strongSelf.inputFirstClearButton?.frame = CGRect(origin: CGPoint(x: inputFieldFrame.maxX, y: inputFieldFrame.minY + floor((inputFieldFrame.size.height - image.size.height) / 2.0) - 1.0 + UIScreenPixel), size: image.size) @@ -1077,9 +1078,9 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo } } - public func avatarTransitionNode() -> ((ASDisplayNode, () -> (UIView?, UIView?)), CGRect) { + public func avatarTransitionNode() -> ((ASDisplayNode, CGRect, () -> (UIView?, UIView?)), CGRect) { let avatarNode = self.avatarNode - return ((self.avatarNode, { [weak avatarNode] in + return ((self.avatarNode, self.avatarNode.bounds, { [weak avatarNode] in return (avatarNode?.view.snapshotContentTree(unhide: true), nil) }), self.avatarNode.bounds) } diff --git a/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift b/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift index 96ccdaf7dd..b9da7f99eb 100644 --- a/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift +++ b/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift @@ -12,23 +12,30 @@ public enum ItemListPeerActionItemHeight { case peerList } +public enum ItemListPeerActionItemColor { + case accent + case destructive +} + public class ItemListPeerActionItem: ListViewItem, ItemListItem { - let theme: PresentationTheme + let presentationData: ItemListPresentationData let icon: UIImage? let title: String public let alwaysPlain: Bool let editing: Bool let height: ItemListPeerActionItemHeight + let color: ItemListPeerActionItemColor public let sectionId: ItemListSectionId - let action: () -> Void + let action: (() -> Void)? - public init(theme: PresentationTheme, icon: UIImage?, title: String, alwaysPlain: Bool = false, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, editing: Bool, action: @escaping () -> Void) { - self.theme = theme + public init(presentationData: ItemListPresentationData, icon: UIImage?, title: String, alwaysPlain: Bool = false, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, editing: Bool, action: (() -> Void)?) { + self.presentationData = presentationData self.icon = icon self.title = title self.alwaysPlain = alwaysPlain self.editing = editing self.height = height + self.color = color self.sectionId = sectionId self.action = action } @@ -79,16 +86,16 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem { } } - public var selectable: Bool = true + public var selectable: Bool { + return self.action != nil + } public func selected(listView: ListView){ listView.clearHighlightAnimated(true) - self.action() + self.action?() } } -private let titleFont = Font.regular(17.0) - class ItemListPeerActionItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -146,31 +153,41 @@ class ItemListPeerActionItemNode: ListViewItemNode { return { item, params, neighbors in var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } let leftInset: CGFloat - let height: CGFloat + let verticalInset: CGFloat let verticalOffset: CGFloat switch item.height { case .generic: - height = 44.0 - verticalOffset = -3.0 + verticalInset = 11.0 + verticalOffset = 0.0 leftInset = 59.0 + params.leftInset case .peerList: - height = 50.0 + verticalInset = 14.0 verticalOffset = 0.0 leftInset = 65.0 + params.leftInset } let editingOffset: CGFloat = (item.editing ? 38.0 : 0.0) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - editingOffset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let textColor: UIColor + switch item.color { + case .accent: + textColor = item.presentationData.theme.list.itemAccentColor + case .destructive: + textColor = item.presentationData.theme.list.itemDestructiveColor + } + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - editingOffset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let separatorHeight = UIScreenPixel let insets = itemListNeighborsGroupedInsets(neighbors) - let contentSize = CGSize(width: params.width, height: height) + let contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset * 2.0) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size @@ -183,10 +200,10 @@ class ItemListPeerActionItemNode: ListViewItemNode { strongSelf.activateArea.accessibilityLabel = item.title if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } let _ = titleApply() @@ -240,16 +257,16 @@ class ItemListPeerActionItemNode: ListViewItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) - transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset, y: 14.0 + verticalOffset), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset, y: verticalInset + verticalOffset), size: titleLayout.size)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: height + UIScreenPixel + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)) } }) } diff --git a/submodules/ItemListPeerItem/BUCK b/submodules/ItemListPeerItem/BUCK index 8b7c78c2d3..ef054e0bec 100644 --- a/submodules/ItemListPeerItem/BUCK +++ b/submodules/ItemListPeerItem/BUCK @@ -20,6 +20,7 @@ static_library( "//submodules/TelegramUIPreferences:TelegramUIPreferences", "//submodules/ContextUI:ContextUI", "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/AccountContext:AccountContext", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 015efd1e7b..8c8fcf6b0f 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -14,13 +14,203 @@ import AvatarNode import TelegramStringFormatting import PeerPresenceStatusManager import ContextUI +import AccountContext + +private final class ShimmerEffectNode: ASDisplayNode { + private var currentBackgroundColor: UIColor? + private var currentForegroundColor: UIColor? + private let imageNodeContainer: ASDisplayNode + private let imageNode: ASImageNode + + private var absoluteLocation: (CGRect, CGSize)? + private var isCurrentlyInHierarchy = false + private var shouldBeAnimating = false + + override init() { + self.imageNodeContainer = ASDisplayNode() + self.imageNodeContainer.isLayerBacked = true + + self.imageNode = ASImageNode() + self.imageNode.isLayerBacked = true + self.imageNode.displaysAsynchronously = false + self.imageNode.displayWithoutProcessing = true + self.imageNode.contentMode = .scaleToFill + + super.init() + + self.isLayerBacked = true + self.clipsToBounds = true + + self.imageNodeContainer.addSubnode(self.imageNode) + self.addSubnode(self.imageNodeContainer) + } + + override func didEnterHierarchy() { + super.didEnterHierarchy() + + self.isCurrentlyInHierarchy = true + self.updateAnimation() + } + + override func didExitHierarchy() { + super.didExitHierarchy() + + self.isCurrentlyInHierarchy = false + self.updateAnimation() + } + + func update(backgroundColor: UIColor, foregroundColor: UIColor) { + if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) { + return + } + self.currentBackgroundColor = backgroundColor + self.currentForegroundColor = foregroundColor + + self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + context.clip(to: CGRect(origin: CGPoint(), size: size)) + + let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor + let peakColor = foregroundColor.cgColor + + var locations: [CGFloat] = [0.0, 0.5, 1.0] + let colors: [CGColor] = [transparentColor, peakColor, transparentColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + } + + func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize { + return + } + let sizeUpdated = self.absoluteLocation?.1 != containerSize + let frameUpdated = self.absoluteLocation?.0 != rect + self.absoluteLocation = (rect, containerSize) + + if sizeUpdated { + if self.shouldBeAnimating { + self.imageNode.layer.removeAnimation(forKey: "shimmer") + self.addImageAnimation() + } + } + + if frameUpdated { + self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize) + } + } + + private func updateAnimation() { + let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil + if shouldBeAnimating != self.shouldBeAnimating { + self.shouldBeAnimating = shouldBeAnimating + if shouldBeAnimating { + self.addImageAnimation() + } else { + self.imageNode.layer.removeAnimation(forKey: "shimmer") + } + } + } + + private func addImageAnimation() { + guard let containerSize = self.absoluteLocation?.1 else { + return + } + let gradientHeight: CGFloat = 250.0 + self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight)) + let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = Float.infinity + animation.beginTime = 1.0 + self.imageNode.layer.add(animation, forKey: "shimmer") + } +} + +private final class LoadingShimmerNode: ASDisplayNode { + enum Shape: Equatable { + case circle(CGRect) + case roundedRectLine(startPoint: CGPoint, width: CGFloat, diameter: CGFloat) + } + + private let backgroundNode: ASDisplayNode + private let effectNode: ShimmerEffectNode + private let foregroundNode: ASImageNode + + private var currentShapes: [Shape] = [] + private var currentBackgroundColor: UIColor? + private var currentForegroundColor: UIColor? + private var currentShimmeringColor: UIColor? + private var currentSize = CGSize() + + override init() { + self.backgroundNode = ASDisplayNode() + + self.effectNode = ShimmerEffectNode() + + self.foregroundNode = ASImageNode() + self.foregroundNode.displaysAsynchronously = false + self.foregroundNode.displayWithoutProcessing = true + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.effectNode) + self.addSubnode(self.foregroundNode) + } + + func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.effectNode.updateAbsoluteRect(rect, within: containerSize) + } + + func update(backgroundColor: UIColor, foregroundColor: UIColor, shimmeringColor: UIColor, shapes: [Shape], size: CGSize) { + if self.currentShapes == shapes, let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor), let currentShimmeringColor = self.currentShimmeringColor, currentShimmeringColor.isEqual(shimmeringColor), self.currentSize == size { + return + } + + self.currentBackgroundColor = backgroundColor + self.currentForegroundColor = foregroundColor + self.currentShimmeringColor = shimmeringColor + self.currentShapes = shapes + self.currentSize = size + + self.backgroundNode.backgroundColor = foregroundColor + + self.effectNode.update(backgroundColor: foregroundColor, foregroundColor: shimmeringColor) + + self.foregroundNode.image = generateImage(size, rotatedContext: { size, context in + context.setFillColor(backgroundColor.cgColor) + context.setBlendMode(.copy) + context.fill(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(UIColor.clear.cgColor) + for shape in shapes { + switch shape { + case let .circle(frame): + context.fillEllipse(in: frame) + case let .roundedRectLine(startPoint, width, diameter): + context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter))) + context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter))) + } + } + }) + + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) + self.foregroundNode.frame = CGRect(origin: CGPoint(), size: size) + self.effectNode.frame = CGRect(origin: CGPoint(), size: size) + } +} public struct ItemListPeerItemEditing: Equatable { public var editable: Bool public var editing: Bool - public var revealed: Bool + public var revealed: Bool? - public init(editable: Bool, editing: Bool, revealed: Bool) { + public init(editable: Bool, editing: Bool, revealed: Bool?) { self.editable = editable self.editing = editing self.revealed = revealed @@ -106,12 +296,19 @@ public struct ItemListPeerItemRevealOptions { } } +public struct ItemListPeerItemShimmering { + public var alternationIndex: Int + + public init(alternationIndex: Int) { + self.alternationIndex = alternationIndex + } +} + public final class ItemListPeerItem: ListViewItem, ItemListItem { - let theme: PresentationTheme - let strings: PresentationStrings + let presentationData: ItemListPresentationData let dateTimeFormat: PresentationDateTimeFormat let nameDisplayOrder: PresentationPersonNameOrder - let account: Account + let context: AccountContext let peer: Peer let height: ItemListPeerItemHeight let aliasHandling: ItemListPeerItemAliasHandling @@ -135,13 +332,15 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { let hasTopGroupInset: Bool let noInsets: Bool public let tag: ItemListItemTag? + let header: ListViewItemHeader? + let shimmering: ItemListPeerItemShimmering? + let displayDecorations: Bool - public init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, account: Account, peer: Peer, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, selectable: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, tag: ItemListItemTag? = nil) { - self.theme = theme - self.strings = strings + public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, selectable: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true) { + self.presentationData = presentationData self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder - self.account = account + self.context = context self.peer = peer self.height = height self.aliasHandling = aliasHandling @@ -165,12 +364,15 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { self.hasTopGroupInset = hasTopGroupInset self.noInsets = noInsets self.tag = tag + self.header = header + self.shimmering = shimmering + self.displayDecorations = displayDecorations } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = ItemListPeerItemNode() - let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), self.getHeaderAtTop(top: previousItem, bottom: nextItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -183,6 +385,19 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { } } + private func getHeaderAtTop(top: ListViewItem?, bottom: ListViewItem?) -> Bool { + var headerAtTop = false + if let top = top as? ItemListPeerItem, top.header != nil { + if top.header?.id != self.header?.id { + headerAtTop = true + } + } else if self.header != nil { + headerAtTop = true + } + + return headerAtTop + } + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { if let nodeValue = node() as? ItemListPeerItemNode { @@ -194,7 +409,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { } async { - let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), self.getHeaderAtTop(top: previousItem, bottom: nextItem)) Queue.mainQueue().async { completion(layout, { _ in apply(false, animated) @@ -211,12 +426,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(17.0) -private let titleBoldFont = Font.medium(17.0) -private let statusFont = Font.regular(14.0) -private let labelFont = Font.regular(13.0) -private let labelDisclosureFont = Font.regular(17.0) -private let avatarFont = avatarPlaceholderFont(size: 15.0) +private let avatarFont = avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0)) private let badgeFont = Font.regular(15.0) public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNode { @@ -238,8 +448,11 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo private var switchNode: SwitchNode? private var checkNode: ASImageNode? + private var shimmerNode: LoadingShimmerNode? + private var absoluteLocation: (CGRect, CGSize)? + private var peerPresenceManager: PeerPresenceStatusManager? - private var layoutParams: (ItemListPeerItem, ListViewItemLayoutParams, ItemListNeighbors)? + private var layoutParams: (ItemListPeerItem, ListViewItemLayoutParams, ItemListNeighbors, Bool)? private var editableControlNode: ItemListEditableControlNode? @@ -309,7 +522,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in if let strongSelf = self, let layoutParams = strongSelf.layoutParams { - let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.1, layoutParams.2) + let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.1, layoutParams.2, layoutParams.3) apply(false, true) } }) @@ -323,7 +536,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } } - public func asyncLayout() -> (_ item: ItemListPeerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { + public func asyncLayout() -> (_ item: ItemListPeerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ headerAtTop: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) @@ -340,21 +553,30 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo let currentHasBadge = self.labelBadgeNode.image != nil - return { item, params, neighbors in + return { item, params, neighbors, headerAtTop in var updateArrowImage: UIImage? var updatedTheme: PresentationTheme? + let statusFontSize: CGFloat = floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0) + let labelFontSize: CGFloat = floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0) + + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + let titleBoldFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize) + let statusFont = Font.regular(statusFontSize) + let labelFont = Font.regular(labelFontSize) + let labelDisclosureFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + var updatedLabelBadgeImage: UIImage? var badgeColor: UIColor? if case .badge = item.label { - badgeColor = item.theme.list.itemAccentColor + badgeColor = item.presentationData.theme.list.itemAccentColor } let badgeDiameter: CGFloat = 20.0 - if currentItem?.theme !== item.theme { - updatedTheme = item.theme - updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.theme) + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) if let badgeColor = badgeColor { updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor) } @@ -376,21 +598,21 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo let textColor: UIColor switch option.type { case .neutral: - color = item.theme.list.itemDisclosureActions.constructive.fillColor - textColor = item.theme.list.itemDisclosureActions.constructive.foregroundColor + color = item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor + textColor = item.presentationData.theme.list.itemDisclosureActions.constructive.foregroundColor case .warning: - color = item.theme.list.itemDisclosureActions.warning.fillColor - textColor = item.theme.list.itemDisclosureActions.warning.foregroundColor + color = item.presentationData.theme.list.itemDisclosureActions.warning.fillColor + textColor = item.presentationData.theme.list.itemDisclosureActions.warning.foregroundColor case .destructive: - color = item.theme.list.itemDisclosureActions.destructive.fillColor - textColor = item.theme.list.itemDisclosureActions.destructive.foregroundColor + color = item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor + textColor = item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor } mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, textColor: textColor)) index += 1 } peerRevealOptions = mappedOptions } else { - peerRevealOptions = [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)] + peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] } } else { peerRevealOptions = [] @@ -402,19 +624,19 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo if let switchValue = item.switchValue { switch switchValue.style { - case .standard: - if currentSwitchNode == nil { - currentSwitchNode = SwitchNode() - } - rightInset += switchSize.width - currentCheckNode = nil - case .check: - checkImage = PresentationResourcesItemList.checkIconImage(item.theme) - if currentCheckNode == nil { - currentCheckNode = ASImageNode() - } - rightInset += 24.0 - currentSwitchNode = nil + case .standard: + if currentSwitchNode == nil { + currentSwitchNode = SwitchNode() + } + rightInset += switchSize.width + currentCheckNode = nil + case .check: + checkImage = PresentationResourcesItemList.checkIconImage(item.presentationData.theme) + if currentCheckNode == nil { + currentCheckNode = ASImageNode() + } + rightInset += 24.0 + currentSwitchNode = nil } } else { currentSwitchNode = nil @@ -423,34 +645,34 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo let titleColor: UIColor switch item.nameColor { - case .primary: - titleColor = item.theme.list.itemPrimaryTextColor - case .secret: - titleColor = item.theme.chatList.secretTitleColor + case .primary: + titleColor = item.presentationData.theme.list.itemPrimaryTextColor + case .secret: + titleColor = item.presentationData.theme.chatList.secretTitleColor } let currentBoldFont: UIFont switch item.nameStyle { - case .distinctBold: - currentBoldFont = titleBoldFont - case .plain: - currentBoldFont = titleFont + case .distinctBold: + currentBoldFont = titleBoldFont + case .plain: + currentBoldFont = titleFont } - if item.peer.id == item.account.peerId, case .threatSelfAsSaved = item.aliasHandling { - titleAttributedString = NSAttributedString(string: item.strings.DialogList_SavedMessages, font: currentBoldFont, textColor: titleColor) + if item.peer.id == item.context.account.peerId, case .threatSelfAsSaved = item.aliasHandling { + titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_SavedMessages, font: currentBoldFont, textColor: titleColor) } else if let user = item.peer as? TelegramUser { if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { let string = NSMutableAttributedString() switch item.nameDisplayOrder { - case .firstLast: - string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor)) - string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor)) - string.append(NSAttributedString(string: lastName, font: currentBoldFont, textColor: titleColor)) - case .lastFirst: - string.append(NSAttributedString(string: lastName, font: currentBoldFont, textColor: titleColor)) - string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor)) - string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor)) + case .firstLast: + string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor)) + string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor)) + string.append(NSAttributedString(string: lastName, font: currentBoldFont, textColor: titleColor)) + case .lastFirst: + string.append(NSAttributedString(string: lastName, font: currentBoldFont, textColor: titleColor)) + string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor)) + string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor)) } titleAttributedString = string } else if let firstName = user.firstName, !firstName.isEmpty { @@ -458,7 +680,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } else if let lastName = user.lastName, !lastName.isEmpty { titleAttributedString = NSAttributedString(string: lastName, font: currentBoldFont, textColor: titleColor) } else { - titleAttributedString = NSAttributedString(string: item.strings.User_DeletedAccount, font: currentBoldFont, textColor: titleColor) + titleAttributedString = NSAttributedString(string: item.presentationData.strings.User_DeletedAccount, font: currentBoldFont, textColor: titleColor) } } else if let group = item.peer as? TelegramGroup { titleAttributedString = NSAttributedString(string: group.title, font: currentBoldFont, textColor: titleColor) @@ -467,52 +689,60 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } switch item.text { - case .presence: - if let user = item.peer as? TelegramUser, let botInfo = user.botInfo { - let botStatus: String - if botInfo.flags.contains(.hasAccessToChatHistory) { - botStatus = item.strings.Bot_GroupStatusReadsHistory - } else { - botStatus = item.strings.Bot_GroupStatusDoesNotReadHistory - } - statusAttributedString = NSAttributedString(string: botStatus, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) - } else if let presence = item.presence as? TelegramUserPresence { - let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, dateTimeFormat: item.dateTimeFormat, presence: presence, relativeTo: Int32(timestamp)) - statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.theme.list.itemAccentColor : item.theme.list.itemSecondaryTextColor) + case .presence: + if let user = item.peer as? TelegramUser, let botInfo = user.botInfo { + let botStatus: String + if botInfo.flags.contains(.hasAccessToChatHistory) { + botStatus = item.presentationData.strings.Bot_GroupStatusReadsHistory } else { - statusAttributedString = NSAttributedString(string: item.strings.LastSeen_Offline, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + botStatus = item.presentationData.strings.Bot_GroupStatusDoesNotReadHistory } - case let .text(text): - statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) - case .none: - break + statusAttributedString = NSAttributedString(string: botStatus, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + } else if let presence = item.presence as? TelegramUserPresence { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + let (string, activity) = stringAndActivityForUserPresence(strings: item.presentationData.strings, dateTimeFormat: item.dateTimeFormat, presence: presence, relativeTo: Int32(timestamp)) + statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor) + } else { + statusAttributedString = NSAttributedString(string: item.presentationData.strings.LastSeen_Offline, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + } + case let .text(text): + statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + case .none: + break } let leftInset: CGFloat - let height: CGFloat + let verticalInset: CGFloat let verticalOffset: CGFloat let avatarSize: CGFloat switch item.height { - case .generic: - height = 44.0 - verticalOffset = -3.0 - avatarSize = 31.0 - leftInset = 59.0 + params.leftInset - case .peerList: - height = 50.0 - verticalOffset = 0.0 - avatarSize = 40.0 - leftInset = 65.0 + params.leftInset + case .generic: + if case .none = item.text { + verticalInset = 11.0 + } else { + verticalInset = 6.0 + } + verticalOffset = 0.0 + avatarSize = 31.0 + leftInset = 59.0 + params.leftInset + case .peerList: + if case .none = item.text { + verticalInset = 14.0 + } else { + verticalInset = 8.0 + } + verticalOffset = 0.0 + avatarSize = 40.0 + leftInset = 65.0 + params.leftInset } - var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? + var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)? let editingOffset: CGFloat if item.editing.editing { - let sizeAndApply = editableControlLayout(50.0, item.theme, false) + let sizeAndApply = editableControlLayout(item.presentationData.theme, false) editableControlSizeAndApply = sizeAndApply - editingOffset = sizeAndApply.0.width + editingOffset = sizeAndApply.0 } else { editingOffset = 0.0 } @@ -530,7 +760,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo case let .custom(value): selectedFont = value } - labelAttributedString = NSAttributedString(string: text, font: selectedFont, textColor: item.theme.list.itemSecondaryTextColor) + labelAttributedString = NSAttributedString(string: text, font: selectedFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) labelInset += 15.0 case let .disclosure(text): if let currentLabelArrowNode = currentLabelArrowNode { @@ -540,36 +770,44 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo arrowNode.isLayerBacked = true arrowNode.displayWithoutProcessing = true arrowNode.displaysAsynchronously = false - arrowNode.image = PresentationResourcesItemList.disclosureArrowImage(item.theme) + arrowNode.image = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) updatedLabelArrowNode = arrowNode } labelInset += 40.0 - labelAttributedString = NSAttributedString(string: text, font: labelDisclosureFont, textColor: item.theme.list.itemSecondaryTextColor) + labelAttributedString = NSAttributedString(string: text, font: labelDisclosureFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) case let .badge(text): - labelAttributedString = NSAttributedString(string: text, font: badgeFont, textColor: item.theme.list.itemCheckColors.foregroundColor) + labelAttributedString = NSAttributedString(string: text, font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) labelInset += 15.0 } let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - labelLayout.size.width - editingOffset - rightInset - labelInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - labelInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset - labelLayout.size.width - labelInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - labelLayout.size.width - labelInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var insets = itemListNeighborsGroupedInsets(neighbors) if !item.hasTopGroupInset { switch neighbors.top { - case .none: - insets.top = 0.0 - default: - break + case .none: + insets.top = 0.0 + default: + break } } if item.noInsets { insets.top = 0.0 insets.bottom = 0.0 } + if headerAtTop, let header = item.header { + insets.top += header.height + 18.0 + } - let contentSize = CGSize(width: params.width, height: height) + let titleSpacing: CGFloat = statusLayout.size.height == 0.0 ? 0.0 : 1.0 + + let minHeight: CGFloat = titleLayout.size.height + verticalInset * 2.0 + let rawHeight: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + statusLayout.size.height + + let contentSize = CGSize(width: params.width, height: max(minHeight, rawHeight)) let separatorHeight = UIScreenPixel let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -578,7 +816,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo if !item.enabled { if currentDisabledOverlayNode == nil { currentDisabledOverlayNode = ASDisplayNode() - currentDisabledOverlayNode?.backgroundColor = item.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.5) + currentDisabledOverlayNode?.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.5) } } else { currentDisabledOverlayNode = nil @@ -586,7 +824,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo return (layout, { [weak self] synchronousLoad, animated in if let strongSelf = self { - strongSelf.layoutParams = (item, params, neighbors) + strongSelf.layoutParams = (item, params, neighbors, headerAtTop) strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) strongSelf.containerNode.isGestureEnabled = item.contextAction != nil @@ -607,10 +845,10 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } let revealOffset = strongSelf.revealOffset @@ -640,9 +878,9 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } if let editableControlSizeAndApply = editableControlSizeAndApply { - let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: editableControlSizeAndApply.0) + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height)) if strongSelf.editableControlNode == nil { - let editableControlNode = editableControlSizeAndApply.1() + let editableControlNode = editableControlSizeAndApply.1(layout.contentSize.height) editableControlNode.tapped = { if let strongSelf = self { strongSelf.setRevealOptionsOpened(true, animated: true) @@ -692,34 +930,34 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo var hasTopCorners = false var hasBottomCorners = false switch neighbors.top { - case .sameSection(false): - strongSelf.topStripeNode.isHidden = true - default: - hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners || !item.hasTopStripe + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = !item.displayDecorations || hasCorners || !item.hasTopStripe } let bottomStripeInset: CGFloat let bottomStripeOffset: CGFloat switch neighbors.bottom { - case .sameSection(false): - bottomStripeInset = leftInset + editingOffset - bottomStripeOffset = -separatorHeight - default: - bottomStripeInset = 0.0 - bottomStripeOffset = 0.0 - hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners + case .sameSection(false): + bottomStripeInset = leftInset + editingOffset + bottomStripeOffset = -separatorHeight + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners || !item.displayDecorations } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) - transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: (statusAttributedString == nil ? 14.0 : 6.0) + verticalOffset), size: titleLayout.size)) - transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 27.0 + verticalOffset), size: statusLayout.size)) + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + verticalOffset), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size)) if let currentSwitchNode = currentSwitchNode { if currentSwitchNode !== strongSelf.switchNode { @@ -795,28 +1033,71 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo strongSelf.labelBadgeNode.frame = CGRect(origin: CGPoint(x: revealOffset + params.width - rightLabelInset - badgeWidth, y: labelFrame.minY - 1.0), size: CGSize(width: badgeWidth, height: badgeDiameter)) - transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0, y: floorToScreenPixels((height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))) + transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))) - if item.peer.id == item.account.peerId, case .threatSelfAsSaved = item.aliasHandling { - strongSelf.avatarNode.setPeer(account: item.account, theme: item.theme, peer: item.peer, overrideImage: .savedMessagesIcon, emptyColor: item.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad) + if item.peer.id == item.context.account.peerId, case .threatSelfAsSaved = item.aliasHandling { + strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: .savedMessagesIcon, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad) } else { var overrideImage: AvatarNodeImageOverride? if item.peer.isDeleted { overrideImage = .deletedIcon } - strongSelf.avatarNode.setPeer(account: item.account, theme: item.theme, peer: item.peer, overrideImage: overrideImage, emptyColor: item.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad) + strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad) } - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: height + UIScreenPixel + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)) if let presence = item.presence as? TelegramUserPresence { strongSelf.peerPresenceManager?.reset(presence: presence) } + if let shimmering = item.shimmering { + strongSelf.avatarNode.isHidden = true + strongSelf.titleNode.isHidden = true + + let shimmerNode: LoadingShimmerNode + if let current = strongSelf.shimmerNode { + shimmerNode = current + } else { + shimmerNode = LoadingShimmerNode() + strongSelf.shimmerNode = shimmerNode + strongSelf.insertSubnode(shimmerNode, aboveSubnode: strongSelf.backgroundNode) + } + shimmerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + if let (rect, size) = strongSelf.absoluteLocation { + shimmerNode.updateAbsoluteRect(rect, within: size) + } + var shapes: [LoadingShimmerNode.Shape] = [] + shapes.append(.circle(strongSelf.avatarNode.frame)) + let possibleLines: [[CGFloat]] = [ + [50.0, 40.0], + [70.0, 45.0] + ] + let titleFrame = strongSelf.titleNode.frame + let lineDiameter: CGFloat = 10.0 + var lineStart = titleFrame.minX + for lineWidth in possibleLines[shimmering.alternationIndex % possibleLines.count] { + shapes.append(.roundedRectLine(startPoint: CGPoint(x: lineStart, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: lineWidth, diameter: lineDiameter)) + lineStart += lineWidth + lineDiameter + } + shimmerNode.update(backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: layout.contentSize) + } else if let shimmerNode = strongSelf.shimmerNode { + strongSelf.avatarNode.isHidden = false + strongSelf.titleNode.isHidden = false + + strongSelf.shimmerNode = nil + shimmerNode.removeFromSupernode() + } + + strongSelf.backgroundNode.isHidden = !item.displayDecorations + strongSelf.highlightedBackgroundNode.isHidden = !item.displayDecorations + strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) strongSelf.setRevealOptions((left: [], right: peerRevealOptions)) - strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) + if let revealed = item.editing.revealed { + strongSelf.setRevealOptionsOpened(revealed, animated: animated) + } } }) } @@ -924,13 +1205,13 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } override public func revealOptionsInteractivelyOpened() { - if let (item, _, _) = self.layoutParams { + if let (item, _, _, _) = self.layoutParams { item.setPeerIdWithRevealedOptions(item.peer.id, nil) } } override public func revealOptionsInteractivelyClosed() { - if let (item, _, _) = self.layoutParams { + if let (item, _, _, _) = self.layoutParams { item.setPeerIdWithRevealedOptions(nil, item.peer.id) } } @@ -939,7 +1220,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo self.setRevealOptionsOpened(false, animated: true) self.revealOptionsInteractivelyClosed() - if let (item, _, _) = self.layoutParams { + if let (item, _, _, _) = self.layoutParams { if let revealOptions = item.revealOptions { if option.key >= 0 && option.key < Int32(revealOptions.options.count) { revealOptions.options[Int(option.key)].action() @@ -951,8 +1232,205 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } private func toggleUpdated(_ value: Bool) { - if let (item, _, _) = self.layoutParams { + if let (item, _, _, _) = self.layoutParams { item.toggleUpdated?(value) } } + + override public func header() -> ListViewItemHeader? { + return self.layoutParams?.0.header + } + + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + var rect = rect + rect.origin.y += self.insets.top + self.absoluteLocation = (rect, containerSize) + if let shimmerNode = self.shimmerNode { + shimmerNode.updateAbsoluteRect(rect, within: containerSize) + } + } +} + +public final class ItemListPeerItemHeader: ListViewItemHeader { + public let id: Int64 + public let text: String + public let additionalText: String + public let stickDirection: ListViewItemHeaderStickDirection = .topEdge + public let theme: PresentationTheme + public let strings: PresentationStrings + public let actionTitle: String? + public let action: (() -> Void)? + + public let height: CGFloat = 28.0 + + public init(theme: PresentationTheme, strings: PresentationStrings, text: String, additionalText: String, actionTitle: String? = nil, id: Int64, action: (() -> Void)? = nil) { + self.text = text + self.additionalText = additionalText + self.id = id + self.theme = theme + self.strings = strings + self.actionTitle = actionTitle + self.action = action + } + + public func node() -> ListViewItemHeaderNode { + return ItemListPeerItemHeaderNode(theme: self.theme, strings: self.strings, text: self.text, additionalText: self.additionalText, actionTitle: self.actionTitle, action: self.action) + } + + public func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) { + (node as? ItemListPeerItemHeaderNode)?.update(text: self.text, additionalText: self.additionalText, actionTitle: self.actionTitle, action: self.action) + } +} + +public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListHeaderItemNode { + private var theme: PresentationTheme + private var strings: PresentationStrings + private var actionTitle: String? + private var action: (() -> Void)? + + private var validLayout: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat)? + + private let backgroundNode: ASDisplayNode + private let snappedBackgroundNode: ASDisplayNode + private let separatorNode: ASDisplayNode + private let textNode: ImmediateTextNode + private let additionalTextNode: ImmediateTextNode + private let actionTextNode: ImmediateTextNode + private let actionButton: HighlightableButtonNode + + private var stickDistanceFactor: CGFloat? + + public init(theme: PresentationTheme, strings: PresentationStrings, text: String, additionalText: String, actionTitle: String?, action: (() -> Void)?) { + self.theme = theme + self.strings = strings + self.actionTitle = actionTitle + self.action = action + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.backgroundColor = theme.list.blocksBackgroundColor + + self.snappedBackgroundNode = ASDisplayNode() + self.snappedBackgroundNode.backgroundColor = theme.rootController.navigationBar.backgroundColor + self.snappedBackgroundNode.alpha = 0.0 + + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor + self.separatorNode.alpha = 0.0 + + let titleFont = Font.regular(13.0) + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.maximumNumberOfLines = 1 + self.textNode.attributedText = NSAttributedString(string: text, font: titleFont, textColor: theme.list.sectionHeaderTextColor) + + self.additionalTextNode = ImmediateTextNode() + self.additionalTextNode.displaysAsynchronously = false + self.additionalTextNode.maximumNumberOfLines = 1 + self.additionalTextNode.attributedText = NSAttributedString(string: additionalText, font: titleFont, textColor: theme.list.sectionHeaderTextColor) + + self.actionTextNode = ImmediateTextNode() + self.actionTextNode.displaysAsynchronously = false + self.actionTextNode.maximumNumberOfLines = 1 + self.actionTextNode.attributedText = NSAttributedString(string: actionTitle ?? "", font: titleFont, textColor: action == nil ? theme.list.sectionHeaderTextColor : theme.list.itemAccentColor) + + self.actionButton = HighlightableButtonNode() + self.actionButton.isUserInteractionEnabled = self.action != nil + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.snappedBackgroundNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.textNode) + self.addSubnode(self.additionalTextNode) + self.addSubnode(self.actionTextNode) + self.addSubnode(self.actionButton) + + self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) + self.actionButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.actionTextNode.layer.removeAnimation(forKey: "opacity") + strongSelf.actionTextNode.alpha = 0.4 + } else { + strongSelf.actionTextNode.alpha = 1.0 + strongSelf.actionTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + @objc private func actionButtonPressed() { + self.action?() + } + + public func updateTheme(theme: PresentationTheme) { + self.theme = theme + + self.backgroundNode.backgroundColor = theme.list.blocksBackgroundColor + self.snappedBackgroundNode.backgroundColor = theme.rootController.navigationBar.backgroundColor + self.separatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor + + let titleFont = Font.regular(13.0) + + self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: titleFont, textColor: theme.list.sectionHeaderTextColor) + self.additionalTextNode.attributedText = NSAttributedString(string: self.additionalTextNode.attributedText?.string ?? "", font: titleFont, textColor: theme.list.sectionHeaderTextColor) + self.actionTextNode.attributedText = NSAttributedString(string: self.actionTextNode.attributedText?.string ?? "", font: titleFont, textColor: theme.list.sectionHeaderTextColor) + } + + public func update(text: String, additionalText: String, actionTitle: String?, action: (() -> Void)?) { + self.actionTitle = actionTitle + self.action = action + let titleFont = Font.regular(13.0) + self.textNode.attributedText = NSAttributedString(string: text, font: titleFont, textColor: theme.list.sectionHeaderTextColor) + self.additionalTextNode.attributedText = NSAttributedString(string: additionalText, font: titleFont, textColor: theme.list.sectionHeaderTextColor) + self.actionTextNode.attributedText = NSAttributedString(string: actionTitle ?? "", font: titleFont, textColor: action == nil ? theme.list.sectionHeaderTextColor : theme.list.itemAccentColor) + self.actionButton.isUserInteractionEnabled = self.action != nil + if let (size, leftInset, rightInset) = self.validLayout { + self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset) + } + } + + override public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + self.validLayout = (size, leftInset, rightInset) + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) + self.snappedBackgroundNode.frame = CGRect(origin: CGPoint(), size: size) + self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel)) + + let sideInset: CGFloat = 15.0 + leftInset + + let actionTextSize = self.actionTextNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: size.height)) + let additionalTextSize = self.additionalTextNode.updateLayout(CGSize(width: size.width - sideInset * 2.0 - actionTextSize.width - 8.0, height: size.height)) + let textSize = self.textNode.updateLayout(CGSize(width: max(1.0, size.width - sideInset * 2.0 - actionTextSize.width - 8.0 - additionalTextSize.width), height: size.height)) + + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 7.0), size: textSize) + self.textNode.frame = textFrame + self.additionalTextNode.frame = CGRect(origin: CGPoint(x: textFrame.maxX, y: 7.0), size: additionalTextSize) + self.actionTextNode.frame = CGRect(origin: CGPoint(x: size.width - sideInset - actionTextSize.width, y: 7.0), size: actionTextSize) + self.actionButton.frame = CGRect(origin: CGPoint(x: size.width - sideInset - actionTextSize.width, y: 0.0), size: CGSize(width: actionTextSize.width, height: size.height)) + } + + override public func animateRemoved(duration: Double) { + self.alpha = 0.0 + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: true) + } + + override public func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { + if self.stickDistanceFactor == factor { + return + } + self.stickDistanceFactor = factor + if let (size, leftInset, _) = self.validLayout { + if leftInset.isZero { + transition.updateAlpha(node: self.separatorNode, alpha: 1.0) + transition.updateAlpha(node: self.snappedBackgroundNode, alpha: (1.0 - factor) * 0.0 + factor * 1.0) + } else { + let distance = factor * size.height + let alpha = abs(distance) / 16.0 + transition.updateAlpha(node: self.separatorNode, alpha: max(0.0, min(1.0, alpha))) + transition.updateAlpha(node: self.snappedBackgroundNode, alpha: 0.0) + } + } + } } diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index d48b59bc09..f707e88839 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -31,11 +31,11 @@ public enum ItemListStickerPackItemControl: Equatable { case none case installation(installed: Bool) case selection + case check(checked: Bool) } public final class ItemListStickerPackItem: ListViewItem, ItemListItem { - let theme: PresentationTheme - let strings: PresentationStrings + let presentationData: ItemListPresentationData let account: Account let packInfo: StickerPackCollectionInfo let itemCount: String @@ -51,9 +51,8 @@ public final class ItemListStickerPackItem: ListViewItem, ItemListItem { let addPack: () -> Void let removePack: () -> Void - public init(theme: PresentationTheme, strings: PresentationStrings, account: Account, packInfo: StickerPackCollectionInfo, itemCount: String, topItem: StickerPackItem?, unread: Bool, control: ItemListStickerPackItemControl, editing: ItemListStickerPackItemEditing, enabled: Bool, playAnimatedStickers: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, addPack: @escaping () -> Void, removePack: @escaping () -> Void) { - self.theme = theme - self.strings = strings + public init(presentationData: ItemListPresentationData, account: Account, packInfo: StickerPackCollectionInfo, itemCount: String, topItem: StickerPackItem?, unread: Bool, control: ItemListStickerPackItemControl, editing: ItemListStickerPackItemEditing, enabled: Bool, playAnimatedStickers: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, addPack: @escaping () -> Void, removePack: @escaping () -> Void) { + self.presentationData = presentationData self.account = account self.packInfo = packInfo self.itemCount = itemCount @@ -116,9 +115,6 @@ public final class ItemListStickerPackItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.bold(15.0) -private let statusFont = Font.regular(14.0) - public enum StickerPackThumbnailItem: Equatable { case still(TelegramMediaImageRepresentation) case animated(MediaResource) @@ -274,18 +270,21 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { let currentItem = self.layoutParams?.0 return { item, params, neighbors in + let titleFont = Font.bold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + let statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + var titleAttributedString: NSAttributedString? var statusAttributedString: NSAttributedString? var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } let packRevealOptions: [ItemListRevealOption] if item.editing.editable && item.enabled { - packRevealOptions = [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)] + packRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] } else { packRevealOptions = [] } @@ -300,57 +299,62 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { case let .installation(installed): rightInset += 50.0 if installed { - installationActionImage = PresentationResourcesItemList.secondaryCheckIconImage(item.theme) + installationActionImage = PresentationResourcesItemList.secondaryCheckIconImage(item.presentationData.theme) } else { - installationActionImage = PresentationResourcesItemList.plusIconImage(item.theme) + installationActionImage = PresentationResourcesItemList.plusIconImage(item.presentationData.theme) } case .selection: rightInset += 16.0 - checkImage = PresentationResourcesItemList.checkIconImage(item.theme) + checkImage = PresentationResourcesItemList.checkIconImage(item.presentationData.theme) + case .check: + rightInset += 16.0 } var unreadImage: UIImage? if item.unread { - unreadImage = PresentationResourcesItemList.stickerUnreadDotImage(item.theme) + unreadImage = PresentationResourcesItemList.stickerUnreadDotImage(item.presentationData.theme) } - titleAttributedString = NSAttributedString(string: item.packInfo.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) - statusAttributedString = NSAttributedString(string: item.itemCount, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + titleAttributedString = NSAttributedString(string: item.packInfo.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + statusAttributedString = NSAttributedString(string: item.itemCount, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) let leftInset: CGFloat = 65.0 + params.leftInset + let verticalInset: CGFloat = 11.0 + let titleSpacing: CGFloat = 2.0 + let insets = itemListNeighborsGroupedInsets(neighbors) - let contentSize = CGSize(width: params.width, height: 59.0) let separatorHeight = UIScreenPixel - let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - let layoutSize = layout.size - - var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? - var reorderControlSizeAndApply: (CGSize, (Bool) -> ItemListEditableReorderControlNode)? + var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)? + var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)? var editingOffset: CGFloat = 0.0 var reorderInset: CGFloat = 0.0 if item.editing.editing { - let sizeAndApply = editableControlLayout(59.0, item.theme, false) + let sizeAndApply = editableControlLayout(item.presentationData.theme, false) editableControlSizeAndApply = sizeAndApply - editingOffset = sizeAndApply.0.width + editingOffset = sizeAndApply.0 if item.editing.reorderable { - let sizeAndApply = reorderControlLayout(contentSize.height, item.theme) + let sizeAndApply = reorderControlLayout(item.presentationData.theme) reorderControlSizeAndApply = sizeAndApply - reorderInset = sizeAndApply.0.width + reorderInset = sizeAndApply.0 } } let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - 10.0 - reorderInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - reorderInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + statusLayout.size.height) + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + if !item.enabled { if currentDisabledOverlayNode == nil { currentDisabledOverlayNode = ASDisplayNode() - currentDisabledOverlayNode?.backgroundColor = item.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.5) + currentDisabledOverlayNode?.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.5) } } else { currentDisabledOverlayNode = nil @@ -420,10 +424,10 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } let revealOffset = strongSelf.revealOffset @@ -453,9 +457,9 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } if let editableControlSizeAndApply = editableControlSizeAndApply { - let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: editableControlSizeAndApply.0) + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.size.height)) if strongSelf.editableControlNode == nil { - let editableControlNode = editableControlSizeAndApply.1() + let editableControlNode = editableControlSizeAndApply.1(layout.size.height) editableControlNode.tapped = { if let strongSelf = self { strongSelf.setRevealOptionsOpened(true, animated: true) @@ -484,13 +488,13 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { if let reorderControlSizeAndApply = reorderControlSizeAndApply { if strongSelf.reorderControlNode == nil { - let reorderControlNode = reorderControlSizeAndApply.1(false) + let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate) strongSelf.reorderControlNode = reorderControlNode strongSelf.addSubnode(reorderControlNode) reorderControlNode.alpha = 0.0 transition.updateAlpha(node: reorderControlNode, alpha: 1.0) } - let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0.width, y: 0.0), size: reorderControlSizeAndApply.0) + let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0, y: 0.0), size: CGSize(width: reorderControlSizeAndApply.0, height: layout.contentSize.height)) strongSelf.reorderControlNode?.frame = reorderControlFrame } else if let reorderControlNode = strongSelf.reorderControlNode { strongSelf.reorderControlNode = nil @@ -530,6 +534,10 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { strongSelf.selectionIconNode.image = image strongSelf.selectionIconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - image.size.width - floor((44.0 - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) } + case let .check(checked): + strongSelf.installationActionNode.isHidden = true + strongSelf.installationActionImageNode.isHidden = true + strongSelf.selectionIconNode.isHidden = true } if strongSelf.backgroundNode.supernode == nil { @@ -568,7 +576,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) @@ -583,12 +591,12 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { strongSelf.unreadNode.isHidden = true } - transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: (strongSelf.unreadNode.isHidden ? 0.0 : 10.0) + leftInset + revealOffset + editingOffset, y: 11.0), size: titleLayout.size)) - transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 32.0), size: statusLayout.size)) + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: (strongSelf.unreadNode.isHidden ? 0.0 : 10.0) + leftInset + revealOffset + editingOffset, y: verticalInset), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size)) let boundingSize = CGSize(width: 34.0, height: 34.0) if let thumbnailItem = thumbnailItem, let imageSize = imageSize { - let imageFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0 + floor((boundingSize.width - imageSize.width) / 2.0), y: 11.0 + floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize) + let imageFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0 + floor((boundingSize.width - imageSize.width) / 2.0), y: floor((layout.contentSize.height - imageSize.height) / 2.0)), size: imageSize) switch thumbnailItem { case .still: transition.updateFrame(node: strongSelf.imageNode, frame: imageFrame) @@ -741,4 +749,11 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } return false } + + override func snapshotForReordering() -> UIView? { + self.backgroundNode.alpha = 0.9 + let result = self.view.snapshotContentTree() + self.backgroundNode.alpha = 1.0 + return result + } } diff --git a/submodules/ItemListUI/Sources/ItemListController.swift b/submodules/ItemListUI/Sources/ItemListController.swift index d6ccf5ff40..70a717c81f 100644 --- a/submodules/ItemListUI/Sources/ItemListController.swift +++ b/submodules/ItemListUI/Sources/ItemListController.swift @@ -56,6 +56,7 @@ public struct ItemListBackButton: Equatable { public enum ItemListControllerTitle: Equatable { case text(String) + case textWithSubtitle(String, String) case sectionControl([String], Int) } @@ -80,7 +81,7 @@ public final class ItemListControllerTabBarItem: Equatable { } public struct ItemListControllerState { - let theme: PresentationTheme + let presentationData: ItemListPresentationData let title: ItemListControllerTitle let leftNavigationButton: ItemListNavigationButton? let rightNavigationButton: ItemListNavigationButton? @@ -89,8 +90,8 @@ public struct ItemListControllerState { let tabBarItem: ItemListControllerTabBarItem? let animateChanges: Bool - public init(theme: PresentationTheme, title: ItemListControllerTitle, leftNavigationButton: ItemListNavigationButton?, rightNavigationButton: ItemListNavigationButton?, secondaryRightNavigationButton: ItemListNavigationButton? = nil, backNavigationButton: ItemListBackButton?, tabBarItem: ItemListControllerTabBarItem? = nil, animateChanges: Bool = true) { - self.theme = theme + public init(presentationData: ItemListPresentationData, title: ItemListControllerTitle, leftNavigationButton: ItemListNavigationButton?, rightNavigationButton: ItemListNavigationButton?, secondaryRightNavigationButton: ItemListNavigationButton? = nil, backNavigationButton: ItemListBackButton?, tabBarItem: ItemListControllerTabBarItem? = nil, animateChanges: Bool = true) { + self.presentationData = presentationData self.title = title self.leftNavigationButton = leftNavigationButton self.rightNavigationButton = rightNavigationButton @@ -111,11 +112,12 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable private var navigationButtonActions: (left: (() -> Void)?, right: (() -> Void)?, secondaryRight: (() -> Void)?) = (nil, nil, nil) private var segmentedTitleView: ItemListControllerSegmentedTitleView? - private var theme: PresentationTheme - private var strings: PresentationStrings + private var presentationData: ItemListPresentationData private var validLayout: ContainerViewLayout? + public var additionalInsets: UIEdgeInsets = UIEdgeInsets() + private var didPlayPresentationAnimation = false public private(set) var didAppearOnce = false public var didAppear: ((Bool) -> Void)? @@ -196,12 +198,12 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable public var willScrollToTop: (() -> Void)? - public func setReorderEntry(_ f: @escaping (Int, Int, [T]) -> Void) { + public func setReorderEntry(_ f: @escaping (Int, Int, [T]) -> Signal) { self.reorderEntry = { a, b, list in - f(a, b, list.map { $0 as! T }) + return f(a, b, list.map { $0 as! T }) } } - private var reorderEntry: ((Int, Int, [ItemListNodeAnyEntry]) -> Void)? { + private var reorderEntry: ((Int, Int, [ItemListNodeAnyEntry]) -> Signal)? { didSet { if self.isNodeLoaded { (self.displayNode as! ItemListControllerNode).reorderEntry = self.reorderEntry @@ -228,21 +230,20 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable public var willDisappear: ((Bool) -> Void)? public var didDisappear: ((Bool) -> Void)? - public init(theme: PresentationTheme, strings: PresentationStrings, updatedPresentationData: Signal<(theme: PresentationTheme, strings: PresentationStrings), NoError>, state: Signal<(ItemListControllerState, (ItemListNodeState, ItemGenerationArguments)), NoError>, tabBarItem: Signal?) { + public init(presentationData: ItemListPresentationData, updatedPresentationData: Signal, state: Signal<(ItemListControllerState, (ItemListNodeState, ItemGenerationArguments)), NoError>, tabBarItem: Signal?) { self.state = state |> map { controllerState, nodeStateAndArgument -> (ItemListControllerState, (ItemListNodeState, Any)) in return (controllerState, (nodeStateAndArgument.0, nodeStateAndArgument.1)) } - self.theme = theme - self.strings = strings + self.presentationData = presentationData - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: theme), strings: NavigationBarStrings(presentationStrings: strings))) + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme), strings: NavigationBarStrings(presentationStrings: presentationData.strings))) self.isOpaqueWhenInOverlay = true self.blocksBackgroundWhenInOverlay = true - self.statusBar.statusBarStyle = theme.rootController.statusBarStyle.style + self.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style self.scrollToTop = { [weak self] in self?.willScrollToTop?() @@ -287,12 +288,16 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable strongSelf.title = text strongSelf.navigationItem.titleView = nil strongSelf.segmentedTitleView = nil + case let .textWithSubtitle(title, subtitle): + strongSelf.title = "" + strongSelf.navigationItem.titleView = ItemListTextWithSubtitleTitleView(theme: controllerState.presentationData.theme, title: title, subtitle: subtitle) + strongSelf.segmentedTitleView = nil case let .sectionControl(sections, index): strongSelf.title = "" if let segmentedTitleView = strongSelf.segmentedTitleView, segmentedTitleView.segments == sections { segmentedTitleView.index = index } else { - let segmentedTitleView = ItemListControllerSegmentedTitleView(theme: controllerState.theme, segments: sections, selectedIndex: index) + let segmentedTitleView = ItemListControllerSegmentedTitleView(theme: controllerState.presentationData.theme, segments: sections, selectedIndex: index) strongSelf.segmentedTitleView = segmentedTitleView strongSelf.navigationItem.titleView = strongSelf.segmentedTitleView segmentedTitleView.indexUpdated = { index in @@ -305,7 +310,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable } strongSelf.navigationButtonActions = (left: controllerState.leftNavigationButton?.action, right: controllerState.rightNavigationButton?.action, secondaryRight: controllerState.secondaryRightNavigationButton?.action) - let themeUpdated = strongSelf.theme !== controllerState.theme + let themeUpdated = strongSelf.presentationData != controllerState.presentationData if strongSelf.leftNavigationButtonTitleAndStyle?.0 != controllerState.leftNavigationButton?.content || strongSelf.leftNavigationButtonTitleAndStyle?.1 != controllerState.leftNavigationButton?.style || themeUpdated { if let leftNavigationButton = controllerState.leftNavigationButton { let item: UIBarButtonItem @@ -318,11 +323,11 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable var image: UIImage? switch icon { case .search: - image = PresentationResourcesRootController.navigationCompactSearchIcon(controllerState.theme) + image = PresentationResourcesRootController.navigationCompactSearchIcon(controllerState.presentationData.theme) case .add: - image = PresentationResourcesRootController.navigationAddIcon(controllerState.theme) + image = PresentationResourcesRootController.navigationAddIcon(controllerState.presentationData.theme) case .action: - image = PresentationResourcesRootController.navigationShareIcon(controllerState.theme) + image = PresentationResourcesRootController.navigationShareIcon(controllerState.presentationData.theme) } item = UIBarButtonItem(image: image, style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed)) } @@ -363,7 +368,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable for (content, style, _) in rightNavigationButtonTitleAndStyle { let item: UIBarButtonItem if case .activity = style { - item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: controllerState.theme.rootController.navigationBar.controlColor)) + item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: controllerState.presentationData.theme.rootController.navigationBar.controlColor)) } else { let action: Selector = (index == 0 && rightNavigationButtonTitleAndStyle.count > 1) ? #selector(strongSelf.secondaryRightNavigationButtonPressed) : #selector(strongSelf.rightNavigationButtonPressed) switch content { @@ -375,11 +380,11 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable var image: UIImage? switch icon { case .search: - image = PresentationResourcesRootController.navigationCompactSearchIcon(controllerState.theme) + image = PresentationResourcesRootController.navigationCompactSearchIcon(controllerState.presentationData.theme) case .add: - image = PresentationResourcesRootController.navigationAddIcon(controllerState.theme) + image = PresentationResourcesRootController.navigationAddIcon(controllerState.presentationData.theme) case .action: - image = PresentationResourcesRootController.navigationShareIcon(controllerState.theme) + image = PresentationResourcesRootController.navigationShareIcon(controllerState.presentationData.theme) } item = UIBarButtonItem(image: image, style: style.barButtonItemStyle, target: strongSelf, action: action) } @@ -409,25 +414,31 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable } } - if strongSelf.theme !== controllerState.theme { - strongSelf.theme = controllerState.theme + if strongSelf.presentationData != controllerState.presentationData { + strongSelf.presentationData = controllerState.presentationData - strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: strongSelf.theme), strings: NavigationBarStrings(presentationStrings: strongSelf.strings))) - strongSelf.statusBar.updateStatusBarStyle(strongSelf.theme.rootController.statusBarStyle.style, animated: true) + strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: strongSelf.presentationData.theme), strings: NavigationBarStrings(presentationStrings: strongSelf.presentationData.strings))) + strongSelf.statusBar.updateStatusBarStyle(strongSelf.presentationData.theme.rootController.statusBarStyle.style, animated: true) - strongSelf.segmentedTitleView?.theme = controllerState.theme + strongSelf.segmentedTitleView?.theme = controllerState.presentationData.theme + + if let titleView = strongSelf.navigationItem.titleView as? ItemListTextWithSubtitleTitleView { + titleView.updateTheme(theme: controllerState.presentationData.theme) + } var items = strongSelf.navigationItem.rightBarButtonItems ?? [] for i in 0 ..< strongSelf.rightNavigationButtonTitleAndStyle.count { if case .activity = strongSelf.rightNavigationButtonTitleAndStyle[i].1 { - items[i] = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: controllerState.theme.rootController.navigationBar.controlColor))! + items[i] = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: controllerState.presentationData.theme.rootController.navigationBar.controlColor))! } } strongSelf.navigationItem.setRightBarButtonItems(items, animated: false) } } } - } |> map { ($0.theme, $1) } + } + |> map { ($0.presentationData, $1) } + let displayNode = ItemListControllerNode(controller: self, navigationBar: self.navigationBar!, updateNavigationOffset: { [weak self] offset in if let strongSelf = self { strongSelf.navigationOffset = offset @@ -459,7 +470,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable self.validLayout = layout - (self.displayNode as! ItemListControllerNode).containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, transition: transition) + (self.displayNode as! ItemListControllerNode).containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, transition: transition, additionalInsets: self.additionalInsets) } @objc func leftNavigationButtonPressed() { @@ -515,6 +526,10 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable self.didDisappear?(animated) } + public var listInsets: UIEdgeInsets { + return (self.displayNode as! ItemListControllerNode).listNode.insets + } + public func frameForItemNode(_ predicate: (ListViewItemNode) -> Bool) -> CGRect? { var result: CGRect? (self.displayNode as! ItemListControllerNode).listNode.forEachItemNode { itemNode in @@ -535,8 +550,8 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable } } - public func ensureItemNodeVisible(_ itemNode: ListViewItemNode, animated: Bool = true) { - (self.displayNode as! ItemListControllerNode).listNode.ensureItemNodeVisible(itemNode, animated: animated) + public func ensureItemNodeVisible(_ itemNode: ListViewItemNode, animated: Bool = true, curve: ListViewAnimationCurve = .Default(duration: 0.25)) { + (self.displayNode as! ItemListControllerNode).listNode.ensureItemNodeVisible(itemNode, animated: animated, curve: curve) } public func afterLayout(_ f: @escaping () -> Void) { @@ -596,3 +611,68 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable })] } } + +private final class ItemListTextWithSubtitleTitleView: UIView, NavigationBarTitleView { + private let titleNode: ImmediateTextNode + private let subtitleNode: ImmediateTextNode + + private var validLayout: (CGSize, CGRect)? + + init(theme: PresentationTheme, title: String, subtitle: String) { + self.titleNode = ImmediateTextNode() + self.titleNode.displaysAsynchronously = false + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.isOpaque = false + + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(17.0), textColor: theme.rootController.navigationBar.primaryTextColor) + + self.subtitleNode = ImmediateTextNode() + self.subtitleNode.displaysAsynchronously = false + self.subtitleNode.maximumNumberOfLines = 1 + self.subtitleNode.isOpaque = false + + self.subtitleNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor) + + super.init(frame: CGRect()) + + self.addSubnode(self.titleNode) + self.addSubnode(self.subtitleNode) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateTheme(theme: PresentationTheme) { + self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.medium(17.0), textColor: theme.rootController.navigationBar.primaryTextColor) + self.subtitleNode.attributedText = NSAttributedString(string: self.subtitleNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor) + if let (size, clearBounds) = self.validLayout { + self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate) + } + } + + override func layoutSubviews() { + super.layoutSubviews() + + if let (size, clearBounds) = self.validLayout { + self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate) + } + } + + func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, clearBounds) + + let titleSize = self.titleNode.updateLayout(size) + let subtitleSize = self.subtitleNode.updateLayout(size) + let spacing: CGFloat = 0.0 + let contentHeight = titleSize.height + spacing + subtitleSize.height + let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - contentHeight) / 2.0)), size: titleSize) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: titleFrame.maxY + spacing), size: subtitleSize) + + self.titleNode.frame = titleFrame + self.subtitleNode.frame = subtitleFrame + } + + func animateLayoutTransition() { + } +} diff --git a/submodules/ItemListUI/Sources/ItemListControllerNode.swift b/submodules/ItemListUI/Sources/ItemListControllerNode.swift index 4bd1eb822c..253b2d14d2 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerNode.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerNode.swift @@ -8,6 +8,10 @@ import SyncCore import TelegramPresentationData import MergeLists +public protocol ItemListHeaderItemNode: class { + func updateTheme(theme: PresentationTheme) +} + public typealias ItemListSectionId = Int32 public protocol ItemListNodeAnyEntry { @@ -15,7 +19,7 @@ public protocol ItemListNodeAnyEntry { var tag: ItemListItemTag? { get } func isLessThan(_ rhs: ItemListNodeAnyEntry) -> Bool func isEqual(_ rhs: ItemListNodeAnyEntry) -> Bool - func item(_ arguments: Any) -> ListViewItem + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem } public protocol ItemListNodeEntry: Comparable, Identifiable, ItemListNodeAnyEntry { @@ -46,18 +50,18 @@ private struct ItemListNodeEntryTransition { let updates: [ListViewUpdateItem] } -private func preparedItemListNodeEntryTransition(from fromEntries: [ItemListNodeAnyEntry], to toEntries: [ItemListNodeAnyEntry], arguments: Any) -> ItemListNodeEntryTransition { +private func preparedItemListNodeEntryTransition(from fromEntries: [ItemListNodeAnyEntry], to toEntries: [ItemListNodeAnyEntry], presentationData: ItemListPresentationData, arguments: Any, presentationDataUpdated: Bool) -> ItemListNodeEntryTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, isLess: { lhs, rhs in return lhs.isLessThan(rhs) }, isEqual: { lhs, rhs in return lhs.isEqual(rhs) }, getId: { value in return value.anyId - }) + }, allUpdated: presentationDataUpdated) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(arguments), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(arguments), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, arguments: arguments), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, arguments: arguments), directionHint: nil) } return ItemListNodeEntryTransition(deletions: deletions, insertions: insertions, updates: updates) } @@ -85,6 +89,7 @@ private struct ItemListNodeTransition { } public final class ItemListNodeState { + let presentationData: ItemListPresentationData let entries: [ItemListNodeAnyEntry] let style: ItemListStyle let emptyStateItem: ItemListControllerEmptyStateItem? @@ -96,7 +101,8 @@ public final class ItemListNodeState { let ensureVisibleItemTag: ItemListItemTag? let initialScrollToItem: ListViewScrollToItem? - public init(entries: [T], style: ItemListStyle, focusItemTag: ItemListItemTag? = nil, ensureVisibleItemTag: ItemListItemTag? = nil, emptyStateItem: ItemListControllerEmptyStateItem? = nil, searchItem: ItemListControllerSearch? = nil, initialScrollToItem: ListViewScrollToItem? = nil, crossfadeState: Bool = false, animateChanges: Bool = true, scrollEnabled: Bool = true) { + public init(presentationData: ItemListPresentationData, entries: [T], style: ItemListStyle, focusItemTag: ItemListItemTag? = nil, ensureVisibleItemTag: ItemListItemTag? = nil, emptyStateItem: ItemListControllerEmptyStateItem? = nil, searchItem: ItemListControllerSearch? = nil, initialScrollToItem: ListViewScrollToItem? = nil, crossfadeState: Bool = false, animateChanges: Bool = true, scrollEnabled: Bool = true) { + self.presentationData = presentationData self.entries = entries.map { $0 } self.style = style self.emptyStateItem = emptyStateItem @@ -215,7 +221,7 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { public var contentOffsetChanged: ((ListViewVisibleContentOffset, Bool) -> Void)? public var contentScrollingEnded: ((ListView) -> Bool)? public var searchActivated: ((Bool) -> Void)? - public var reorderEntry: ((Int, Int, [ItemListNodeAnyEntry]) -> Void)? + public var reorderEntry: ((Int, Int, [ItemListNodeAnyEntry]) -> Signal)? public var reorderCompleted: (([ItemListNodeAnyEntry]) -> Void)? public var requestLayout: ((ContainedViewLayoutTransition) -> Void)? @@ -226,7 +232,7 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { var alwaysSynchronous = false - public init(controller: ItemListController?, navigationBar: NavigationBar, updateNavigationOffset: @escaping (CGFloat) -> Void, state: Signal<(PresentationTheme, (ItemListNodeState, Any)), NoError>) { + public init(controller: ItemListController?, navigationBar: NavigationBar, updateNavigationOffset: @escaping (CGFloat) -> Void, state: Signal<(ItemListPresentationData, (ItemListNodeState, Any)), NoError>) { self.navigationBar = navigationBar self.updateNavigationOffset = updateNavigationOffset @@ -267,7 +273,7 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { self.listNode.reorderItem = { [weak self] fromIndex, toIndex, opaqueTransactionState in if let strongSelf = self, let reorderEntry = strongSelf.reorderEntry, let mergedEntries = (opaqueTransactionState as? ItemListNodeOpaqueState)?.mergedEntries { if fromIndex >= 0 && fromIndex < mergedEntries.count && toIndex >= 0 && toIndex < mergedEntries.count { - reorderEntry(fromIndex, toIndex, mergedEntries) + return reorderEntry(fromIndex, toIndex, mergedEntries) } } return .single(false) @@ -298,7 +304,8 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { } let previousState = Atomic(value: nil) - self.transitionDisposable.set(((state |> map { theme, stateAndArguments -> ItemListNodeTransition in + self.transitionDisposable.set(((state + |> map { presentationData, stateAndArguments -> ItemListNodeTransition in let (state, arguments) = stateAndArguments if state.entries.count > 1 { for i in 1 ..< state.entries.count { @@ -306,7 +313,7 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { } } let previous = previousState.swap(state) - let transition = preparedItemListNodeEntryTransition(from: previous?.entries ?? [], to: state.entries, arguments: arguments) + let transition = preparedItemListNodeEntryTransition(from: previous?.entries ?? [], to: state.entries, presentationData: presentationData, arguments: arguments, presentationDataUpdated: previous?.presentationData != presentationData) var updatedStyle: ItemListStyle? if previous?.style != state.style { updatedStyle = state.style @@ -317,8 +324,9 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { scrollToItem = state.initialScrollToItem } - return ItemListNodeTransition(theme: theme, entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, searchItem: state.searchItem, focusItemTag: state.focusItemTag, ensureVisibleItemTag: state.ensureVisibleItemTag, scrollToItem: scrollToItem, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && state.animateChanges, crossfade: state.crossfadeState, mergedEntries: state.entries, scrollEnabled: state.scrollEnabled) - }) |> deliverOnMainQueue).start(next: { [weak self] transition in + return ItemListNodeTransition(theme: presentationData.theme, entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, searchItem: state.searchItem, focusItemTag: state.focusItemTag, ensureVisibleItemTag: state.ensureVisibleItemTag, scrollToItem: scrollToItem, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && state.animateChanges, crossfade: state.crossfadeState, mergedEntries: state.entries, scrollEnabled: state.scrollEnabled) + }) + |> deliverOnMainQueue).start(next: { [weak self] transition in if let strongSelf = self { strongSelf.enqueueTransition(transition) } @@ -365,31 +373,10 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { }) } - open func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - + open func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight + insets.bottom = max(insets.bottom, additionalInsets.bottom) var addedInsets: UIEdgeInsets? if layout.size.width > 480.0 { @@ -416,10 +403,12 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { } } + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.leftOverlayNode.frame = CGRect(x: 0.0, y: 0.0, width: insets.left, height: layout.size.height) self.rightOverlayNode.frame = CGRect(x: layout.size.width - insets.right, y: 0.0, width: insets.right, height: layout.size.height) @@ -433,6 +422,9 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { } if let searchNode = self.searchNode { + var layout = layout + layout = layout.addedInsets(insets: additionalInsets) + searchNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, transition: transition) } @@ -479,6 +471,12 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { self.rightOverlayNode.backgroundColor = transition.theme.list.blocksBackgroundColor } } + + self.listNode.forEachItemHeaderNode({ itemHeaderNode in + if let itemHeaderNode = itemHeaderNode as? ItemListHeaderItemNode { + itemHeaderNode.updateTheme(theme: transition.theme) + } + }) } if let updateStyle = transition.updateStyle { @@ -513,9 +511,12 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { options.insert(.PreferSynchronousDrawing) options.insert(.AnimateAlpha) } else if transition.crossfade { + options.insert(.PreferSynchronousResourceLoading) + options.insert(.PreferSynchronousDrawing) options.insert(.AnimateCrossfade) } else { options.insert(.Synchronous) + options.insert(.PreferSynchronousResourceLoading) options.insert(.PreferSynchronousDrawing) } if self.alwaysSynchronous { @@ -567,7 +568,7 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { if let validLayout = self.validLayout { updatedNode.updateLayout(layout: validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate) } - self.insertSubnode(updatedNode, belowSubnode: self.navigationBar) + self.insertSubnode(updatedNode, aboveSubnode: self.listNode) updatedNode.activate() } } else { @@ -667,7 +668,6 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { emptyStateNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak emptyStateNode] _ in emptyStateNode?.removeFromSupernode() }) - self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.emptyStateNode = nil } } @@ -700,6 +700,11 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let searchNode = self.searchNode { + if !self.navigationBar.isHidden && self.navigationBar.supernode != nil { + if let result = self.navigationBar.hitTest(self.view.convert(point, to: self.navigationBar.view), with: event) { + return result + } + } if let result = searchNode.hitTest(point, with: event) { return result } diff --git a/submodules/ItemListUI/Sources/ItemListEditableDeleteControlNode.swift b/submodules/ItemListUI/Sources/ItemListEditableDeleteControlNode.swift index 92a88917fa..815b6f77d7 100644 --- a/submodules/ItemListUI/Sources/ItemListEditableDeleteControlNode.swift +++ b/submodules/ItemListUI/Sources/ItemListEditableDeleteControlNode.swift @@ -23,8 +23,8 @@ public final class ItemListEditableControlNode: ASDisplayNode { self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } - public static func asyncLayout(_ node: ItemListEditableControlNode?) -> (_ height: CGFloat, _ theme: PresentationTheme, _ hidden: Bool) -> (CGSize, () -> ItemListEditableControlNode) { - return { height, theme, hidden in + public static func asyncLayout(_ node: ItemListEditableControlNode?) -> (_ theme: PresentationTheme, _ hidden: Bool) -> (CGFloat, (CGFloat) -> ItemListEditableControlNode) { + return { theme, hidden in let image = PresentationResourcesItemList.itemListDeleteIndicatorIcon(theme) let resultNode: ItemListEditableControlNode @@ -35,7 +35,7 @@ public final class ItemListEditableControlNode: ASDisplayNode { } resultNode.iconNode.image = image - return (CGSize(width: 38.0, height: height), { + return (38.0, { height in if let image = image { resultNode.iconNode.frame = CGRect(origin: CGPoint(x: 12.0, y: floor((height - image.size.height) / 2.0)), size: image.size) resultNode.iconNode.isHidden = hidden diff --git a/submodules/ItemListUI/Sources/ItemListEditableReorderControlNode.swift b/submodules/ItemListUI/Sources/ItemListEditableReorderControlNode.swift index 5c2c5d6c13..e3c6028ec1 100644 --- a/submodules/ItemListUI/Sources/ItemListEditableReorderControlNode.swift +++ b/submodules/ItemListUI/Sources/ItemListEditableReorderControlNode.swift @@ -19,8 +19,8 @@ public final class ItemListEditableReorderControlNode: ASDisplayNode { self.addSubnode(self.iconNode) } - public static func asyncLayout(_ node: ItemListEditableReorderControlNode?) -> (_ height: CGFloat, _ theme: PresentationTheme) -> (CGSize, (Bool) -> ItemListEditableReorderControlNode) { - return { height, theme in + public static func asyncLayout(_ node: ItemListEditableReorderControlNode?) -> (_ theme: PresentationTheme) -> (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode) { + return { theme in let image = PresentationResourcesItemList.itemListReorderIndicatorIcon(theme) let resultNode: ItemListEditableReorderControlNode @@ -31,9 +31,9 @@ public final class ItemListEditableReorderControlNode: ASDisplayNode { } resultNode.iconNode.image = image - return (CGSize(width: 40.0, height: height), { offsetForLabel in + return (40.0, { height, offsetForLabel, transition in if let image = image { - resultNode.iconNode.frame = CGRect(origin: CGPoint(x: 7.0, y: floor((height - image.size.height) / 2.0) - (offsetForLabel ? 6.0 : 0.0)), size: image.size) + transition.updateFrame(node: resultNode.iconNode, frame: CGRect(origin: CGPoint(x: 7.0, y: floor((height - image.size.height) / 2.0) - (offsetForLabel ? 6.0 : 0.0)), size: image.size)) } return resultNode }) diff --git a/submodules/ItemListUI/Sources/ItemListItem.swift b/submodules/ItemListUI/Sources/ItemListItem.swift index 6cb50af62e..2629683f2b 100644 --- a/submodules/ItemListUI/Sources/ItemListItem.swift +++ b/submodules/ItemListUI/Sources/ItemListItem.swift @@ -1,6 +1,8 @@ import Foundation import UIKit import Display +import TelegramUIPreferences +import TelegramPresentationData public protocol ItemListItemTag { func isEqual(to other: ItemListItemTag) -> Bool @@ -148,3 +150,78 @@ public func itemListNeighborsGroupedInsets(_ neighbors: ItemListNeighbors) -> UI public func itemListHasRoundedBlockLayout(_ params: ListViewItemLayoutParams) -> Bool { return params.width > 480.0 } + +public final class ItemListPresentationData: Equatable { + public let theme: PresentationTheme + public let fontSize: PresentationFontSize + public let strings: PresentationStrings + + public init(theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings) { + self.theme = theme + self.fontSize = fontSize + self.strings = strings + } + + public static func ==(lhs: ItemListPresentationData, rhs: ItemListPresentationData) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.fontSize != rhs.fontSize { + return false + } + return true + } +} + +public extension PresentationFontSize { + var itemListBaseHeaderFontSize: CGFloat { + return floor(self.itemListBaseFontSize * 13.0 / 17.0) + } + + var itemListBaseFontSize: CGFloat { + switch self { + case .extraSmall: + return 14.0 + case .small: + return 15.0 + case .medium: + return 16.0 + case .regular: + return 17.0 + case .large: + return 19.0 + case .extraLarge: + return 23.0 + case .extraLargeX2: + return 26.0 + } + } + + var itemListBaseLabelFontSize: CGFloat { + switch self { + case .extraSmall: + return 11.0 + case .small: + return 12.0 + case .medium: + return 13.0 + case .regular: + return 14.0 + case .large: + return 16.0 + case .extraLarge: + return 20.0 + case .extraLargeX2: + return 23.0 + } + } +} + +public extension ItemListPresentationData { + convenience init(_ presentationData: PresentationData) { + self.init(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings) + } +} diff --git a/submodules/ItemListUI/Sources/Items/ItemListActionItem.swift b/submodules/ItemListUI/Sources/Items/ItemListActionItem.swift index e3e451f717..9bd3ebe49c 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListActionItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListActionItem.swift @@ -18,7 +18,7 @@ public enum ItemListActionAlignment { } public class ItemListActionItem: ListViewItem, ItemListItem { - let theme: PresentationTheme + let presentationData: ItemListPresentationData let title: String let kind: ItemListActionKind let alignment: ItemListActionAlignment @@ -29,8 +29,8 @@ public class ItemListActionItem: ListViewItem, ItemListItem { let clearHighlightAutomatically: Bool public let tag: Any? - public init(theme: PresentationTheme, title: String, kind: ItemListActionKind, alignment: ItemListActionAlignment, sectionId: ItemListSectionId, style: ItemListStyle, action: @escaping () -> Void, longTapAction: (() -> Void)? = nil, clearHighlightAutomatically: Bool = true, tag: Any? = nil) { - self.theme = theme + public init(presentationData: ItemListPresentationData, title: String, kind: ItemListActionKind, alignment: ItemListActionAlignment, sectionId: ItemListSectionId, style: ItemListStyle, action: @escaping () -> Void, longTapAction: (() -> Void)? = nil, clearHighlightAutomatically: Bool = true, tag: Any? = nil) { + self.presentationData = presentationData self.title = title self.kind = kind self.alignment = alignment @@ -85,8 +85,6 @@ public class ItemListActionItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(17.0) - public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -138,22 +136,24 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { let currentItem = self.item return { item, params, neighbors in + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } let textColor: UIColor switch item.kind { - case .destructive: - textColor = item.theme.list.itemDestructiveColor - case .generic: - textColor = item.theme.list.itemAccentColor - case .neutral: - textColor = item.theme.list.itemPrimaryTextColor - case .disabled: - textColor = item.theme.list.itemDisabledTextColor + case .destructive: + textColor = item.presentationData.theme.list.itemDestructiveColor + case .generic: + textColor = item.presentationData.theme.list.itemAccentColor + case .neutral: + textColor = item.presentationData.theme.list.itemPrimaryTextColor + case .disabled: + textColor = item.presentationData.theme.list.itemDisabledTextColor } let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -165,16 +165,16 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { let itemBackgroundColor: UIColor let itemSeparatorColor: UIColor switch item.style { - case .plain: - itemBackgroundColor = item.theme.list.plainBackgroundColor - itemSeparatorColor = item.theme.list.itemPlainSeparatorColor - contentSize = CGSize(width: params.width, height: 44.0) - insets = itemListNeighborsPlainInsets(neighbors) - case .blocks: - itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor - itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor - contentSize = CGSize(width: params.width, height: 44.0) - insets = itemListNeighborsGroupedInsets(neighbors) + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: titleLayout.size.height + 22.0) + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: titleLayout.size.height + 22.0) + insets = itemListNeighborsGroupedInsets(neighbors) } let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -199,7 +199,7 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { strongSelf.topStripeNode.backgroundColor = itemSeparatorColor strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } let _ = titleApply() @@ -258,7 +258,7 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) @@ -267,13 +267,13 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { } switch item.alignment { - case .natural: - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) - case .center: - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((params.width - params.leftInset - params.rightInset - titleLayout.size.width) / 2.0), y: 11.0), size: titleLayout.size) + case .natural: + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) + case .center: + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((params.width - params.leftInset - params.rightInset - titleLayout.size.width) / 2.0), y: 11.0), size: titleLayout.size) } - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)) } }) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListActivityTextItem.swift b/submodules/ItemListUI/Sources/Items/ItemListActivityTextItem.swift index 260e05dc3a..f65e1fbcc9 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListActivityTextItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListActivityTextItem.swift @@ -8,15 +8,15 @@ import ActivityIndicator public class ItemListActivityTextItem: ListViewItem, ItemListItem { let displayActivity: Bool - let theme: PresentationTheme + let presentationData: ItemListPresentationData let text: NSAttributedString public let sectionId: ItemListSectionId public let isAlwaysPlain: Bool = true - public init(displayActivity: Bool, theme: PresentationTheme, text: NSAttributedString, sectionId: ItemListSectionId) { + public init(displayActivity: Bool, presentationData: ItemListPresentationData, text: NSAttributedString, sectionId: ItemListSectionId) { self.displayActivity = displayActivity - self.theme = theme + self.presentationData = presentationData self.text = text self.sectionId = sectionId } @@ -58,8 +58,6 @@ public class ItemListActivityTextItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(14.0) - public class ItemListActivityTextItemNode: ListViewItemNode { private let titleNode: TextNode private let activityIndicator: ActivityIndicator @@ -87,6 +85,8 @@ public class ItemListActivityTextItemNode: ListViewItemNode { let leftInset: CGFloat = 12.0 + params.leftInset let verticalInset: CGFloat = 7.0 + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize) + var activityWidth: CGFloat = 0.0 if item.displayActivity { activityWidth = 25.0 @@ -99,7 +99,7 @@ public class ItemListActivityTextItemNode: ListViewItemNode { titleString.addAttributes([NSAttributedString.Key.font: titleFont], range: NSMakeRange(0, titleString.length)) } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - 22.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: TextNodeCutout(topLeft: CGSize(width: activityWidth, height: 4.0)), insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - 22.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: TextNodeCutout(topLeft: CGSize(width: activityWidth, height: 22.0)), insets: UIEdgeInsets())) let contentSize: CGSize let insets: UIEdgeInsets @@ -116,9 +116,9 @@ public class ItemListActivityTextItemNode: ListViewItemNode { let _ = titleApply() strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size) - strongSelf.activityIndicator.frame = CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: CGSize(width: 16.0, height: 16.0)) + strongSelf.activityIndicator.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((contentSize.height - 16.0) / 2.0)), size: CGSize(width: 16.0, height: 16.0)) - strongSelf.activityIndicator.type = .custom(item.theme.list.itemAccentColor, 16.0, 2.0, false) + strongSelf.activityIndicator.type = .custom(item.presentationData.theme.list.itemAccentColor, 16.0, 2.0, false) if item.displayActivity { strongSelf.activityIndicator.isHidden = false diff --git a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift index 25558f9c3d..666929fccb 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift @@ -16,7 +16,7 @@ public enum ItemListCheckboxItemColor { } public class ItemListCheckboxItem: ListViewItem, ItemListItem { - let theme: PresentationTheme + let presentationData: ItemListPresentationData let title: String let style: ItemListCheckboxItemStyle let color: ItemListCheckboxItemColor @@ -25,8 +25,8 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem { public let sectionId: ItemListSectionId let action: () -> Void - public init(theme: PresentationTheme, title: String, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { - self.theme = theme + public init(presentationData: ItemListPresentationData, title: String, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { + self.presentationData = presentationData self.title = title self.style = style self.color = color @@ -77,8 +77,6 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(17.0) - public class ItemListCheckboxItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -141,34 +139,36 @@ public class ItemListCheckboxItemNode: ListViewItemNode { var leftInset: CGFloat = params.leftInset switch item.style { - case .left: - leftInset += 44.0 - case .right: - leftInset += 16.0 + case .left: + leftInset += 44.0 + case .right: + leftInset += 16.0 } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let separatorHeight = UIScreenPixel let insets = itemListNeighborsGroupedInsets(neighbors) - let contentSize = CGSize(width: params.width, height: 44.0) + let contentSize = CGSize(width: params.width, height: titleLayout.size.height + 22.0) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) var updateCheckImage: UIImage? var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } - if currentItem?.theme !== item.theme || currentItem?.color != item.color { + if currentItem?.presentationData.theme !== item.presentationData.theme || currentItem?.color != item.color { switch item.color { - case .accent: - updateCheckImage = PresentationResourcesItemList.checkIconImage(item.theme) - case .secondary: - updateCheckImage = PresentationResourcesItemList.secondaryCheckIconImage(item.theme) + case .accent: + updateCheckImage = PresentationResourcesItemList.checkIconImage(item.presentationData.theme) + case .secondary: + updateCheckImage = PresentationResourcesItemList.secondaryCheckIconImage(item.presentationData.theme) } } @@ -190,20 +190,20 @@ public class ItemListCheckboxItemNode: ListViewItemNode { } if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } let _ = titleApply() if let image = strongSelf.iconNode.image { switch item.style { - case .left: - strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) - case .right: - strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - image.size.width - floor((44.0 - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) + case .left: + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) + case .right: + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - image.size.width - floor((44.0 - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) } } strongSelf.iconNode.isHidden = !item.checked @@ -244,7 +244,7 @@ public class ItemListCheckboxItemNode: ListViewItemNode { } } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index 7da5f7ca78..95424291cf 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -24,7 +24,7 @@ public enum ItemListDisclosureLabelStyle { } public class ItemListDisclosureItem: ListViewItem, ItemListItem { - let theme: PresentationTheme + let presentationData: ItemListPresentationData let icon: UIImage? let title: String let titleColor: ItemListDisclosureItemTitleColor @@ -38,8 +38,8 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { let clearHighlightAutomatically: Bool public let tag: ItemListItemTag? - public init(theme: PresentationTheme, icon: UIImage? = nil, title: String, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, label: String, labelStyle: ItemListDisclosureLabelStyle = .text, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil) { - self.theme = theme + public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, title: String, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, label: String, labelStyle: ItemListDisclosureLabelStyle = .text, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil) { + self.presentationData = presentationData self.icon = icon self.title = title self.titleColor = titleColor @@ -99,9 +99,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(17.0) private let badgeFont = Font.regular(15.0) -private let detailFont = Font.regular(13.0) public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode @@ -190,12 +188,12 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { let currentHasBadge = self.labelBadgeNode.image != nil return { item, params, neighbors in - let rightInset: CGFloat + var rightInset: CGFloat switch item.disclosureStyle { - case .none: - rightInset = 16.0 + params.rightInset - case .arrow: - rightInset = 34.0 + params.rightInset + case .none: + rightInset = 16.0 + params.rightInset + case .arrow: + rightInset = 34.0 + params.rightInset } var updateArrowImage: UIImage? @@ -221,9 +219,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { } let badgeDiameter: CGFloat = 20.0 - if currentItem?.theme !== item.theme { - updatedTheme = item.theme - updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.theme) + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) if let badgeColor = badgeColor { updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor) } @@ -247,29 +245,41 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { leftInset += 43.0 } - let titleColor: UIColor - if item.enabled { - titleColor = item.titleColor == .accent ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor - } else { - titleColor = item.theme.list.itemDisabledTextColor + var additionalTextRightInset: CGFloat = 0.0 + switch item.labelStyle { + case .badge: + additionalTextRightInset += 44.0 + default: + break } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let titleColor: UIColor + if item.enabled { + titleColor = item.titleColor == .accent ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemPrimaryTextColor + } else { + titleColor = item.presentationData.theme.list.itemDisabledTextColor + } + + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - additionalTextRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let detailFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0)) let labelFont: UIFont let labelBadgeColor: UIColor var labelConstrain: CGFloat = params.width - params.rightInset - leftInset - 40.0 - titleLayout.size.width - 10.0 switch item.labelStyle { - case .badge: - labelBadgeColor = item.theme.list.itemCheckColors.foregroundColor - labelFont = badgeFont - case .detailText, .multilineDetailText: - labelBadgeColor = item.theme.list.itemSecondaryTextColor - labelFont = detailFont - labelConstrain = params.width - params.rightInset - 40.0 - leftInset - default: - labelBadgeColor = item.theme.list.itemSecondaryTextColor - labelFont = titleFont + case .badge: + labelBadgeColor = item.presentationData.theme.list.itemCheckColors.foregroundColor + labelFont = badgeFont + case .detailText, .multilineDetailText: + labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor + labelFont = detailFont + labelConstrain = params.width - params.rightInset - 40.0 - leftInset + default: + labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor + labelFont = titleFont } var multilineLabel = false if case .multilineDetailText = item.labelStyle { @@ -278,27 +288,30 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor:labelBadgeColor), backgroundColor: nil, maximumNumberOfLines: multilineLabel ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: labelConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let verticalInset: CGFloat = 11.0 + let titleSpacing: CGFloat = 3.0 + let height: CGFloat switch item.labelStyle { - case .detailText: - height = 64.0 - case .multilineDetailText: - height = 44.0 + labelLayout.size.height - default: - height = 44.0 + case .detailText: + height = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height + case .multilineDetailText: + height = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height + default: + height = verticalInset * 2.0 + titleLayout.size.height } switch item.style { - case .plain: - itemBackgroundColor = item.theme.list.plainBackgroundColor - itemSeparatorColor = item.theme.list.itemPlainSeparatorColor - contentSize = CGSize(width: params.width, height: height) - insets = itemListNeighborsPlainInsets(neighbors) - case .blocks: - itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor - itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor - contentSize = CGSize(width: params.width, height: height) - insets = itemListNeighborsGroupedInsets(neighbors) + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: height) + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: height) + insets = itemListNeighborsGroupedInsets(neighbors) } let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -343,70 +356,71 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { strongSelf.topStripeNode.backgroundColor = itemSeparatorColor strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } let _ = titleApply() let _ = labelApply() switch item.style { - case .plain: - if strongSelf.backgroundNode.supernode != nil { - strongSelf.backgroundNode.removeFromSupernode() - } - if strongSelf.topStripeNode.supernode != nil { - strongSelf.topStripeNode.removeFromSupernode() - } - if strongSelf.bottomStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) - } - if strongSelf.maskNode.supernode != nil { - strongSelf.maskNode.removeFromSupernode() - } - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) - case .blocks: - if strongSelf.backgroundNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) - } - if strongSelf.topStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) - } - if strongSelf.bottomStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) - } - if strongSelf.maskNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.maskNode, at: 3) - } - - let hasCorners = itemListHasRoundedBlockLayout(params) - var hasTopCorners = false - var hasBottomCorners = false - switch neighbors.top { - case .sameSection(false): - strongSelf.topStripeNode.isHidden = true - default: - hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners - } - let bottomStripeInset: CGFloat - switch neighbors.bottom { - case .sameSection(false): - bottomStripeInset = leftInset - default: - bottomStripeInset = 0.0 - hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners - } - - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil - - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) - strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + if strongSelf.maskNode.supernode != nil { + strongSelf.maskNode.removeFromSupernode() + } + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + default: + bottomStripeInset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) } - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) + strongSelf.titleNode.frame = titleFrame if let updateBadgeImage = updatedLabelBadgeImage { if strongSelf.labelBadgeNode.supernode == nil { @@ -420,14 +434,15 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { } let badgeWidth = max(badgeDiameter, labelLayout.size.width + 10.0) - strongSelf.labelBadgeNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth, y: 12.0), size: CGSize(width: badgeWidth, height: badgeDiameter)) + let badgeFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth, y: floor((contentSize.height - badgeDiameter) / 2.0)), size: CGSize(width: badgeWidth, height: badgeDiameter)) + strongSelf.labelBadgeNode.frame = badgeFrame let labelFrame: CGRect switch item.labelStyle { case .badge: - labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: 13.0), size: labelLayout.size) + labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: badgeFrame.minY + 1), size: labelLayout.size) case .detailText, .multilineDetailText: - labelFrame = CGRect(origin: CGPoint(x: leftInset, y: 36.0), size: labelLayout.size) + labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size) default: labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: 11.0), size: labelLayout.size) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListEditableItem.swift b/submodules/ItemListUI/Sources/Items/ItemListEditableItem.swift index efd9e4e4c2..dc6f9a1a6f 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListEditableItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListEditableItem.swift @@ -82,6 +82,10 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, UIGestureRecognizerD super.init(layerBacked: layerBacked, dynamicBounce: dynamicBounce, rotated: rotated, seeThrough: seeThrough) } + open var controlsContainer: ASDisplayNode { + return self + } + override open func didLoad() { super.didLoad() @@ -310,7 +314,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, UIGestureRecognizerD revealNode.updateRevealOffset(offset: 0.0, sideInset: leftInset, transition: .immediate) } - self.addSubnode(revealNode) + self.controlsContainer.addSubnode(revealNode) } } @@ -332,7 +336,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, UIGestureRecognizerD revealNode.updateRevealOffset(offset: 0.0, sideInset: -rightInset, transition: .immediate) } - self.addSubnode(revealNode) + self.controlsContainer.addSubnode(revealNode) } } @@ -492,4 +496,8 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, UIGestureRecognizerD } self.hapticFeedback?.impact(.medium) } + + override open func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) { + super.animateFrameTransition(progress, currentValue) + } } diff --git a/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift b/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift index 0cc2c753b4..717b8c2b51 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift @@ -19,15 +19,15 @@ public enum InfoListItemLinkAction { public class InfoListItem: ListViewItem { public let selectable: Bool = false - let theme: PresentationTheme + let presentationData: ItemListPresentationData let title: String let text: InfoListItemText let style: ItemListStyle let linkAction: ((InfoListItemLinkAction) -> Void)? let closeAction: (() -> Void)? - public init(theme: PresentationTheme, title: String, text: InfoListItemText, style: ItemListStyle, linkAction: ((InfoListItemLinkAction) -> Void)? = nil, closeAction: (() -> Void)?) { - self.theme = theme + public init(presentationData: ItemListPresentationData, title: String, text: InfoListItemText, style: ItemListStyle, linkAction: ((InfoListItemLinkAction) -> Void)? = nil, closeAction: (() -> Void)?) { + self.presentationData = presentationData self.title = title self.text = text self.style = style @@ -72,9 +72,9 @@ public class InfoListItem: ListViewItem { public class ItemListInfoItem: InfoListItem, ItemListItem { public let sectionId: ItemListSectionId - public init(theme: PresentationTheme, title: String, text: InfoListItemText, style: ItemListStyle, sectionId: ItemListSectionId, linkAction: ((InfoListItemLinkAction) -> Void)? = nil, closeAction: (() -> Void)?) { + public init(presentationData: ItemListPresentationData, title: String, text: InfoListItemText, style: ItemListStyle, sectionId: ItemListSectionId, linkAction: ((InfoListItemLinkAction) -> Void)? = nil, closeAction: (() -> Void)?) { self.sectionId = sectionId - super.init(theme: theme, title: title, text: text, style: style, linkAction: linkAction, closeAction: closeAction) + super.init(presentationData: presentationData, title: title, text: text, style: style, linkAction: linkAction, closeAction: closeAction) } override public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -111,11 +111,6 @@ public class ItemListInfoItem: InfoListItem, ItemListItem { } } -private let titleFont = Font.semibold(17.0) -private let textFont = Font.regular(16.0) -private let textBoldFont = Font.semibold(16.0) -private let badgeFont = Font.regular(15.0) - class InfoItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -211,6 +206,11 @@ class InfoItemNode: ListViewItemNode { return { item, params, neighbors in let leftInset: CGFloat = 15.0 + params.leftInset let rightInset: CGFloat = 15.0 + params.rightInset + + let titleFont = Font.semibold(item.presentationData.fontSize.itemListBaseFontSize) + let textFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize) + let textBoldFont = Font.semibold(item.presentationData.fontSize.itemListBaseHeaderFontSize) + let badgeFont = Font.regular(15.0) var updatedTheme: PresentationTheme? var updatedBadgeImage: UIImage? @@ -218,10 +218,10 @@ class InfoItemNode: ListViewItemNode { var updatedCloseIcon: UIImage? let badgeDiameter: CGFloat = 20.0 - if currentItem?.theme !== item.theme { - updatedTheme = item.theme - updatedBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: item.theme.list.itemDestructiveColor) - updatedCloseIcon = PresentationResourcesItemList.itemListCloseIconImage(item.theme) + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + updatedBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: item.presentationData.theme.list.itemDestructiveColor) + updatedCloseIcon = PresentationResourcesItemList.itemListCloseIconImage(item.presentationData.theme) } let insets: UIEdgeInsets @@ -236,27 +236,27 @@ class InfoItemNode: ListViewItemNode { let itemSeparatorColor: UIColor switch item.style { - case .plain: - itemBackgroundColor = item.theme.list.plainBackgroundColor - itemSeparatorColor = item.theme.list.itemPlainSeparatorColor - case .blocks: - itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor - itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor } let attributedText: NSAttributedString switch item.text { - case let .plain(text): - attributedText = NSAttributedString(string: text, font: textFont, textColor: item.theme.list.itemPrimaryTextColor) - case let .markdown(text): - attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.itemPrimaryTextColor), bold: MarkdownAttributeSet(font: textBoldFont, textColor: item.theme.list.itemPrimaryTextColor), link: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in - return (TelegramTextAttributes.URL, contents) - })) + case let .plain(text): + attributedText = NSAttributedString(string: text, font: textFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + case let .markdown(text): + attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), bold: MarkdownAttributeSet(font: textBoldFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), link: MarkdownAttributeSet(font: textFont, textColor: item.presentationData.theme.list.itemAccentColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + })) } - let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "!", font: badgeFont, textColor: item.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: badgeDiameter, height: badgeDiameter), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - badgeDiameter - 8.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "!", font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: badgeDiameter, height: badgeDiameter), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - badgeDiameter - 8.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let contentSize = CGSize(width: params.width, height: titleLayout.size.height + textLayout.size.height + 36.0) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -320,7 +320,7 @@ class InfoItemNode: ListViewItemNode { strongSelf.closeButton.isHidden = item.closeAction == nil - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) @@ -338,7 +338,7 @@ class InfoItemNode: ListViewItemNode { strongSelf.closeButton.setImage(updatedCloseIcon, for: []) } - strongSelf.badgeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 15.0), size: CGSize(width: badgeDiameter, height: badgeDiameter)) + strongSelf.badgeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 15.0 + floor((titleLayout.size.height - badgeDiameter) / 2.0)), size: CGSize(width: badgeDiameter, height: badgeDiameter)) strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: strongSelf.badgeNode.frame.midX - labelLayout.size.width / 2.0, y: strongSelf.badgeNode.frame.minY + 1.0), size: labelLayout.size) @@ -420,7 +420,7 @@ class InfoItemNode: ListViewItemNode { if let current = self.linkHighlightingNode { linkHighlightingNode = current } else { - linkHighlightingNode = LinkHighlightingNode(color: item.theme.list.itemAccentColor.withAlphaComponent(0.5)) + linkHighlightingNode = LinkHighlightingNode(color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.5)) self.linkHighlightingNode = linkHighlightingNode self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListMultilineInputItem.swift b/submodules/ItemListUI/Sources/Items/ItemListMultilineInputItem.swift index 078dd251e0..8b26cbd59a 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListMultilineInputItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListMultilineInputItem.swift @@ -33,7 +33,7 @@ public struct ItemListMultilineInputInlineAction { } public class ItemListMultilineInputItem: ListViewItem, ItemListItem { - let theme: PresentationTheme + let presentationData: ItemListPresentationData let text: String let placeholder: String public let sectionId: ItemListSectionId @@ -51,8 +51,8 @@ public class ItemListMultilineInputItem: ListViewItem, ItemListItem { let inlineAction: ItemListMultilineInputInlineAction? public let tag: ItemListItemTag? - public init(theme: PresentationTheme, text: String, placeholder: String, maxLength: ItemListMultilineInputItemTextLimit?, sectionId: ItemListSectionId, style: ItemListStyle, capitalization: Bool = true, autocorrection: Bool = true, returnKeyType: UIReturnKeyType = .default, minimalHeight: CGFloat? = nil, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> Void)? = nil, updatedFocus: ((Bool) -> Void)? = nil, tag: ItemListItemTag? = nil, action: (() -> Void)? = nil, inlineAction: ItemListMultilineInputInlineAction? = nil) { - self.theme = theme + public init(presentationData: ItemListPresentationData, text: String, placeholder: String, maxLength: ItemListMultilineInputItemTextLimit?, sectionId: ItemListSectionId, style: ItemListStyle, capitalization: Bool = true, autocorrection: Bool = true, returnKeyType: UIReturnKeyType = .default, minimalHeight: CGFloat? = nil, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> Void)? = nil, updatedFocus: ((Bool) -> Void)? = nil, tag: ItemListItemTag? = nil, action: (() -> Void)? = nil, inlineAction: ItemListMultilineInputInlineAction? = nil) { + self.presentationData = presentationData self.text = text self.placeholder = placeholder self.maxLength = maxLength @@ -105,8 +105,6 @@ public class ItemListMultilineInputItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(17.0) - public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelegate, ItemListItemNode, ItemListItemFocusableNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -159,9 +157,11 @@ public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNod var textColor: UIColor = .black if let item = self.item { - textColor = item.theme.list.itemPrimaryTextColor + textColor = item.presentationData.theme.list.itemPrimaryTextColor + self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor] + } else { + self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: textColor] } - self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: textColor] self.textNode.clipsToBounds = true self.textNode.delegate = self self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) @@ -175,8 +175,8 @@ public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNod return { item, params, neighbors in var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } let itemBackgroundColor: UIColor @@ -185,11 +185,11 @@ public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNod let leftInset = 16.0 + params.rightInset switch item.style { case .plain: - itemBackgroundColor = item.theme.list.plainBackgroundColor - itemSeparatorColor = item.theme.list.itemPlainSeparatorColor + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor case .blocks: - itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor - itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor } var limitTextString: NSAttributedString? @@ -206,7 +206,7 @@ public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNod let displayTextLimit = textLength > maxLength.value * 70 / 100 let remainingCount = maxLength.value - textLength if displayTextLimit { - limitTextString = NSAttributedString(string: "\(remainingCount)", font: Font.regular(13.0), textColor: remainingCount < 0 ? item.theme.list.itemDestructiveColor : item.theme.list.itemSecondaryTextColor) + limitTextString = NSAttributedString(string: "\(remainingCount)", font: Font.regular(13.0), textColor: remainingCount < 0 ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemSecondaryTextColor) } rightInset += 30.0 + 4.0 @@ -226,8 +226,8 @@ public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNod if measureText.hasSuffix("\n") || measureText.isEmpty { measureText += "|" } - let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), textColor: .black) - let attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor) + let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: .black) + let attributedText = NSAttributedString(string: item.text, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPrimaryTextColor) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedMeasureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 16.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) let separatorHeight = UIScreenPixel @@ -246,7 +246,7 @@ public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNod let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size - let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor) + let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPlaceholderTextColor) return (layout, { [weak self] in if let strongSelf = self { @@ -259,12 +259,12 @@ public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNod strongSelf.backgroundNode.backgroundColor = itemBackgroundColor if strongSelf.isNodeLoaded { - strongSelf.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: item.theme.list.itemPrimaryTextColor] - strongSelf.textNode.tintColor = item.theme.list.itemAccentColor + strongSelf.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), NSAttributedString.Key.foregroundColor.rawValue: item.presentationData.theme.list.itemPrimaryTextColor] + strongSelf.textNode.tintColor = item.presentationData.theme.list.itemAccentColor } if let inlineAction = item.inlineAction { - strongSelf.inlineActionButtonNode?.setImage(generateTintedImage(image: inlineAction.icon, color: item.theme.list.itemAccentColor), for: .normal) + strongSelf.inlineActionButtonNode?.setImage(generateTintedImage(image: inlineAction.icon, color: item.presentationData.theme.list.itemAccentColor), for: .normal) } } @@ -323,10 +323,8 @@ public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNod strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) @@ -334,9 +332,13 @@ public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNod strongSelf.textNode.attributedPlaceholderText = attributedPlaceholderText } - strongSelf.textNode.keyboardAppearance = item.theme.rootController.keyboardColor.keyboardAppearance + strongSelf.textNode.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance - strongSelf.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - params.rightInset, height: textLayout.size.height)) + if strongSelf.animationForKey("apparentHeight") == nil { + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - params.rightInset, height: textLayout.size.height)) + } strongSelf.textNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width - leftInset - 16.0 - rightInset, height: textLayout.size.height + 1.0)) let _ = limitTextApply() @@ -355,7 +357,7 @@ public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNod inlineActionButtonNode = currentInlineActionButtonNode } else { inlineActionButtonNode = HighlightableButtonNode() - inlineActionButtonNode.setImage(generateTintedImage(image: inlineAction.icon, color: item.theme.list.itemAccentColor), for: .normal) + inlineActionButtonNode.setImage(generateTintedImage(image: inlineAction.icon, color: item.presentationData.theme.list.itemAccentColor), for: .normal) inlineActionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.inlineActionPressed), forControlEvents: .touchUpInside) strongSelf.addSubnode(inlineActionButtonNode) strongSelf.inlineActionButtonNode = inlineActionButtonNode @@ -394,6 +396,7 @@ public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNod let textBottomInset: CGFloat = 11.0 self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + self.maskNode.frame = self.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: self.bottomStripeNode.frame.minX, y: contentSize.height), size: CGSize(width: self.bottomStripeNode.frame.size.width, height: separatorHeight)) self.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: max(0.0, params.width - leftInset - params.rightInset), height: max(0.0, contentSize.height - textTopInset - textBottomInset))) @@ -431,7 +434,7 @@ public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNod if let item = self.item { if let text = self.textNode.attributedText { let updatedText = text.string - let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor) + let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPrimaryTextColor) if text.string != updatedAttributedText.string { self.textNode.attributedText = updatedAttributedText } diff --git a/submodules/ItemListUI/Sources/Items/ItemListMultilineTextItem.swift b/submodules/ItemListUI/Sources/Items/ItemListMultilineTextItem.swift index 7939fbb159..608641f99a 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListMultilineTextItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListMultilineTextItem.swift @@ -13,7 +13,7 @@ public enum ItemListMultilineTextBaseFont { } public class ItemListMultilineTextItem: ListViewItem, ItemListItem { - let theme: PresentationTheme + let presentationData: ItemListPresentationData let text: String let enabledEntityTypes: EnabledEntityTypes let font: ItemListMultilineTextBaseFont @@ -27,8 +27,8 @@ public class ItemListMultilineTextItem: ListViewItem, ItemListItem { public let selectable: Bool - public init(theme: PresentationTheme, text: String, enabledEntityTypes: EnabledEntityTypes, font: ItemListMultilineTextBaseFont = .default, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)? = nil, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) { - self.theme = theme + public init(presentationData: ItemListPresentationData, text: String, enabledEntityTypes: EnabledEntityTypes, font: ItemListMultilineTextBaseFont = .default, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)? = nil, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) { + self.presentationData = presentationData self.text = text self.enabledEntityTypes = enabledEntityTypes self.font = font @@ -81,12 +81,6 @@ public class ItemListMultilineTextItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(17.0) -private let titleBoldFont = Font.medium(17.0) -private let titleItalicFont = Font.italic(17.0) -private let titleBoldItalicFont = Font.semiboldItalic(17.0) -private let titleFixedFont = Font.regular(17.0) - public class ItemListMultilineTextItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -165,11 +159,11 @@ public class ItemListMultilineTextItemNode: ListViewItemNode { return { item, params, neighbors in var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } - let textColor: UIColor = item.theme.list.itemPrimaryTextColor + let textColor: UIColor = item.presentationData.theme.list.itemPrimaryTextColor let leftInset: CGFloat let itemBackgroundColor: UIColor @@ -177,30 +171,33 @@ public class ItemListMultilineTextItemNode: ListViewItemNode { switch item.style { case .plain: - itemBackgroundColor = item.theme.list.plainBackgroundColor - itemSeparatorColor = item.theme.list.itemPlainSeparatorColor + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor leftInset = 16.0 + params.leftInset case .blocks: - itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor - itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor leftInset = 16.0 + params.rightInset } - var baseFont = titleFont - var linkFont = titleFont - var boldFont = titleBoldFont - var italicFont = titleItalicFont - var boldItalicFont = titleBoldItalicFont + let fontSize = item.presentationData.fontSize.itemListBaseFontSize + + var baseFont = Font.regular(fontSize) + var linkFont = baseFont + var boldFont = Font.medium(fontSize) + var italicFont = Font.italic(fontSize) + var boldItalicFont = Font.semiboldItalic(fontSize) + let titleFixedFont = Font.monospace(fontSize) if case .monospace = item.font { - baseFont = Font.monospace(17.0) - linkFont = Font.monospace(17.0) - boldFont = Font.semiboldMonospace(17.0) - italicFont = Font.italicMonospace(17.0) - boldItalicFont = Font.semiboldItalicMonospace(17.0) + baseFont = Font.monospace(fontSize) + linkFont = Font.monospace(fontSize) + boldFont = Font.semiboldMonospace(fontSize) + italicFont = Font.italicMonospace(fontSize) + boldItalicFont = Font.semiboldItalicMonospace(fontSize) } let entities = generateTextEntities(item.text, enabledTypes: item.enabledEntityTypes) - let string = stringWithAppliedEntities(item.text, entities: entities, baseColor: textColor, linkColor: item.theme.list.itemAccentColor, baseFont: baseFont, linkFont: linkFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: titleFixedFont, blockQuoteFont: titleFont) + let string = stringWithAppliedEntities(item.text, entities: entities, baseColor: textColor, linkColor: item.presentationData.theme.list.itemAccentColor, baseFont: baseFont, linkFont: linkFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: titleFixedFont, blockQuoteFont: baseFont) let (titleLayout, titleApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -231,7 +228,7 @@ public class ItemListMultilineTextItemNode: ListViewItemNode { strongSelf.topStripeNode.backgroundColor = itemSeparatorColor strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } let _ = titleApply() @@ -289,7 +286,7 @@ public class ItemListMultilineTextItemNode: ListViewItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) @@ -416,7 +413,7 @@ public class ItemListMultilineTextItemNode: ListViewItemNode { if let current = self.linkHighlightingNode { linkHighlightingNode = current } else { - linkHighlightingNode = LinkHighlightingNode(color: item.theme.list.itemAccentColor.withAlphaComponent(0.5)) + linkHighlightingNode = LinkHighlightingNode(color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.5)) self.linkHighlightingNode = linkHighlightingNode self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift index 18d396b2b8..7433ec6112 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift @@ -39,7 +39,7 @@ public enum ItemListSectionHeaderActivityIndicator { } public class ItemListSectionHeaderItem: ListViewItem, ItemListItem { - let theme: PresentationTheme + let presentationData: ItemListPresentationData let text: String let multiline: Bool let activityIndicator: ItemListSectionHeaderActivityIndicator @@ -48,8 +48,8 @@ public class ItemListSectionHeaderItem: ListViewItem, ItemListItem { public let isAlwaysPlain: Bool = true - public init(theme: PresentationTheme, text: String, multiline: Bool = false, activityIndicator: ItemListSectionHeaderActivityIndicator = .none, accessoryText: ItemListSectionHeaderAccessoryText? = nil, sectionId: ItemListSectionId) { - self.theme = theme + public init(presentationData: ItemListPresentationData, text: String, multiline: Bool = false, activityIndicator: ItemListSectionHeaderActivityIndicator = .none, accessoryText: ItemListSectionHeaderAccessoryText? = nil, sectionId: ItemListSectionId) { + self.presentationData = presentationData self.text = text self.multiline = multiline self.activityIndicator = activityIndicator @@ -93,8 +93,6 @@ public class ItemListSectionHeaderItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(14.0) - public class ItemListSectionHeaderItemNode: ListViewItemNode { private var item: ItemListSectionHeaderItem? @@ -135,16 +133,18 @@ public class ItemListSectionHeaderItemNode: ListViewItemNode { return { item, params, neighbors in let leftInset: CGFloat = 15.0 + params.leftInset - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var accessoryTextString: NSAttributedString? var accessoryIcon: UIImage? if let accessoryText = item.accessoryText { let color: UIColor switch accessoryText.color { - case .generic: - color = item.theme.list.sectionHeaderTextColor - case .destructive: - color = item.theme.list.freeTextErrorColor + case .generic: + color = item.presentationData.theme.list.sectionHeaderTextColor + case .destructive: + color = item.presentationData.theme.list.freeTextErrorColor } accessoryTextString = NSAttributedString(string: accessoryText.value, font: titleFont, textColor: color) accessoryIcon = accessoryText.icon @@ -208,7 +208,7 @@ public class ItemListSectionHeaderItemNode: ListViewItemNode { if let currentActivityIndicator = strongSelf.activityIndicator { activityIndicator = currentActivityIndicator } else { - activityIndicator = ActivityIndicator(type: .custom(item.theme.list.sectionHeaderTextColor, 18.0, 1.0, false)) + activityIndicator = ActivityIndicator(type: .custom(item.presentationData.theme.list.sectionHeaderTextColor, 18.0, 1.0, false)) strongSelf.addSubnode(activityIndicator) strongSelf.activityIndicator = activityIndicator } @@ -227,12 +227,12 @@ public class ItemListSectionHeaderItemNode: ListViewItemNode { var activityIndicatorOrigin: CGPoint? switch item.activityIndicator { - case .left: - activityIndicatorOrigin = CGPoint(x: strongSelf.titleNode.frame.maxX + 6.0, y: 7.0 - UIScreenPixel) - case .right: - activityIndicatorOrigin = CGPoint(x: params.width - leftInset - 18.0, y: 7.0 - UIScreenPixel) - default: - break + case .left: + activityIndicatorOrigin = CGPoint(x: strongSelf.titleNode.frame.maxX + 6.0, y: 7.0 - UIScreenPixel) + case .right: + activityIndicatorOrigin = CGPoint(x: params.width - leftInset - 18.0, y: 7.0 - UIScreenPixel) + default: + break } if let activityIndicatorOrigin = activityIndicatorOrigin { strongSelf.activityIndicator?.frame = CGRect(origin: activityIndicatorOrigin, size: CGSize(width: 18.0, height: 18.0)) diff --git a/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift index 3dda105fc4..5b8028f7a9 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift @@ -30,8 +30,7 @@ public enum ItemListSingleLineInputClearType: Equatable { } public class ItemListSingleLineInputItem: ListViewItem, ItemListItem { - let theme: PresentationTheme - let strings: PresentationStrings + let presentationData: ItemListPresentationData let title: NSAttributedString let text: String let placeholder: String @@ -48,9 +47,8 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem { let updatedFocus: ((Bool) -> Void)? public let tag: ItemListItemTag? - public init(theme: PresentationTheme, strings: PresentationStrings, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, enabled: Bool = true, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void) { - self.theme = theme - self.strings = strings + public init(presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, enabled: Bool = true, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void) { + self.presentationData = presentationData self.title = title self.text = text self.placeholder = placeholder @@ -103,8 +101,6 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(17.0) - public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, ItemListItemNode, ItemListItemFocusableNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -112,6 +108,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg private let maskNode: ASImageNode private let titleNode: TextNode + private let measureTitleSizeNode: TextNode private let textNode: TextFieldNode private let clearIconNode: ASImageNode private let clearButtonNode: HighlightableButtonNode @@ -135,6 +132,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg self.maskNode = ASImageNode() self.titleNode = TextNode() + self.measureTitleSizeNode = TextNode() self.textNode = TextFieldNode() self.clearIconNode = ASImageNode() @@ -168,14 +166,19 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg override public func didLoad() { super.didLoad() - self.textNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(17.0)] - self.textNode.textField.font = Font.regular(17.0) if let item = self.item { - self.textNode.textField.textColor = item.theme.list.itemPrimaryTextColor - self.textNode.textField.keyboardAppearance = item.theme.rootController.keyboardColor.keyboardAppearance - self.textNode.textField.tintColor = item.theme.list.itemAccentColor + self.textNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)] + self.textNode.textField.font = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + + self.textNode.textField.textColor = item.presentationData.theme.list.itemPrimaryTextColor + self.textNode.textField.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance + self.textNode.textField.tintColor = item.presentationData.theme.list.itemAccentColor self.textNode.textField.accessibilityHint = item.placeholder + } else { + self.textNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(17.0)] + self.textNode.textField.font = Font.regular(17.0) } + self.textNode.clipsToBounds = true self.textNode.textField.delegate = self self.textNode.textField.addTarget(self, action: #selector(self.textFieldTextChanged(_:)), for: .editingChanged) @@ -184,6 +187,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg public func asyncLayout() -> (_ item: ItemListSingleLineInputItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeMeasureTitleSizeLayout = TextNode.asyncLayout(self.measureTitleSizeNode) let currentItem = self.item @@ -191,9 +195,14 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg var updatedTheme: PresentationTheme? var updatedClearIcon: UIImage? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme - updatedClearIcon = PresentationResourcesItemList.itemListClearInputIcon(item.theme) + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + updatedClearIcon = PresentationResourcesItemList.itemListClearInputIcon(item.presentationData.theme) + } + + var fontUpdated = false + if currentItem?.presentationData.fontSize != item.presentationData.fontSize { + fontUpdated = true } let leftInset: CGFloat = 16.0 + params.leftInset @@ -205,77 +214,85 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg let titleString = NSMutableAttributedString(attributedString: item.title) titleString.removeAttribute(NSAttributedString.Key.font, range: NSMakeRange(0, titleString.length)) - titleString.addAttributes([NSAttributedString.Key.font: Font.regular(17.0)], range: NSMakeRange(0, titleString.length)) + titleString.addAttributes([NSAttributedString.Key.font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)], range: NSMakeRange(0, titleString.length)) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (measureTitleLayout, measureTitleSizeApply) = makeMeasureTitleSizeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "A", font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let separatorHeight = UIScreenPixel - let contentSize = CGSize(width: params.width, height: 44.0) + let contentSize = CGSize(width: params.width, height: max(titleLayout.size.height, measureTitleLayout.size.height) + 22.0) let insets = itemListNeighborsGroupedInsets(neighbors) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size - let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor) + let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPlaceholderTextColor) return (layout, { [weak self] in if let strongSelf = self { strongSelf.item = item if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor - strongSelf.textNode.textField.textColor = item.theme.list.itemPrimaryTextColor - strongSelf.textNode.textField.keyboardAppearance = item.theme.rootController.keyboardColor.keyboardAppearance - strongSelf.textNode.textField.tintColor = item.theme.list.itemAccentColor + strongSelf.textNode.textField.textColor = item.presentationData.theme.list.itemPrimaryTextColor + strongSelf.textNode.textField.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance + strongSelf.textNode.textField.tintColor = item.presentationData.theme.list.itemAccentColor + } + + if fontUpdated { + strongSelf.textNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)] } let _ = titleApply() strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) + let _ = measureTitleSizeApply() + let secureEntry: Bool let capitalizationType: UITextAutocapitalizationType let autocorrectionType: UITextAutocorrectionType let keyboardType: UIKeyboardType switch item.type { - case let .regular(capitalization, autocorrection): - secureEntry = false - capitalizationType = capitalization ? .sentences : .none - autocorrectionType = autocorrection ? .default : .no - keyboardType = .default - case .email: - secureEntry = false - capitalizationType = .none - autocorrectionType = .no - keyboardType = .emailAddress - case .password: - secureEntry = true - capitalizationType = .none - autocorrectionType = .no - keyboardType = .default - case .number: - secureEntry = false - capitalizationType = .none - autocorrectionType = .no - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - keyboardType = .asciiCapableNumberPad - } else { - keyboardType = .numberPad - } - case .decimal: - secureEntry = false - capitalizationType = .none - autocorrectionType = .no - keyboardType = .decimalPad - case .username: - secureEntry = false - capitalizationType = .none - autocorrectionType = .no - keyboardType = .asciiCapable + case let .regular(capitalization, autocorrection): + secureEntry = false + capitalizationType = capitalization ? .sentences : .none + autocorrectionType = autocorrection ? .default : .no + keyboardType = .default + case .email: + secureEntry = false + capitalizationType = .none + autocorrectionType = .no + keyboardType = .emailAddress + case .password: + secureEntry = true + capitalizationType = .none + autocorrectionType = .no + keyboardType = .default + case .number: + secureEntry = false + capitalizationType = .none + autocorrectionType = .no + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + keyboardType = .asciiCapableNumberPad + } else { + keyboardType = .numberPad + } + case .decimal: + secureEntry = false + capitalizationType = .none + autocorrectionType = .no + keyboardType = .decimalPad + case .username: + secureEntry = false + capitalizationType = .none + autocorrectionType = .no + keyboardType = .asciiCapable } if strongSelf.textNode.textField.isSecureTextEntry != secureEntry { @@ -302,7 +319,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg strongSelf.textNode.textField.text = item.text } - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width + item.spacing, y: floor((layout.contentSize.height - 40.0) / 2.0)), size: CGSize(width: max(1.0, params.width - (leftInset + rightInset + titleLayout.size.width + item.spacing)), height: 40.0)) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width + item.spacing, y: 1.0), size: CGSize(width: max(1.0, params.width - (leftInset + rightInset + titleLayout.size.width + item.spacing)), height: layout.contentSize.height - 2.0)) if let image = updatedClearIcon { strongSelf.clearIconNode.image = image @@ -349,7 +366,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) @@ -364,7 +381,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg strongSelf.textNode.isUserInteractionEnabled = item.enabled strongSelf.textNode.alpha = item.enabled ? 1.0 : 0.4 - strongSelf.clearButtonNode.accessibilityLabel = item.strings.VoiceOver_Editing_ClearText + strongSelf.clearButtonNode.accessibilityLabel = item.presentationData.strings.VoiceOver_Editing_ClearText } }) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift index f94511e73c..009f372aeb 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift @@ -12,12 +12,13 @@ public enum ItemListSwitchItemNodeType { } public class ItemListSwitchItem: ListViewItem, ItemListItem { - let theme: PresentationTheme + let presentationData: ItemListPresentationData let title: String let value: Bool let type: ItemListSwitchItemNodeType let enableInteractiveChanges: Bool let enabled: Bool + let disableLeadingInset: Bool let maximumNumberOfLines: Int public let sectionId: ItemListSectionId let style: ItemListStyle @@ -25,13 +26,14 @@ public class ItemListSwitchItem: ListViewItem, ItemListItem { let activatedWhileDisabled: () -> Void public let tag: ItemListItemTag? - public init(theme: PresentationTheme, title: String, value: Bool, type: ItemListSwitchItemNodeType = .regular, enableInteractiveChanges: Bool = true, enabled: Bool = true, maximumNumberOfLines: Int = 1, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void, activatedWhileDisabled: @escaping () -> Void = {}, tag: ItemListItemTag? = nil) { - self.theme = theme + public init(presentationData: ItemListPresentationData, title: String, value: Bool, type: ItemListSwitchItemNodeType = .regular, enableInteractiveChanges: Bool = true, enabled: Bool = true, disableLeadingInset: Bool = false, maximumNumberOfLines: Int = 1, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void, activatedWhileDisabled: @escaping () -> Void = {}, tag: ItemListItemTag? = nil) { + self.presentationData = presentationData self.title = title self.value = value self.type = type self.enableInteractiveChanges = enableInteractiveChanges self.enabled = enabled + self.disableLeadingInset = disableLeadingInset self.maximumNumberOfLines = maximumNumberOfLines self.sectionId = sectionId self.style = style @@ -78,8 +80,6 @@ public class ItemListSwitchItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(17.0) - private protocol ItemListSwitchNodeImpl { var frameColor: UIColor { get set } var contentColor: UIColor { get set } @@ -131,7 +131,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { return self.item?.tag } - init(type: ItemListSwitchItemNodeType) { + public init(type: ItemListSwitchItemNodeType) { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.backgroundColor = .white @@ -195,31 +195,38 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { return { item, params, neighbors in var contentSize: CGSize - let insets: UIEdgeInsets + var insets: UIEdgeInsets let separatorHeight = UIScreenPixel let itemBackgroundColor: UIColor let itemSeparatorColor: UIColor + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } switch item.style { - case .plain: - itemBackgroundColor = item.theme.list.plainBackgroundColor - itemSeparatorColor = item.theme.list.itemPlainSeparatorColor - contentSize = CGSize(width: params.width, height: 44.0) - insets = itemListNeighborsPlainInsets(neighbors) - case .blocks: - itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor - itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor - contentSize = CGSize(width: params.width, height: 44.0) - insets = itemListNeighborsGroupedInsets(neighbors) + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: 44.0) + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: 44.0) + insets = itemListNeighborsGroupedInsets(neighbors) } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: item.maximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 80.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + if item.disableLeadingInset { + insets.top = 0.0 + insets.bottom = 0.0 + } + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: item.maximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 80.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) contentSize.height = max(contentSize.height, titleLayout.size.height + 22.0) @@ -280,13 +287,13 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor - strongSelf.switchNode.frameColor = item.theme.list.itemSwitchColors.frameColor - strongSelf.switchNode.contentColor = item.theme.list.itemSwitchColors.contentColor - strongSelf.switchNode.handleColor = item.theme.list.itemSwitchColors.handleColor - strongSelf.switchNode.positiveContentColor = item.theme.list.itemSwitchColors.positiveColor - strongSelf.switchNode.negativeContentColor = item.theme.list.itemSwitchColors.negativeColor + strongSelf.switchNode.frameColor = item.presentationData.theme.list.itemSwitchColors.frameColor + strongSelf.switchNode.contentColor = item.presentationData.theme.list.itemSwitchColors.contentColor + strongSelf.switchNode.handleColor = item.presentationData.theme.list.itemSwitchColors.handleColor + strongSelf.switchNode.positiveContentColor = item.presentationData.theme.list.itemSwitchColors.positiveColor + strongSelf.switchNode.negativeContentColor = item.presentationData.theme.list.itemSwitchColors.negativeColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } let _ = titleApply() @@ -342,7 +349,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) @@ -350,14 +357,14 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) if let switchView = strongSelf.switchNode.view as? UISwitch { if strongSelf.switchNode.bounds.size.width.isZero { switchView.sizeToFit() } let switchSize = switchView.bounds.size - strongSelf.switchNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - switchSize.width - 15.0, y: 6.0), size: switchSize) + strongSelf.switchNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - switchSize.width - 15.0, y: floor((contentSize.height - switchSize.height) / 2.0)), size: switchSize) strongSelf.switchGestureNode.frame = strongSelf.switchNode.frame if switchView.isOn != item.value { switchView.setOn(item.value, animated: animated) diff --git a/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift b/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift index 6f67e65615..e8bf1de0d2 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift @@ -9,6 +9,7 @@ import Markdown public enum ItemListTextItemText { case plain(String) + case large(String) case markdown(String) } @@ -17,19 +18,21 @@ public enum ItemListTextItemLinkAction { } public class ItemListTextItem: ListViewItem, ItemListItem { - let theme: PresentationTheme + let presentationData: ItemListPresentationData let text: ItemListTextItemText public let sectionId: ItemListSectionId let linkAction: ((ItemListTextItemLinkAction) -> Void)? let style: ItemListStyle public let isAlwaysPlain: Bool = true + public let tag: ItemListItemTag? - public init(theme: PresentationTheme, text: ItemListTextItemText, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil, style: ItemListStyle = .blocks) { - self.theme = theme + public init(presentationData: ItemListPresentationData, text: ItemListTextItemText, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil, style: ItemListStyle = .blocks, tag: ItemListItemTag? = nil) { + self.presentationData = presentationData self.text = text self.sectionId = sectionId self.linkAction = linkAction self.style = style + self.tag = tag } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -69,15 +72,16 @@ public class ItemListTextItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(14.0) -private let titleBoldFont = Font.semibold(14.0) - -public class ItemListTextItemNode: ListViewItemNode { +public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { private let titleNode: TextNode private let activateArea: AccessibilityAreaNode private var item: ItemListTextItem? + public var tag: ItemListItemTag? { + return self.item?.tag + } + public init() { self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false @@ -108,23 +112,34 @@ public class ItemListTextItemNode: ListViewItemNode { return { item, params, neighbors in let leftInset: CGFloat = 15.0 + params.leftInset - let verticalInset: CGFloat = 7.0 + let topInset: CGFloat = 7.0 + var bottomInset: CGFloat = 7.0 + + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize) + let largeTitleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize)) + let titleBoldFont = Font.semibold(item.presentationData.fontSize.itemListBaseHeaderFontSize) let attributedText: NSAttributedString switch item.text { - case let .plain(text): - attributedText = NSAttributedString(string: text, font: titleFont, textColor: item.theme.list.freeTextColor) - case let .markdown(text): - attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: item.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in - return (TelegramTextAttributes.URL, contents) - })) + case let .plain(text): + attributedText = NSAttributedString(string: text, font: titleFont, textColor: item.presentationData.theme.list.freeTextColor) + case let .large(text): + attributedText = NSAttributedString(string: text, font: largeTitleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + case let .markdown(text): + attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: item.presentationData.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + })) } let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let contentSize: CGSize - contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset + verticalInset) - let insets = itemListNeighborsGroupedInsets(neighbors) + var insets = itemListNeighborsGroupedInsets(neighbors) + if case .large = item.text { + insets.top = 14.0 + bottomInset = -6.0 + } + contentSize = CGSize(width: params.width, height: titleLayout.size.height + topInset + bottomInset) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -139,7 +154,7 @@ public class ItemListTextItemNode: ListViewItemNode { let _ = titleApply() - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: topInset), size: titleLayout.size) } }) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListTextWithLabelItem.swift b/submodules/ItemListUI/Sources/Items/ItemListTextWithLabelItem.swift index 3d12ca8e31..a54f938957 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListTextWithLabelItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListTextWithLabelItem.swift @@ -14,7 +14,7 @@ public enum ItemListTextWithLabelItemTextColor { } public final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { - let theme: PresentationTheme + let presentationData: ItemListPresentationData public let label: String public let text: String let style: ItemListStyle @@ -30,8 +30,8 @@ public final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { public let tag: Any? - public init(theme: PresentationTheme, label: String, text: String, style: ItemListStyle = .plain, labelColor: ItemListTextWithLabelItemTextColor = .primary, textColor: ItemListTextWithLabelItemTextColor = .primary, enabledEntityTypes: EnabledEntityTypes, multiline: Bool, selected: Bool? = nil, sectionId: ItemListSectionId, action: (() -> Void)?, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) { - self.theme = theme + public init(presentationData: ItemListPresentationData, label: String, text: String, style: ItemListStyle = .plain, labelColor: ItemListTextWithLabelItemTextColor = .primary, textColor: ItemListTextWithLabelItemTextColor = .primary, enabledEntityTypes: EnabledEntityTypes, multiline: Bool, selected: Bool? = nil, sectionId: ItemListSectionId, action: (() -> Void)?, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) { + self.presentationData = presentationData self.label = label self.text = text self.style = style @@ -90,13 +90,6 @@ public final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { } } -private let labelFont = Font.regular(14.0) -private let textFont = Font.regular(17.0) -private let textBoldFont = Font.medium(17.0) -private let textItalicFont = Font.italic(17.0) -private let textBoldItalicFont = Font.semiboldItalic(17.0) -private let textFixedFont = Font.regular(17.0) - public class ItemListTextWithLabelItemNode: ListViewItemNode { let labelNode: TextNode let textNode: TextNode @@ -173,8 +166,8 @@ public class ItemListTextWithLabelItemNode: ListViewItemNode { return { item, params, neighbors in var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } let insets = itemListNeighborsPlainInsets(neighbors) @@ -182,39 +175,46 @@ public class ItemListTextWithLabelItemNode: ListViewItemNode { let rightInset: CGFloat = 8.0 + params.rightInset let separatorHeight = UIScreenPixel + let labelFont = Font.regular(item.presentationData.fontSize.itemListBaseLabelFontSize) + let textFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + let textBoldFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize) + let textItalicFont = Font.italic(item.presentationData.fontSize.itemListBaseFontSize) + let textBoldItalicFont = Font.semiboldItalic(item.presentationData.fontSize.itemListBaseFontSize) + let textFixedFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + var leftOffset: CGFloat = 0.0 var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)? if let selected = item.selected { - let (selectionWidth, selectionApply) = selectionNodeLayout(item.theme.list.itemCheckColors.strokeColor, item.theme.list.itemCheckColors.fillColor, item.theme.list.itemCheckColors.foregroundColor, selected, false) + let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, selected, false) selectionNodeWidthAndApply = (selectionWidth, selectionApply) leftOffset += selectionWidth - 8.0 } let labelColor: UIColor switch item.labelColor { - case .primary: - labelColor = item.theme.list.itemPrimaryTextColor - case .accent: - labelColor = item.theme.list.itemAccentColor - case .highlighted: - labelColor = item.theme.list.itemHighlightedColor + case .primary: + labelColor = item.presentationData.theme.list.itemPrimaryTextColor + case .accent: + labelColor = item.presentationData.theme.list.itemAccentColor + case .highlighted: + labelColor = item.presentationData.theme.list.itemHighlightedColor } let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: labelColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftOffset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let entities = generateTextEntities(item.text, enabledTypes: item.enabledEntityTypes) let baseColor: UIColor switch item.textColor { - case .primary: - baseColor = item.theme.list.itemPrimaryTextColor - case .accent: - baseColor = item.theme.list.itemAccentColor - case .highlighted: - baseColor = item.theme.list.itemHighlightedColor + case .primary: + baseColor = item.presentationData.theme.list.itemPrimaryTextColor + case .accent: + baseColor = item.presentationData.theme.list.itemAccentColor + case .highlighted: + baseColor = item.presentationData.theme.list.itemHighlightedColor } - let string = stringWithAppliedEntities(item.text, entities: entities, baseColor: baseColor, linkColor: item.theme.list.itemAccentColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textFont) + let string = stringWithAppliedEntities(item.text, entities: entities, baseColor: baseColor, linkColor: item.presentationData.theme.list.itemAccentColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textFont) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftOffset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let contentSize = CGSize(width: params.width, height: textLayout.size.height + 39.0) + let contentSize = CGSize(width: params.width, height: textLayout.size.height + labelLayout.size.height + 22.0) let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) return (nodeLayout, { [weak self] animation in if let strongSelf = self { @@ -233,15 +233,15 @@ public class ItemListTextWithLabelItemNode: ListViewItemNode { if let _ = updatedTheme { switch item.style { case .plain: - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor case .blocks: - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor } - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } let _ = labelApply() @@ -267,8 +267,9 @@ public class ItemListTextWithLabelItemNode: ListViewItemNode { }) } - strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 11.0), size: labelLayout.size) - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 31.0), size: textLayout.size) + let labelFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 11.0), size: labelLayout.size) + strongSelf.labelNode.frame = labelFrame + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: labelFrame.maxY + 3.0), size: textLayout.size) let leftInset: CGFloat switch item.style { @@ -436,7 +437,7 @@ public class ItemListTextWithLabelItemNode: ListViewItemNode { if let current = self.linkHighlightingNode { linkHighlightingNode = current } else { - linkHighlightingNode = LinkHighlightingNode(color: item.theme.list.itemAccentColor.withAlphaComponent(0.5)) + linkHighlightingNode = LinkHighlightingNode(color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.5)) self.linkHighlightingNode = linkHighlightingNode self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode) } diff --git a/submodules/ItemListVenueItem/BUCK b/submodules/ItemListVenueItem/BUCK new file mode 100644 index 0000000000..c7570bcf89 --- /dev/null +++ b/submodules/ItemListVenueItem/BUCK @@ -0,0 +1,25 @@ +load("//Config:buck_rule_macros.bzl", "static_library") + +static_library( + name = "ItemListVenueItem", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared", + "//submodules/AsyncDisplayKit:AsyncDisplayKit#shared", + "//submodules/Display:Display#shared", + "//submodules/Postbox:Postbox#shared", + "//submodules/TelegramCore:TelegramCore#shared", + "//submodules/SyncCore:SyncCore#shared", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/ItemListUI:ItemListUI", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/LocationResources:LocationResources", + ], + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/UIKit.framework", + ], +) diff --git a/submodules/ItemListVenueItem/Info.plist b/submodules/ItemListVenueItem/Info.plist new file mode 100644 index 0000000000..e1fe4cfb7b --- /dev/null +++ b/submodules/ItemListVenueItem/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift b/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift new file mode 100644 index 0000000000..007a4cd826 --- /dev/null +++ b/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift @@ -0,0 +1,416 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import ItemListUI +import LocationResources + +public final class ItemListVenueItem: ListViewItem, ItemListItem { + let presentationData: ItemListPresentationData + let account: Account + let venue: TelegramMediaMap + let title: String? + let subtitle: String? + let style: ItemListStyle + let action: (() -> Void)? + let infoAction: (() -> Void)? + + public let sectionId: ItemListSectionId + let header: ListViewItemHeader? + + public init(presentationData: ItemListPresentationData, account: Account, venue: TelegramMediaMap, title: String? = nil, subtitle: String? = nil, sectionId: ItemListSectionId = 0, style: ItemListStyle, action: (() -> Void)?, infoAction: (() -> Void)? = nil, header: ListViewItemHeader? = nil) { + self.presentationData = presentationData + self.account = account + self.venue = venue + self.title = title + self.subtitle = subtitle + self.sectionId = sectionId + self.style = style + self.action = action + self.infoAction = infoAction + self.header = header + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + var firstWithHeader = false + var last = false + if self.style == .plain { + if previousItem == nil { + firstWithHeader = true + } else if let previousItem = previousItem as? ItemListVenueItem, self.header != nil && previousItem.header?.id != self.header?.id { + firstWithHeader = true + } + if nextItem == nil { + last = true + } else if let nextItem = nextItem as? ItemListVenueItem, self.header != nil && nextItem.header?.id != self.header?.id { + last = true + } + } + let node = ItemListVenueItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), firstWithHeader, last) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? ItemListVenueItemNode { + let makeLayout = nodeValue.asyncLayout() + + var animated = true + if case .None = animation { + animated = false + } + + async { + var firstWithHeader = false + var last = false + if self.style == .plain { + if previousItem == nil { + firstWithHeader = true + } else if let previousItem = previousItem as? ItemListVenueItem, self.header != nil && previousItem.header?.id != self.header?.id { + firstWithHeader = true + } + if nextItem == nil { + last = true + } else if let nextItem = nextItem as? ItemListVenueItem, self.header != nil && nextItem.header?.id != self.header?.id { + last = true + } + } + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), firstWithHeader, last) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } + + public var selectable: Bool = true + + public func selected(listView: ListView){ + listView.clearHighlightAnimated(true) + self.action?() + } +} + +public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let maskNode: ASImageNode + + private let iconNode: TransformImageNode + private let titleNode: TextNode + private let addressNode: TextNode + private let infoButton: HighlightableButtonNode + + private var item: ItemListVenueItem? + private var layoutParams: (ItemListVenueItem, ListViewItemLayoutParams, ItemListNeighbors, Bool, Bool)? + + public var tag: ItemListItemTag? + + override public var canBeSelected: Bool { + if let item = self.layoutParams?.0, let _ = item.action { + return true + } else { + return false + } + } + + public init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + self.maskNode = ASImageNode() + + self.iconNode = TransformImageNode() + + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.addressNode = TextNode() + self.addressNode.isUserInteractionEnabled = false + self.addressNode.contentMode = .left + self.addressNode.contentsScale = UIScreen.main.scale + + self.infoButton = HighlightableButtonNode() + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.isAccessibilityElement = true + + self.addSubnode(self.iconNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.addressNode) + self.addSubnode(self.infoButton) + + self.infoButton.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside) + } + + public func asyncLayout() -> (_ item: ItemListVenueItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ firstWithHeader: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeAddressLayout = TextNode.asyncLayout(self.addressNode) + let iconLayout = self.iconNode.asyncLayout() + + let currentItem = self.layoutParams?.0 + + return { item, params, neighbors, firstWithHeader, last in + var updatedTheme: PresentationTheme? + var updatedVenueType: String? + + let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize) + let addressFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + let venueType = item.venue.venue?.type ?? "" + if currentItem?.venue.venue?.type != venueType { + updatedVenueType = venueType + } + + let title: String + if let venueTitle = item.venue.venue?.title { + title = venueTitle + } else if let customTitle = item.title { + title = customTitle + } else { + title = "" + } + + let subtitle: String + if let address = item.venue.venue?.address { + subtitle = address + } else if let customSubtitle = item.subtitle { + subtitle = customSubtitle + } else { + subtitle = "" + } + + let titleAttributedString = NSAttributedString(string: title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + let addressAttributedString = NSAttributedString(string: subtitle, font: addressFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + + let leftInset: CGFloat = 65.0 + params.leftInset + let rightInset: CGFloat = 16.0 + params.rightInset + (item.infoAction != nil ? 48.0 : 0.0) + let verticalInset: CGFloat = addressAttributedString.string.isEmpty ? 14.0 : 8.0 + let iconSize: CGFloat = 40.0 + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (addressLayout, addressApply) = makeAddressLayout(TextNodeLayoutArguments(attributedString: addressAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let titleSpacing: CGFloat = 1.0 + + let minHeight: CGFloat = titleLayout.size.height + verticalInset * 2.0 + let rawHeight: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + addressLayout.size.height + + var insets: UIEdgeInsets + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + insets = itemListNeighborsPlainInsets(neighbors) + insets.top = firstWithHeader ? 29.0 : 0.0 + insets.bottom = 0.0 + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + insets = itemListNeighborsGroupedInsets(neighbors) + } + + let contentSize = CGSize(width: params.width, height: max(minHeight, rawHeight)) + let separatorHeight = UIScreenPixel + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = (item, params, neighbors, firstWithHeader, last) + + strongSelf.accessibilityLabel = titleAttributedString.string + strongSelf.accessibilityValue = addressAttributedString.string + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + strongSelf.infoButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoIcon"), color: item.presentationData.theme.list.itemAccentColor), for: .normal) + } + + let transition = ContainedViewLayoutTransition.immediate + + let _ = titleApply() + let _ = addressApply() + + if let updatedVenueType = updatedVenueType { + strongSelf.iconNode.setSignal(venueIcon(postbox: item.account.postbox, type: updatedVenueType, background: true)) + } + + let iconApply = iconLayout(TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: iconSize, height: iconSize), boundingSize: CGSize(width: iconSize, height: iconSize), intrinsicInsets: UIEdgeInsets())) + iconApply() + + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + if strongSelf.maskNode.supernode != nil { + strongSelf.maskNode.removeFromSupernode() + } + + let stripeInset: CGFloat + if case .none = neighbors.bottom { + stripeInset = 0.0 + } else { + stripeInset = leftInset + } + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: stripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - stripeInset, height: separatorHeight)) + strongSelf.bottomStripeNode.isHidden = last + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + default: + bottomStripeInset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) + } + + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.addressNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: addressLayout.size)) + + transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: floorToScreenPixels((layout.contentSize.height - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize))) + + transition.updateFrame(node: strongSelf.infoButton, frame: CGRect(x: layout.contentSize.width - params.rightInset - 60.0, y: 0.0, width: 60.0, height: layout.contentSize.height)) + strongSelf.infoButton.isHidden = item.infoAction == nil + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel)) + } + }) + } + } + + override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + @objc private func infoPressed() { + self.item?.infoAction?() + } + + override public func header() -> ListViewItemHeader? { + return self.item?.header + } +} diff --git a/submodules/JoinLinkPreviewUI/BUCK b/submodules/JoinLinkPreviewUI/BUCK index e99fe4bc84..2871179be6 100644 --- a/submodules/JoinLinkPreviewUI/BUCK +++ b/submodules/JoinLinkPreviewUI/BUCK @@ -18,6 +18,7 @@ static_library( "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/ShareController:ShareController", "//submodules/SelectablePeerNode:SelectablePeerNode", + "//submodules/PeerInfoUI:PeerInfoUI", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift index 15fcd1e12e..23c81a3bb1 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift @@ -10,6 +10,7 @@ import TelegramPresentationData import AccountContext import AlertUI import PresentationDataUtils +import PeerInfoUI public final class JoinLinkPreviewController: ViewController { private var controllerNode: JoinLinkPreviewControllerNode { @@ -21,14 +22,18 @@ public final class JoinLinkPreviewController: ViewController { private let context: AccountContext private let link: String private let navigateToPeer: (PeerId) -> Void + private let parentNavigationController: NavigationController? + private var resolvedState: ExternalJoiningChatState? private var presentationData: PresentationData private let disposable = MetaDisposable() - public init(context: AccountContext, link: String, navigateToPeer: @escaping (PeerId) -> Void) { + public init(context: AccountContext, link: String, navigateToPeer: @escaping (PeerId) -> Void, parentNavigationController: NavigationController?, resolvedState: ExternalJoiningChatState? = nil) { self.context = context self.link = link self.navigateToPeer = navigateToPeer + self.parentNavigationController = parentNavigationController + self.resolvedState = resolvedState self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -59,9 +64,18 @@ public final class JoinLinkPreviewController: ViewController { self?.join() } self.displayNodeDidLoad() - self.disposable.set((joinLinkInformation(self.link, account: self.context.account) + + let signal: Signal + if let resolvedState = self.resolvedState { + signal = .single(resolvedState) + } else { + signal = joinLinkInformation(self.link, account: self.context.account) + } + + self.disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { + strongSelf.resolvedState = result switch result { case let .invite(title, photoRepresentation, participantsCount, participants): let data = JoinLinkPreviewData(isGroup: participants != nil, isJoined: false) @@ -112,7 +126,19 @@ public final class JoinLinkPreviewController: ViewController { }, error: { [weak self] error in if let strongSelf = self { if case .tooMuchJoined = error { - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Join_ChannelsTooMuch, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + if let parentNavigationController = strongSelf.parentNavigationController { + let context = strongSelf.context + let link = strongSelf.link + let navigateToPeer = strongSelf.navigateToPeer + let resolvedState = strongSelf.resolvedState + parentNavigationController.pushViewController(oldChannelsController(context: strongSelf.context, intent: .join, completed: { [weak parentNavigationController] value in + if value { + (parentNavigationController?.viewControllers.last as? ViewController)?.present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: navigateToPeer, parentNavigationController: parentNavigationController, resolvedState: resolvedState), in: .window(.root)) + } + })) + } else { + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Join_ChannelsTooMuch, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } strongSelf.dismiss() } } diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift index 7e26d46071..3937a380fd 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift @@ -50,7 +50,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer self.peerNodes = members.map { peer in let node = SelectablePeerNode() - node.setup(account: context.account, theme: theme, strings: strings, peer: RenderedPeer(peer: peer), synchronousLoad: false) + node.setup(context: context, theme: theme, strings: strings, peer: RenderedPeer(peer: peer), synchronousLoad: false) node.theme = itemTheme return node } @@ -66,7 +66,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer let peer = TelegramGroup(id: PeerId(namespace: 0, id: 0), title: title, photo: image.flatMap { [$0] } ?? [], participantCount: Int(memberCount), role: .member, membership: .Left, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) self.addSubnode(self.avatarNode) - self.avatarNode.setPeer(account: context.account, theme: theme, peer: peer, emptyColor: theme.list.mediaPlaceholderColor) + self.avatarNode.setPeer(context: context, theme: theme, peer: peer, emptyColor: theme.list.mediaPlaceholderColor) self.addSubnode(self.titleNode) self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(16.0), textColor: theme.actionSheet.primaryTextColor) diff --git a/submodules/LanguageSuggestionUI/Sources/LanguageSuggestionController.swift b/submodules/LanguageSuggestionUI/Sources/LanguageSuggestionController.swift index 931f7d09a8..bdde36d467 100644 --- a/submodules/LanguageSuggestionUI/Sources/LanguageSuggestionController.swift +++ b/submodules/LanguageSuggestionUI/Sources/LanguageSuggestionController.swift @@ -217,21 +217,21 @@ private final class LanguageSuggestionAlertContentNode: AlertContentNode { return self.isUserInteractionEnabled } - init(theme: PresentationTheme, strings: LanguageSuggestionControllerStrings, englishStrings: LanguageSuggestionControllerStrings, suggestedLocalization: LocalizationInfo, openSelection: @escaping () -> Void, applyLocalization: @escaping (String, () -> Void) -> Void, dismiss: @escaping () -> Void) { + init(presentationData: PresentationData, strings: LanguageSuggestionControllerStrings, englishStrings: LanguageSuggestionControllerStrings, suggestedLocalization: LocalizationInfo, openSelection: @escaping () -> Void, applyLocalization: @escaping (String, () -> Void) -> Void, dismiss: @escaping () -> Void) { let selectedLocalization = ValuePromise(suggestedLocalization.languageCode, ignoreRepeated: true) self.titleNode = ASTextNode() - self.titleNode.attributedText = NSAttributedString(string: strings.ChooseLanguage, font: Font.bold(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) + self.titleNode.attributedText = NSAttributedString(string: strings.ChooseLanguage, font: Font.bold(presentationData.listsFontSize.baseDisplaySize), textColor: presentationData.theme.actionSheet.primaryTextColor, paragraphAlignment: .center) self.titleNode.maximumNumberOfLines = 2 self.subtitleNode = ASTextNode() - self.subtitleNode.attributedText = NSAttributedString(string: englishStrings.ChooseLanguage, font: Font.regular(14.0), textColor: theme.actionSheet.secondaryTextColor, paragraphAlignment: .center) + self.subtitleNode.attributedText = NSAttributedString(string: englishStrings.ChooseLanguage, font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0)), textColor: presentationData.theme.actionSheet.secondaryTextColor, paragraphAlignment: .center) self.subtitleNode.maximumNumberOfLines = 2 self.titleSeparatorNode = ASDisplayNode() - self.titleSeparatorNode.backgroundColor = theme.actionSheet.opaqueItemSeparatorColor + self.titleSeparatorNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemSeparatorColor - self.activityIndicator = ActivityIndicator(type: .custom(theme.actionSheet.controlAccentColor, 22.0, 1.0, false)) + self.activityIndicator = ActivityIndicator(type: .custom(presentationData.theme.actionSheet.controlAccentColor, 22.0, 1.0, false)) self.activityIndicator.isHidden = true var items: [LanguageSuggestionItem] = [] @@ -250,7 +250,7 @@ private final class LanguageSuggestionAlertContentNode: AlertContentNode { applyImpl?() })) - self.nodes = items.map { LanguageSuggestionItemNode(theme: theme, item: $0) } + self.nodes = items.map { LanguageSuggestionItemNode(theme: presentationData.theme, item: $0) } super.init() @@ -334,7 +334,8 @@ public func languageSuggestionController(context: AccountContext, suggestedLocal return nil } - let theme = context.sharedContext.currentPresentationData.with { $0 }.theme + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let theme = context.sharedContext.presentationData let strings = LanguageSuggestionControllerStrings(localization: suggestedLocalization) guard let mainPath = getAppBundle().path(forResource: "en", ofType: "lproj") else { return nil @@ -344,7 +345,7 @@ public func languageSuggestionController(context: AccountContext, suggestedLocal let disposable = MetaDisposable() var dismissImpl: ((Bool) -> Void)? - let contentNode = LanguageSuggestionAlertContentNode(theme: theme, strings: strings, englishStrings: englishStrings, suggestedLocalization: localization, openSelection: { + let contentNode = LanguageSuggestionAlertContentNode(presentationData: presentationData, strings: strings, englishStrings: englishStrings, suggestedLocalization: localization, openSelection: { dismissImpl?(true) openSelection() }, applyLocalization: { languageCode, startActivity in @@ -360,7 +361,7 @@ public func languageSuggestionController(context: AccountContext, suggestedLocal }, dismiss: { dismissImpl?(true) }) - let controller = AlertController(theme: AlertControllerTheme(presentationTheme: theme), contentNode: contentNode) + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) dismissImpl = { [weak controller] animated in if animated { controller?.dismissAnimated() diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentCarouselItemView.h b/submodules/LegacyComponents/LegacyComponents/TGAttachmentCarouselItemView.h index da42f7f657..54638ed711 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGAttachmentCarouselItemView.h +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentCarouselItemView.h @@ -32,6 +32,7 @@ @property (nonatomic) bool disableStickers; @property (nonatomic) bool hasSilentPosting; @property (nonatomic) bool hasSchedule; +@property (nonatomic) bool reminder; @property (nonatomic, copy) void (^presentScheduleController)(void (^)(int32_t)); @property (nonatomic, strong) NSArray *underlyingViews; diff --git a/submodules/LegacyComponents/LegacyComponents/TGAttachmentCarouselItemView.m b/submodules/LegacyComponents/LegacyComponents/TGAttachmentCarouselItemView.m index 17318f32fd..27e58db3ca 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGAttachmentCarouselItemView.m +++ b/submodules/LegacyComponents/LegacyComponents/TGAttachmentCarouselItemView.m @@ -257,7 +257,7 @@ const NSUInteger TGAttachmentDisplayedAssetLimit = 500; if (_cameraView) [_collectionView addSubview:_cameraView]; - _sendMediaItemView = [[TGMenuSheetButtonItemView alloc] initWithTitle:nil type:TGMenuSheetButtonTypeSend action:^ + _sendMediaItemView = [[TGMenuSheetButtonItemView alloc] initWithTitle:nil type:TGMenuSheetButtonTypeSend fontSize:20.0 action:^ { __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; if (strongSelf != nil && strongSelf.sendPressed != nil) @@ -272,7 +272,7 @@ const NSUInteger TGAttachmentDisplayedAssetLimit = 500; if (!_document) { - _sendFileItemView = [[TGMenuSheetButtonItemView alloc] initWithTitle:nil type:TGMenuSheetButtonTypeDefault action:^ + _sendFileItemView = [[TGMenuSheetButtonItemView alloc] initWithTitle:nil type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ { __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; if (strongSelf != nil && strongSelf.sendPressed != nil) @@ -801,7 +801,7 @@ const NSUInteger TGAttachmentDisplayedAssetLimit = 500; if ([cell isKindOfClass:[TGAttachmentAssetCell class]]) thumbnailImage = cell.imageView.image; - TGMediaPickerModernGalleryMixin *mixin = [[TGMediaPickerModernGalleryMixin alloc] initWithContext:_context item:asset fetchResult:_fetchResult parentController:self.parentController thumbnailImage:thumbnailImage selectionContext:_selectionContext editingContext:_editingContext suggestionContext:self.suggestionContext hasCaptions:(_allowCaptions && !_forProfilePhoto) allowCaptionEntities:self.allowCaptionEntities hasTimer:self.hasTimer onlyCrop:self.onlyCrop inhibitDocumentCaptions:_inhibitDocumentCaptions inhibitMute:self.inhibitMute asFile:self.asFile itemsLimit:TGAttachmentDisplayedAssetLimit recipientName:self.recipientName hasSilentPosting:self.hasSilentPosting hasSchedule:self.hasSchedule]; + TGMediaPickerModernGalleryMixin *mixin = [[TGMediaPickerModernGalleryMixin alloc] initWithContext:_context item:asset fetchResult:_fetchResult parentController:self.parentController thumbnailImage:thumbnailImage selectionContext:_selectionContext editingContext:_editingContext suggestionContext:self.suggestionContext hasCaptions:(_allowCaptions && !_forProfilePhoto) allowCaptionEntities:self.allowCaptionEntities hasTimer:self.hasTimer onlyCrop:self.onlyCrop inhibitDocumentCaptions:_inhibitDocumentCaptions inhibitMute:self.inhibitMute asFile:self.asFile itemsLimit:TGAttachmentDisplayedAssetLimit recipientName:self.recipientName hasSilentPosting:self.hasSilentPosting hasSchedule:self.hasSchedule reminder:self.reminder]; mixin.presentScheduleController = self.presentScheduleController; __weak TGAttachmentCarouselItemView *weakSelf = self; mixin.thumbnailSignalForItem = ^SSignal *(id item) diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraController.h b/submodules/LegacyComponents/LegacyComponents/TGCameraController.h index 85bb772f78..087ed30219 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGCameraController.h +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraController.h @@ -37,6 +37,7 @@ typedef enum { @property (nonatomic, assign) bool hasTimer; @property (nonatomic, assign) bool hasSilentPosting; @property (nonatomic, assign) bool hasSchedule; +@property (nonatomic, assign) bool reminder; @property (nonatomic, strong) TGSuggestionContext *suggestionContext; @property (nonatomic, assign) bool shortcut; diff --git a/submodules/LegacyComponents/LegacyComponents/TGCameraController.m b/submodules/LegacyComponents/LegacyComponents/TGCameraController.m index 8bb1df16ee..80429b92c7 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGCameraController.m +++ b/submodules/LegacyComponents/LegacyComponents/TGCameraController.m @@ -1050,7 +1050,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus NSArray *items = @ [ - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Camera.Discard") type:TGMenuSheetButtonTypeDefault action:^ + [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Camera.Discard") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; if (strongController == nil) @@ -1063,7 +1063,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus [strongController dismissAnimated:true manual:false completion:nil]; [strongSelf beginTransitionOutWithVelocity:0.0f]; }], - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel action:^ + [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; if (strongController != nil) @@ -1279,7 +1279,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus } } - TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:strongSelf->_context sendButtonFrame:strongModel.interfaceView.doneButtonFrame canSendSilently:strongSelf->_hasSilentPosting canSchedule:effectiveHasSchedule]; + TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:strongSelf->_context isDark:true sendButtonFrame:strongModel.interfaceView.doneButtonFrame canSendSilently:strongSelf->_hasSilentPosting canSchedule:effectiveHasSchedule reminder:strongSelf->_reminder]; controller.send = ^{ __strong TGCameraController *strongSelf = weakSelf; __strong TGMediaPickerGalleryModel *strongModel = weakModel; diff --git a/submodules/LegacyComponents/LegacyComponents/TGCheckButtonView.m b/submodules/LegacyComponents/LegacyComponents/TGCheckButtonView.m index 8aa9440404..998363d54b 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGCheckButtonView.m +++ b/submodules/LegacyComponents/LegacyComponents/TGCheckButtonView.m @@ -24,19 +24,14 @@ NSInteger _number; UIColor *_checkColor; + + CGAffineTransform TGCheckButtonDefaultTransform; } @end @implementation TGCheckButtonView -static NSMutableDictionary *backgroundImages; -static NSMutableDictionary *fillImages; -static CGAffineTransform TGCheckButtonDefaultTransform; - -+ (void)resetCache -{ - [backgroundImages removeAllObjects]; - [fillImages removeAllObjects]; ++ (void)resetCache { } - (instancetype)initWithStyle:(TGCheckButtonStyle)style { @@ -55,15 +50,12 @@ static CGAffineTransform TGCheckButtonDefaultTransform; self = [super initWithFrame:CGRectMake(0, 0, size.width, size.height)]; if (self != nil) { - static dispatch_once_t onceToken; - static CGFloat screenScale = 2.0f; - dispatch_once(&onceToken, ^ - { - TGCheckButtonDefaultTransform = CGAffineTransformMakeRotation(-M_PI_4); - backgroundImages = [[NSMutableDictionary alloc] init]; - fillImages = [[NSMutableDictionary alloc] init]; - screenScale = [UIScreen mainScreen].scale; - }); + CGFloat screenScale = 2.0f; + + TGCheckButtonDefaultTransform = CGAffineTransformMakeRotation(-M_PI_4); + NSMutableDictionary *backgroundImages = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *fillImages = [[NSMutableDictionary alloc] init]; + screenScale = [UIScreen mainScreen].scale; int32_t hex = 0x29c519; UIColor *greenColor = [[UIColor alloc] initWithRed:(((hex >> 16) & 0xff) / 255.0f) green:(((hex >> 8) & 0xff) / 255.0f) blue:(((hex) & 0xff) / 255.0f) alpha:1.0f]; diff --git a/submodules/LegacyComponents/LegacyComponents/TGClipboardMenu.m b/submodules/LegacyComponents/LegacyComponents/TGClipboardMenu.m index e61ce11057..7a3b0c3169 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGClipboardMenu.m +++ b/submodules/LegacyComponents/LegacyComponents/TGClipboardMenu.m @@ -64,7 +64,7 @@ sendTitle = [NSString stringWithFormat:format, [NSString stringWithFormat:@"%ld", photosCount]]; } - TGMenuSheetButtonItemView *sendItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:sendTitle type:TGMenuSheetButtonTypeSend action:^ + TGMenuSheetButtonItemView *sendItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:sendTitle type:TGMenuSheetButtonTypeSend fontSize:20.0 action:^ { __strong TGClipboardPreviewItemView *strongPreviewItem = weakPreviewItem; completed(strongPreviewItem.selectionContext, strongPreviewItem.editingContext, nil); @@ -90,7 +90,7 @@ [strongPreviewItem setCollapsed:count == 0 animated:true]; }; - TGMenuSheetButtonItemView *cancelItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel action:^ + TGMenuSheetButtonItemView *cancelItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; [strongController dismissAnimated:true]; diff --git a/submodules/LegacyComponents/LegacyComponents/TGLocationAnnotation.h b/submodules/LegacyComponents/LegacyComponents/TGLocationAnnotation.h index 1f24a53165..591f6da30f 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGLocationAnnotation.h +++ b/submodules/LegacyComponents/LegacyComponents/TGLocationAnnotation.h @@ -19,6 +19,7 @@ @property (nonatomic, readonly) TGLocationMediaAttachment *location; @property (nonatomic, readonly) bool isLiveLocation; @property (nonatomic, strong) id peer; +@property (nonatomic, strong) UIColor *color; @property (nonatomic, assign) CLLocationCoordinate2D coordinate; @property (nonatomic, assign) int32_t messageId; @property (nonatomic, assign) bool isOwn; @@ -26,5 +27,6 @@ @property (nonatomic, assign) bool isExpired; - (instancetype)initWithLocation:(TGLocationMediaAttachment *)location; +- (instancetype)initWithLocation:(TGLocationMediaAttachment *)location color:(UIColor *)color; @end diff --git a/submodules/LegacyComponents/LegacyComponents/TGLocationAnnotation.m b/submodules/LegacyComponents/LegacyComponents/TGLocationAnnotation.m index 8db9ba8cd3..d882d91b14 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGLocationAnnotation.m +++ b/submodules/LegacyComponents/LegacyComponents/TGLocationAnnotation.m @@ -30,11 +30,17 @@ } - (instancetype)initWithLocation:(TGLocationMediaAttachment *)location +{ + return [self initWithLocation:location color:nil]; +} + +- (instancetype)initWithLocation:(TGLocationMediaAttachment *)location color:(UIColor *)color { self = [super init]; if (self != nil) { _coordinate = CLLocationCoordinate2DMake(location.latitude, location.longitude); + _color = color; _location = location; _observers = [[NSMutableSet alloc] init]; } diff --git a/submodules/LegacyComponents/LegacyComponents/TGLocationInfoCell.h b/submodules/LegacyComponents/LegacyComponents/TGLocationInfoCell.h index 63d31f243e..97fbe5fcce 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGLocationInfoCell.h +++ b/submodules/LegacyComponents/LegacyComponents/TGLocationInfoCell.h @@ -14,7 +14,7 @@ @property (nonatomic, readonly) UIButton *directionsButton; -- (void)setLocation:(TGLocationMediaAttachment *)location messageId:(int32_t)messageId userLocationSignal:(SSignal *)userLocationSignal; +- (void)setLocation:(TGLocationMediaAttachment *)location color:(UIColor *)color messageId:(int32_t)messageId userLocationSignal:(SSignal *)userLocationSignal; @end diff --git a/submodules/LegacyComponents/LegacyComponents/TGLocationInfoCell.m b/submodules/LegacyComponents/LegacyComponents/TGLocationInfoCell.m index 154374f138..46064ac1da 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGLocationInfoCell.m +++ b/submodules/LegacyComponents/LegacyComponents/TGLocationInfoCell.m @@ -158,7 +158,7 @@ const CGFloat TGLocationInfoCellHeight = 134.0f; return _directionsButton; } -- (void)setLocation:(TGLocationMediaAttachment *)location messageId:(int32_t)messageId userLocationSignal:(SSignal *)userLocationSignal +- (void)setLocation:(TGLocationMediaAttachment *)location color:(UIColor *)color messageId:(int32_t)messageId userLocationSignal:(SSignal *)userLocationSignal { if (_messageId == messageId) return; @@ -167,8 +167,14 @@ const CGFloat TGLocationInfoCellHeight = 134.0f; _titleLabel.text = location.venue.title.length > 0 ? location.venue.title : TGLocalized(@"Map.Location"); + UIColor *pinColor = _pallete != nil ? _pallete.iconColor : [UIColor whiteColor]; + if (color != nil) { + [_circleView setImage:TGTintedImage([TGLocationVenueCell circleImage], color)]; + pinColor = [UIColor whiteColor]; + } + if (location.venue.type.length > 0 && [location.venue.provider isEqualToString:@"foursquare"]) - [_iconView loadUri:[NSString stringWithFormat:@"location-venue-icon://type=%@&width=%d&height=%d&color=%d", location.venue.type, 48, 48, TGColorHexCode(_pallete != nil ? _pallete.iconColor : [UIColor whiteColor])] withOptions:nil]; + [_iconView loadUri:[NSString stringWithFormat:@"location-venue-icon://type=%@&width=%d&height=%d&color=%d", location.venue.type, 48, 48, TGColorHexCode(pinColor)] withOptions:nil]; SSignal *addressSignal = [SSignal single:@""]; if (location.venue.address.length > 0) diff --git a/submodules/LegacyComponents/LegacyComponents/TGLocationLiveSessionItemView.m b/submodules/LegacyComponents/LegacyComponents/TGLocationLiveSessionItemView.m index 4e4bb3530c..98d3bfc810 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGLocationLiveSessionItemView.m +++ b/submodules/LegacyComponents/LegacyComponents/TGLocationLiveSessionItemView.m @@ -23,7 +23,7 @@ { bool isUser = [peer isKindOfClass:[TGUser class]]; NSString *title = isUser ? ((TGUser *)peer).displayName : ((TGConversation *)peer).chatTitle; - self = [super initWithTitle:@"" type:TGMenuSheetButtonTypeDefault action:action]; + self = [super initWithTitle:@"" type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:action]; if (self != nil) { _label = [[UILabel alloc] init]; diff --git a/submodules/LegacyComponents/LegacyComponents/TGLocationMapViewController.m b/submodules/LegacyComponents/LegacyComponents/TGLocationMapViewController.m index 6703d330d7..3a02f3c329 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGLocationMapViewController.m +++ b/submodules/LegacyComponents/LegacyComponents/TGLocationMapViewController.m @@ -491,7 +491,7 @@ const CGFloat TGLocationMapInset = 100.0f; [itemViews addObject:titleItem]; __weak TGMenuSheetController *weakController = controller; - TGMenuSheetButtonItemView *for15MinutesItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Map.LiveLocationFor15Minutes") type:TGMenuSheetButtonTypeDefault action:^ + TGMenuSheetButtonItemView *for15MinutesItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Map.LiveLocationFor15Minutes") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ { __strong TGLocationMapViewController *strongSelf = weakSelf; if (strongSelf == nil) @@ -508,7 +508,7 @@ const CGFloat TGLocationMapInset = 100.0f; }]; [itemViews addObject:for15MinutesItem]; - TGMenuSheetButtonItemView *for1HourItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Map.LiveLocationFor1Hour") type:TGMenuSheetButtonTypeDefault action:^ + TGMenuSheetButtonItemView *for1HourItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Map.LiveLocationFor1Hour") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ { __strong TGLocationMapViewController *strongSelf = weakSelf; if (strongSelf == nil) @@ -525,7 +525,7 @@ const CGFloat TGLocationMapInset = 100.0f; }]; [itemViews addObject:for1HourItem]; - TGMenuSheetButtonItemView *for8HoursItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Map.LiveLocationFor8Hours") type:TGMenuSheetButtonTypeDefault action:^ + TGMenuSheetButtonItemView *for8HoursItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Map.LiveLocationFor8Hours") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ { __strong TGLocationMapViewController *strongSelf = weakSelf; if (strongSelf == nil) @@ -542,7 +542,7 @@ const CGFloat TGLocationMapInset = 100.0f; }]; [itemViews addObject:for8HoursItem]; - TGMenuSheetButtonItemView *cancelItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel action:^ + TGMenuSheetButtonItemView *cancelItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; if (strongController == nil) diff --git a/submodules/LegacyComponents/LegacyComponents/TGLocationPinAnnotationView.m b/submodules/LegacyComponents/LegacyComponents/TGLocationPinAnnotationView.m index 0f93d8c45b..b8e2439dd0 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGLocationPinAnnotationView.m +++ b/submodules/LegacyComponents/LegacyComponents/TGLocationPinAnnotationView.m @@ -265,10 +265,17 @@ NSString *const TGLocationPinAnnotationKind = @"TGLocationPinAnnotation"; _avatarView.alpha = 1.0f; _iconView.hidden = false; - _backgroundView.image = TGTintedImage(TGComponentsImageNamed(@"LocationPinBackground"), _pallete != nil ? _pallete.locationColor : UIColorRGB(0x008df2)); + UIColor *color = _pallete != nil ? _pallete.locationColor : UIColorRGB(0x008df2); + UIColor *pinColor = _pallete != nil ? _pallete.iconColor : [UIColor whiteColor]; + if (locationAnnotation.color != nil) { + color = locationAnnotation.color; + pinColor = [UIColor whiteColor]; + } + + _backgroundView.image = TGTintedImage(TGComponentsImageNamed(@"LocationPinBackground"), color); if (location.venue.type.length > 0) { - [_iconView loadUri:[NSString stringWithFormat:@"location-venue-icon://type=%@&width=%d&height=%d&color=%d", location.venue.type, 64, 64, TGColorHexCode(_pallete != nil ? _pallete.iconColor : [UIColor whiteColor])] withOptions:nil]; + [_iconView loadUri:[NSString stringWithFormat:@"location-venue-icon://type=%@&width=%d&height=%d&color=%d", location.venue.type, 64, 64, TGColorHexCode(pinColor)] withOptions:nil]; } else { diff --git a/submodules/LegacyComponents/LegacyComponents/TGLocationViewController.h b/submodules/LegacyComponents/LegacyComponents/TGLocationViewController.h index a3cd815d65..a14cf034c4 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGLocationViewController.h +++ b/submodules/LegacyComponents/LegacyComponents/TGLocationViewController.h @@ -48,8 +48,8 @@ @property (nonatomic, copy) SSignal *(^remainingTimeForMessage)(TGMessage *message); - (instancetype)initWithContext:(id)context liveLocation:(TGLiveLocation *)liveLocation; -- (instancetype)initWithContext:(id)context locationAttachment:(TGLocationMediaAttachment *)locationAttachment peer:(id)peer; -- (instancetype)initWithContext:(id)context message:(TGMessage *)message peer:(id)peer; +- (instancetype)initWithContext:(id)context locationAttachment:(TGLocationMediaAttachment *)locationAttachment peer:(id)peer color:(UIColor *)color; +- (instancetype)initWithContext:(id)context message:(TGMessage *)message peer:(id)peer color:(UIColor *)color; - (void)actionsButtonPressed; diff --git a/submodules/LegacyComponents/LegacyComponents/TGLocationViewController.m b/submodules/LegacyComponents/LegacyComponents/TGLocationViewController.m index 0b8bcd2f27..97489ab445 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGLocationViewController.m +++ b/submodules/LegacyComponents/LegacyComponents/TGLocationViewController.m @@ -43,6 +43,7 @@ id _peer; TGMessage *_message; TGLocationMediaAttachment *_locationAttachment; + UIColor *_venueColor; TGLocationAnnotation *_annotation; @@ -75,12 +76,13 @@ @implementation TGLocationViewController -- (instancetype)initWithContext:(id)context locationAttachment:(TGLocationMediaAttachment *)locationAttachment peer:(id)peer +- (instancetype)initWithContext:(id)context locationAttachment:(TGLocationMediaAttachment *)locationAttachment peer:(id)peer color:(UIColor *)color { self = [self initWithContext:context]; if (self != nil) { _locationAttachment = locationAttachment; + _venueColor = color; _reloadDisposable = [[SMetaDisposable alloc] init]; _reloadReady = [[SVariable alloc] init]; @@ -90,7 +92,7 @@ _peer = peer; if (locationAttachment.period == 0) - _annotation = [[TGLocationAnnotation alloc] initWithLocation:locationAttachment]; + _annotation = [[TGLocationAnnotation alloc] initWithLocation:locationAttachment color:color]; _liveLocationsDisposable = [[SMetaDisposable alloc] init]; @@ -128,7 +130,7 @@ return self; } -- (instancetype)initWithContext:(id)context message:(TGMessage *)message peer:(id)peer +- (instancetype)initWithContext:(id)context message:(TGMessage *)message peer:(id)peer color:(UIColor *)color { self = [self initWithContext:context]; if (self != nil) @@ -142,9 +144,10 @@ _context = context; _peer = peer; + _venueColor = color; if (_locationAttachment.period == 0) - _annotation = [[TGLocationAnnotation alloc] initWithLocation:_locationAttachment]; + _annotation = [[TGLocationAnnotation alloc] initWithLocation:_locationAttachment color:color]; _liveLocationsDisposable = [[SMetaDisposable alloc] init]; @@ -739,7 +742,7 @@ NSMutableArray *itemViews = [[NSMutableArray alloc] init]; __weak TGMenuSheetController *weakController = controller; - TGMenuSheetButtonItemView *openItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Map.OpenInMaps") type:TGMenuSheetButtonTypeDefault action:^ + TGMenuSheetButtonItemView *openItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Map.OpenInMaps") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ { __strong TGLocationViewController *strongSelf = weakSelf; if (strongSelf == nil) @@ -754,7 +757,7 @@ }]; [itemViews addObject:openItem]; - TGMenuSheetButtonItemView *shareItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Conversation.ContextMenuShare") type:TGMenuSheetButtonTypeDefault action:^ + TGMenuSheetButtonItemView *shareItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Conversation.ContextMenuShare") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; if (strongController == nil) @@ -767,7 +770,7 @@ }]; [itemViews addObject:shareItem]; - TGMenuSheetButtonItemView *cancelItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel action:^ + TGMenuSheetButtonItemView *cancelItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; if (strongController == nil) @@ -867,7 +870,7 @@ __weak TGMenuSheetController *weakController = controller; NSArray *items = @ [ - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Map.GetDirections") type:TGMenuSheetButtonTypeDefault action:^ + [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Map.GetDirections") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; if (strongController == nil) @@ -876,7 +879,7 @@ [strongController dismissAnimated:true]; block(); }], - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel action:^ + [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; if (strongController != nil) @@ -1005,7 +1008,7 @@ if (cell == nil) cell = [[TGLocationInfoCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:TGLocationInfoCellKind]; cell.pallete = self.pallete; - [cell setLocation:_locationAttachment messageId:_message.mid userLocationSignal:[self userLocationSignal]]; + [cell setLocation:_locationAttachment color: _venueColor messageId:_message.mid userLocationSignal:[self userLocationSignal]]; cell.locatePressed = ^ { __strong TGLocationViewController *strongSelf = weakSelf; diff --git a/submodules/LegacyComponents/LegacyComponents/TGMediaAssetModernImageSignals.m b/submodules/LegacyComponents/LegacyComponents/TGMediaAssetModernImageSignals.m index 3b0cefb595..fcdd71cea9 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGMediaAssetModernImageSignals.m +++ b/submodules/LegacyComponents/LegacyComponents/TGMediaAssetModernImageSignals.m @@ -735,7 +735,16 @@ { PHImageRequestID token = PHInvalidImageRequestID; - if (asset.subtypes & TGMediaAssetSubtypePhotoLive) + bool processLive = false; + if (asset.subtypes & TGMediaAssetSubtypePhotoLive) { + if (iosMajorVersion() < 9 || (iosMajorVersion() == 9 && iosMinorVersion() < 1)) { + processLive = false; + } else { + processLive = true; + } + } + + if (processLive) { PHLivePhotoRequestOptions *requestOptions = [[PHLivePhotoRequestOptions alloc] init]; requestOptions.networkAccessAllowed = true; @@ -830,7 +839,16 @@ { PHImageRequestID token = PHInvalidImageRequestID; - if (asset.subtypes & TGMediaAssetSubtypePhotoLive) + bool processLive = false; + if (asset.subtypes & TGMediaAssetSubtypePhotoLive) { + if (iosMajorVersion() < 9 || (iosMajorVersion() == 9 && iosMinorVersion() < 1)) { + processLive = false; + } else { + processLive = true; + } + } + + if (processLive) { NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.mov", asset.identifier]]; NSURL *fileUrl = [NSURL fileURLWithPath:filePath]; diff --git a/submodules/LegacyComponents/LegacyComponents/TGMediaAssetsController.h b/submodules/LegacyComponents/LegacyComponents/TGMediaAssetsController.h index d60bdba4ba..348bab118a 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGMediaAssetsController.h +++ b/submodules/LegacyComponents/LegacyComponents/TGMediaAssetsController.h @@ -59,6 +59,7 @@ typedef enum @property (nonatomic, assign) bool inhibitMute; @property (nonatomic, assign) bool hasSilentPosting; @property (nonatomic, assign) bool hasSchedule; +@property (nonatomic, assign) bool reminder; @property (nonatomic, copy) void (^presentScheduleController)(void (^)(int32_t)); @property (nonatomic, assign) bool liveVideoUploadEnabled; diff --git a/submodules/LegacyComponents/LegacyComponents/TGMediaAssetsController.m b/submodules/LegacyComponents/LegacyComponents/TGMediaAssetsController.m index 0506130e2b..8c6fd58736 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGMediaAssetsController.m +++ b/submodules/LegacyComponents/LegacyComponents/TGMediaAssetsController.m @@ -122,6 +122,7 @@ pickerController.onlyCrop = strongController.onlyCrop; pickerController.hasSilentPosting = strongController.hasSilentPosting; pickerController.hasSchedule = strongController.hasSchedule; + pickerController.reminder = strongController.reminder; pickerController.presentScheduleController = strongController.presentScheduleController; [strongController pushViewController:pickerController animated:true]; }; @@ -214,6 +215,12 @@ self.pickerController.hasSchedule = hasSchedule; } +- (void)setReminder:(bool)reminder +{ + _reminder = reminder; + self.pickerController.reminder = reminder; +} + - (void)setPresentScheduleController:(void (^)(void (^)(int32_t)))presentScheduleController { _presentScheduleController = [presentScheduleController copy]; self.pickerController.presentScheduleController = presentScheduleController; diff --git a/submodules/LegacyComponents/LegacyComponents/TGMediaAssetsPickerController.m b/submodules/LegacyComponents/LegacyComponents/TGMediaAssetsPickerController.m index c780a58ab0..965c013b24 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGMediaAssetsPickerController.m +++ b/submodules/LegacyComponents/LegacyComponents/TGMediaAssetsPickerController.m @@ -323,7 +323,7 @@ - (TGMediaPickerModernGalleryMixin *)_galleryMixinForContext:(id)context item:(id)item thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext suggestionContext:(TGSuggestionContext *)suggestionContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities inhibitDocumentCaptions:(bool)inhibitDocumentCaptions asFile:(bool)asFile { - return [[TGMediaPickerModernGalleryMixin alloc] initWithContext:context item:item fetchResult:_fetchResult parentController:self thumbnailImage:thumbnailImage selectionContext:selectionContext editingContext:editingContext suggestionContext:suggestionContext hasCaptions:hasCaptions allowCaptionEntities:allowCaptionEntities hasTimer:self.hasTimer onlyCrop:self.onlyCrop inhibitDocumentCaptions:inhibitDocumentCaptions inhibitMute:self.inhibitMute asFile:asFile itemsLimit:0 recipientName:self.recipientName hasSilentPosting:self.hasSilentPosting hasSchedule:self.hasSchedule]; + return [[TGMediaPickerModernGalleryMixin alloc] initWithContext:context item:item fetchResult:_fetchResult parentController:self thumbnailImage:thumbnailImage selectionContext:selectionContext editingContext:editingContext suggestionContext:suggestionContext hasCaptions:hasCaptions allowCaptionEntities:allowCaptionEntities hasTimer:self.hasTimer onlyCrop:self.onlyCrop inhibitDocumentCaptions:inhibitDocumentCaptions inhibitMute:self.inhibitMute asFile:asFile itemsLimit:0 recipientName:self.recipientName hasSilentPosting:self.hasSilentPosting hasSchedule:self.hasSchedule reminder:self.reminder]; } - (TGMediaPickerModernGalleryMixin *)galleryMixinForIndexPath:(NSIndexPath *)indexPath previewMode:(bool)previewMode outAsset:(TGMediaAsset **)outAsset diff --git a/submodules/LegacyComponents/LegacyComponents/TGMediaAvatarMenuMixin.m b/submodules/LegacyComponents/LegacyComponents/TGMediaAvatarMenuMixin.m index b43bcd9353..324804226e 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGMediaAvatarMenuMixin.m +++ b/submodules/LegacyComponents/LegacyComponents/TGMediaAvatarMenuMixin.m @@ -128,7 +128,7 @@ }; [itemViews addObject:carouselItem]; - TGMenuSheetButtonItemView *galleryItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.ChoosePhoto") type:TGMenuSheetButtonTypeDefault action:^ + TGMenuSheetButtonItemView *galleryItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.ChoosePhoto") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ { __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; if (strongSelf == nil) @@ -145,7 +145,7 @@ if (_hasSearchButton) { - TGMenuSheetButtonItemView *viewItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"AttachmentMenu.WebSearch") type:TGMenuSheetButtonTypeDefault action:^ + TGMenuSheetButtonItemView *viewItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"AttachmentMenu.WebSearch") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ { __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; if (strongSelf == nil) @@ -164,7 +164,7 @@ if (_hasViewButton) { - TGMenuSheetButtonItemView *viewItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Settings.ViewPhoto") type:TGMenuSheetButtonTypeDefault action:^ + TGMenuSheetButtonItemView *viewItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Settings.ViewPhoto") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ { __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; if (strongSelf == nil) @@ -182,7 +182,7 @@ if (_hasDeleteButton) { - TGMenuSheetButtonItemView *deleteItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"GroupInfo.SetGroupPhotoDelete") type:TGMenuSheetButtonTypeDestructive action:^ + TGMenuSheetButtonItemView *deleteItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"GroupInfo.SetGroupPhotoDelete") type:TGMenuSheetButtonTypeDestructive fontSize:20.0 action:^ { __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; if (strongSelf == nil) @@ -198,7 +198,7 @@ [itemViews addObject:deleteItem]; } - TGMenuSheetButtonItemView *cancelItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel action:^ + TGMenuSheetButtonItemView *cancelItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ { __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; if (strongSelf == nil) @@ -257,6 +257,9 @@ { if (![[[LegacyComponentsGlobals provider] accessChecker] checkCameraAuthorizationStatusForIntent:TGCameraAccessIntentDefault alertDismissCompletion:nil]) return; + + if ([_context currentlyInSplitView]) + return; if ([TGCameraController useLegacyCamera]) { diff --git a/submodules/LegacyComponents/LegacyComponents/TGMediaPickerController.h b/submodules/LegacyComponents/LegacyComponents/TGMediaPickerController.h index b78e565560..8cd104554b 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGMediaPickerController.h +++ b/submodules/LegacyComponents/LegacyComponents/TGMediaPickerController.h @@ -29,6 +29,7 @@ @property (nonatomic, strong) NSString *recipientName; @property (nonatomic, assign) bool hasSilentPosting; @property (nonatomic, assign) bool hasSchedule; +@property (nonatomic, assign) bool reminder; @property (nonatomic, copy) void (^presentScheduleController)(void (^)(int32_t)); @property (nonatomic, strong) TGMediaAssetsPallete *pallete; diff --git a/submodules/LegacyComponents/LegacyComponents/TGMediaPickerGalleryInterfaceView.m b/submodules/LegacyComponents/LegacyComponents/TGMediaPickerGalleryInterfaceView.m index c94f055775..7bcdea25b5 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGMediaPickerGalleryInterfaceView.m +++ b/submodules/LegacyComponents/LegacyComponents/TGMediaPickerGalleryInterfaceView.m @@ -609,7 +609,7 @@ NSArray *items = @ [ - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Camera.Discard") type:TGMenuSheetButtonTypeDefault action:^ + [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Camera.Discard") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; if (strongController == nil) @@ -624,7 +624,7 @@ [strongController dismissAnimated:true manual:false completion:nil]; }], - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel action:^ + [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; if (strongController != nil) diff --git a/submodules/LegacyComponents/LegacyComponents/TGMediaPickerModernGalleryMixin.h b/submodules/LegacyComponents/LegacyComponents/TGMediaPickerModernGalleryMixin.h index d3deb03362..f35fd05706 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGMediaPickerModernGalleryMixin.h +++ b/submodules/LegacyComponents/LegacyComponents/TGMediaPickerModernGalleryMixin.h @@ -29,9 +29,9 @@ @property (nonatomic, copy) void (^presentScheduleController)(void (^)(int32_t)); -- (instancetype)initWithContext:(id)context item:(id)item fetchResult:(TGMediaAssetFetchResult *)fetchResult parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext suggestionContext:(TGSuggestionContext *)suggestionContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions inhibitMute:(bool)inhibitMute asFile:(bool)asFile itemsLimit:(NSUInteger)itemsLimit recipientName:(NSString *)recipientName hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule; +- (instancetype)initWithContext:(id)context item:(id)item fetchResult:(TGMediaAssetFetchResult *)fetchResult parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext suggestionContext:(TGSuggestionContext *)suggestionContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions inhibitMute:(bool)inhibitMute asFile:(bool)asFile itemsLimit:(NSUInteger)itemsLimit recipientName:(NSString *)recipientName hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule reminder:(bool)reminder; -- (instancetype)initWithContext:(id)context item:(id)item momentList:(TGMediaAssetMomentList *)momentList parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext suggestionContext:(TGSuggestionContext *)suggestionContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions inhibitMute:(bool)inhibitMute asFile:(bool)asFile itemsLimit:(NSUInteger)itemsLimit hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule; +- (instancetype)initWithContext:(id)context item:(id)item momentList:(TGMediaAssetMomentList *)momentList parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext suggestionContext:(TGSuggestionContext *)suggestionContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions inhibitMute:(bool)inhibitMute asFile:(bool)asFile itemsLimit:(NSUInteger)itemsLimit hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule reminder:(bool)reminder; - (void)present; - (void)updateWithFetchResult:(TGMediaAssetFetchResult *)fetchResult; diff --git a/submodules/LegacyComponents/LegacyComponents/TGMediaPickerModernGalleryMixin.m b/submodules/LegacyComponents/LegacyComponents/TGMediaPickerModernGalleryMixin.m index eaca03b4ec..a2e2b5292b 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGMediaPickerModernGalleryMixin.m +++ b/submodules/LegacyComponents/LegacyComponents/TGMediaPickerModernGalleryMixin.m @@ -41,17 +41,17 @@ @implementation TGMediaPickerModernGalleryMixin -- (instancetype)initWithContext:(id)context item:(id)item fetchResult:(TGMediaAssetFetchResult *)fetchResult parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext suggestionContext:(TGSuggestionContext *)suggestionContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions inhibitMute:(bool)inhibitMute asFile:(bool)asFile itemsLimit:(NSUInteger)itemsLimit recipientName:(NSString *)recipientName hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule +- (instancetype)initWithContext:(id)context item:(id)item fetchResult:(TGMediaAssetFetchResult *)fetchResult parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext suggestionContext:(TGSuggestionContext *)suggestionContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions inhibitMute:(bool)inhibitMute asFile:(bool)asFile itemsLimit:(NSUInteger)itemsLimit recipientName:(NSString *)recipientName hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule reminder:(bool)reminder { - return [self initWithContext:context item:item fetchResult:fetchResult momentList:nil parentController:parentController thumbnailImage:thumbnailImage selectionContext:selectionContext editingContext:editingContext suggestionContext:suggestionContext hasCaptions:hasCaptions allowCaptionEntities:allowCaptionEntities hasTimer:hasTimer onlyCrop:onlyCrop inhibitDocumentCaptions:inhibitDocumentCaptions inhibitMute:inhibitMute asFile:asFile itemsLimit:itemsLimit recipientName:recipientName hasSilentPosting:hasSilentPosting hasSchedule:hasSchedule]; + return [self initWithContext:context item:item fetchResult:fetchResult momentList:nil parentController:parentController thumbnailImage:thumbnailImage selectionContext:selectionContext editingContext:editingContext suggestionContext:suggestionContext hasCaptions:hasCaptions allowCaptionEntities:allowCaptionEntities hasTimer:hasTimer onlyCrop:onlyCrop inhibitDocumentCaptions:inhibitDocumentCaptions inhibitMute:inhibitMute asFile:asFile itemsLimit:itemsLimit recipientName:recipientName hasSilentPosting:hasSilentPosting hasSchedule:hasSchedule reminder:reminder]; } -- (instancetype)initWithContext:(id)context item:(id)item momentList:(TGMediaAssetMomentList *)momentList parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext suggestionContext:(TGSuggestionContext *)suggestionContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions inhibitMute:(bool)inhibitMute asFile:(bool)asFile itemsLimit:(NSUInteger)itemsLimit hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule +- (instancetype)initWithContext:(id)context item:(id)item momentList:(TGMediaAssetMomentList *)momentList parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext suggestionContext:(TGSuggestionContext *)suggestionContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions inhibitMute:(bool)inhibitMute asFile:(bool)asFile itemsLimit:(NSUInteger)itemsLimit hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule reminder:(bool)reminder { - return [self initWithContext:context item:item fetchResult:nil momentList:momentList parentController:parentController thumbnailImage:thumbnailImage selectionContext:selectionContext editingContext:editingContext suggestionContext:suggestionContext hasCaptions:hasCaptions allowCaptionEntities:allowCaptionEntities hasTimer:hasTimer onlyCrop:onlyCrop inhibitDocumentCaptions:inhibitDocumentCaptions inhibitMute:inhibitMute asFile:asFile itemsLimit:itemsLimit recipientName:nil hasSilentPosting:hasSilentPosting hasSchedule:hasSchedule]; + return [self initWithContext:context item:item fetchResult:nil momentList:momentList parentController:parentController thumbnailImage:thumbnailImage selectionContext:selectionContext editingContext:editingContext suggestionContext:suggestionContext hasCaptions:hasCaptions allowCaptionEntities:allowCaptionEntities hasTimer:hasTimer onlyCrop:onlyCrop inhibitDocumentCaptions:inhibitDocumentCaptions inhibitMute:inhibitMute asFile:asFile itemsLimit:itemsLimit recipientName:nil hasSilentPosting:hasSilentPosting hasSchedule:hasSchedule reminder:reminder]; } -- (instancetype)initWithContext:(id)context item:(id)item fetchResult:(TGMediaAssetFetchResult *)fetchResult momentList:(TGMediaAssetMomentList *)momentList parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext suggestionContext:(TGSuggestionContext *)suggestionContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions inhibitMute:(bool)inhibitMute asFile:(bool)asFile itemsLimit:(NSUInteger)itemsLimit recipientName:(NSString *)recipientName hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule +- (instancetype)initWithContext:(id)context item:(id)item fetchResult:(TGMediaAssetFetchResult *)fetchResult momentList:(TGMediaAssetMomentList *)momentList parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext suggestionContext:(TGSuggestionContext *)suggestionContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions inhibitMute:(bool)inhibitMute asFile:(bool)asFile itemsLimit:(NSUInteger)itemsLimit recipientName:(NSString *)recipientName hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule reminder:(bool)reminder { self = [super init]; if (self != nil) @@ -164,7 +164,7 @@ } } - TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:strongSelf->_context sendButtonFrame:strongSelf.galleryModel.interfaceView.doneButtonFrame canSendSilently:hasSilentPosting canSchedule:effectiveHasSchedule]; + TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:strongSelf->_context isDark:true sendButtonFrame:strongSelf.galleryModel.interfaceView.doneButtonFrame canSendSilently:hasSilentPosting canSchedule:effectiveHasSchedule reminder:reminder]; controller.send = ^{ __strong TGMediaPickerModernGalleryMixin *strongSelf = weakSelf; if (strongSelf == nil) diff --git a/submodules/LegacyComponents/LegacyComponents/TGMediaPickerSendActionSheetController.h b/submodules/LegacyComponents/LegacyComponents/TGMediaPickerSendActionSheetController.h index 6ad00b1e4c..8bac447722 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGMediaPickerSendActionSheetController.h +++ b/submodules/LegacyComponents/LegacyComponents/TGMediaPickerSendActionSheetController.h @@ -8,7 +8,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy) void (^sendSilently)(void); @property (nonatomic, copy) void (^schedule)(void); -- (instancetype)initWithContext:(id)context sendButtonFrame:(CGRect)sendButtonFrame canSendSilently:(bool)canSendSilently canSchedule:(bool)canSchedule; +- (instancetype)initWithContext:(id)context isDark:(bool)isDark sendButtonFrame:(CGRect)sendButtonFrame canSendSilently:(bool)canSendSilently canSchedule:(bool)canSchedule reminder:(bool)reminder; @end diff --git a/submodules/LegacyComponents/LegacyComponents/TGMediaPickerSendActionSheetController.m b/submodules/LegacyComponents/LegacyComponents/TGMediaPickerSendActionSheetController.m index da25e89099..1471e0ac08 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGMediaPickerSendActionSheetController.m +++ b/submodules/LegacyComponents/LegacyComponents/TGMediaPickerSendActionSheetController.m @@ -11,6 +11,7 @@ TGModernButton *_buttonView; UILabel *_buttonLabel; UIImageView *_buttonIcon; + UIView *_separatorView; } @property (nonatomic, readonly) UILabel *buttonLabel; @@ -20,7 +21,7 @@ @implementation TGMediaPickerSendActionSheetItemView -- (instancetype)initWithTitle:(NSString *)title icon:(UIImage *)icon { +- (instancetype)initWithTitle:(NSString *)title icon:(UIImage *)icon isDark:(bool)isDark isLast:(bool)isLast { self = [super init]; if (self != nil) { _buttonView = [[TGModernButton alloc] init]; @@ -31,9 +32,17 @@ __strong TGMediaPickerSendActionSheetItemView *strongSelf = weakSelf; if (strongSelf != nil) { if (highlighted) { - strongSelf->_buttonView.backgroundColor = UIColorRGB(0x363636); + if (isDark) { + strongSelf->_buttonView.backgroundColor = UIColorRGB(0x363636); + } else { + strongSelf->_buttonView.backgroundColor = UIColorRGBA(0x3c3c43, 0.2); + } } else { - strongSelf->_buttonView.backgroundColor = [UIColor clearColor]; + if (isDark) { + strongSelf->_buttonView.backgroundColor = [UIColor clearColor]; + } else { + strongSelf->_buttonView.backgroundColor = [UIColor clearColor]; + } } } }; @@ -43,15 +52,32 @@ _buttonLabel = [[UILabel alloc] init]; _buttonLabel.font = TGSystemFontOfSize(17.0f); _buttonLabel.text = title; - _buttonLabel.textColor = [UIColor whiteColor]; + if (isDark) { + _buttonLabel.textColor = [UIColor whiteColor]; + } else { + _buttonLabel.textColor = [UIColor blackColor]; + } [_buttonLabel sizeToFit]; _buttonLabel.userInteractionEnabled = false; [self addSubview:_buttonLabel]; _buttonIcon = [[UIImageView alloc] init]; - _buttonIcon.image = TGTintedImage(icon, [UIColor whiteColor]); + if (isDark) { + _buttonIcon.image = TGTintedImage(icon, [UIColor whiteColor]); + } else { + _buttonIcon.image = TGTintedImage(icon, [UIColor blackColor]); + } [_buttonIcon sizeToFit]; [self addSubview:_buttonIcon]; + + if (!isLast) { + _separatorView = [[UIView alloc] init]; + if (isDark) { + } else { + _separatorView.backgroundColor = UIColorRGBA(0x3c3c43, 0.2); + } + [self addSubview:_separatorView]; + } } return self; } @@ -65,6 +91,7 @@ _buttonLabel.frame = CGRectMake(16.0, 11.0, _buttonLabel.frame.size.width, _buttonLabel.frame.size.height); _buttonView.frame = self.bounds; _buttonIcon.frame = CGRectMake(self.bounds.size.width - _buttonIcon.frame.size.width - 12.0, 9.0, _buttonIcon.frame.size.width, _buttonIcon.frame.size.height); + _separatorView.frame = CGRectMake(0.0f, self.bounds.size.height, self.bounds.size.width, 1.0f / [UIScreen mainScreen].scale); } @end @@ -73,9 +100,11 @@ { id _context; + bool _isDark; CGRect _sendButtonFrame; bool _canSendSilently; bool _canSchedule; + bool _reminder; bool _autorotationWasEnabled; bool _dismissed; @@ -91,13 +120,15 @@ @implementation TGMediaPickerSendActionSheetController -- (instancetype)initWithContext:(id)context sendButtonFrame:(CGRect)sendButtonFrame canSendSilently:(bool)canSendSilently canSchedule:(bool)canSchedule { +- (instancetype)initWithContext:(id)context isDark:(bool)isDark sendButtonFrame:(CGRect)sendButtonFrame canSendSilently:(bool)canSendSilently canSchedule:(bool)canSchedule reminder:(bool)reminder { self = [super initWithContext:context]; if (self != nil) { _context = context; + _isDark = isDark; _sendButtonFrame = sendButtonFrame; _canSendSilently = canSendSilently; _canSchedule = canSchedule; + _reminder = reminder; } return self; } @@ -107,19 +138,41 @@ _effectView = [[UIVisualEffectView alloc] initWithEffect:nil]; if (iosMajorVersion() >= 9) { - _effectView.effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; + if (_isDark) { + _effectView.effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; + } else { + _effectView.effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; + } } [self.view addSubview:_effectView]; + /* + let contextMenu = PresentationThemeContextMenu( + dimColor: UIColor(rgb: 0x000a26, alpha: 0.2), + backgroundColor: UIColor(rgb: 0xf9f9f9, alpha: 0.78), + itemSeparatorColor: UIColor(rgb: 0x3c3c43, alpha: 0.2), + sectionSeparatorColor: UIColor(rgb: 0x8a8a8a, alpha: 0.2), + itemBackgroundColor: UIColor(rgb: 0x000000, alpha: 0.0), + itemHighlightedBackgroundColor: UIColor(rgb: 0x3c3c43, alpha: 0.2), + primaryColor: UIColor(rgb: 0x000000, alpha: 1.0), + secondaryColor: UIColor(rgb: 0x000000, alpha: 0.8), + destructiveColor: UIColor(rgb: 0xff3b30) + ) + */ + _containerView = [[UIView alloc] init]; - _containerView.backgroundColor = UIColorRGB(0x1f1f1f); + if (_isDark) { + _containerView.backgroundColor = UIColorRGB(0x1f1f1f); + } else { + _containerView.backgroundColor = UIColorRGBA(0xf9f9f9, 0.78); + } _containerView.clipsToBounds = true; _containerView.layer.cornerRadius = 12.0; [self.view addSubview:_containerView]; __weak TGMediaPickerSendActionSheetController *weakSelf = self; if (_canSendSilently) { - _sendSilentlyButton = [[TGMediaPickerSendActionSheetItemView alloc] initWithTitle:TGLocalized(@"Conversation.SendMessage.SendSilently") icon:TGComponentsImageNamed(@"MediaMute")]; + _sendSilentlyButton = [[TGMediaPickerSendActionSheetItemView alloc] initWithTitle:TGLocalized(@"Conversation.SendMessage.SendSilently") icon:TGComponentsImageNamed(@"MediaMute") isDark:_isDark isLast:!_canSchedule]; _sendSilentlyButton.pressed = ^{ __strong TGMediaPickerSendActionSheetController *strongSelf = weakSelf; [strongSelf sendSilentlyPressed]; @@ -128,7 +181,7 @@ } if (_canSchedule) { - _scheduleButton = [[TGMediaPickerSendActionSheetItemView alloc] initWithTitle:TGLocalized(@"Conversation.SendMessage.ScheduleMessage") icon:TGComponentsImageNamed(@"MediaSchedule")]; + _scheduleButton = [[TGMediaPickerSendActionSheetItemView alloc] initWithTitle:TGLocalized(_reminder ? @"Conversation.SendMessage.SetReminder" : @"Conversation.SendMessage.ScheduleMessage") icon:TGComponentsImageNamed(@"MediaSchedule") isDark:_isDark isLast:true]; _scheduleButton.pressed = ^{ __strong TGMediaPickerSendActionSheetController *strongSelf = weakSelf; [strongSelf schedulePressed]; diff --git a/submodules/LegacyComponents/LegacyComponents/TGMenuSheetButtonItemView.h b/submodules/LegacyComponents/LegacyComponents/TGMenuSheetButtonItemView.h index 610e8925cd..730f304e2f 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGMenuSheetButtonItemView.h +++ b/submodules/LegacyComponents/LegacyComponents/TGMenuSheetButtonItemView.h @@ -21,7 +21,7 @@ typedef enum @property (nonatomic, copy) void (^action)(void); @property (nonatomic, assign) bool thickDivider; -- (instancetype)initWithTitle:(NSString *)title type:(TGMenuSheetButtonType)type action:(void (^)(void))action; +- (instancetype)initWithTitle:(NSString *)title type:(TGMenuSheetButtonType)type fontSize:(CGFloat)fontSize action:(void (^)(void))action; @property (nonatomic, assign) bool collapsed; - (void)setCollapsed:(bool)collapsed animated:(bool)animated; diff --git a/submodules/LegacyComponents/LegacyComponents/TGMenuSheetButtonItemView.m b/submodules/LegacyComponents/LegacyComponents/TGMenuSheetButtonItemView.m index e6c781d84a..bdd068021c 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGMenuSheetButtonItemView.m +++ b/submodules/LegacyComponents/LegacyComponents/TGMenuSheetButtonItemView.m @@ -16,6 +16,7 @@ const CGFloat TGMenuSheetButtonItemViewHeight = 57.0f; bool _dark; bool _requiresDivider; UIView *_customDivider; + CGFloat _fontSize; TGMenuSheetPallete *_pallete; } @@ -23,12 +24,13 @@ const CGFloat TGMenuSheetButtonItemViewHeight = 57.0f; @implementation TGMenuSheetButtonItemView -- (instancetype)initWithTitle:(NSString *)title type:(TGMenuSheetButtonType)type action:(void (^)(void))action +- (instancetype)initWithTitle:(NSString *)title type:(TGMenuSheetButtonType)type fontSize:(CGFloat)fontSize action:(void (^)(void))action { self = [super initWithType:(type == TGMenuSheetButtonTypeCancel) ? TGMenuSheetItemTypeFooter : TGMenuSheetItemTypeDefault]; if (self != nil) { self.action = action; + _fontSize = fontSize; _buttonType = type; _requiresDivider = true; @@ -115,7 +117,7 @@ const CGFloat TGMenuSheetButtonItemViewHeight = 57.0f; - (void)_updateForType:(TGMenuSheetButtonType)type { - _button.titleLabel.font = (type == TGMenuSheetButtonTypeCancel || type == TGMenuSheetButtonTypeSend) ? TGMediumSystemFontOfSize(20) : TGSystemFontOfSize(20); + _button.titleLabel.font = (type == TGMenuSheetButtonTypeCancel || type == TGMenuSheetButtonTypeSend) ? TGMediumSystemFontOfSize(_fontSize) : TGSystemFontOfSize(_fontSize); UIColor *accentColor = _dark ? UIColorRGB(0x4fbcff) : TGAccentColor(); if (_pallete != nil) accentColor = _pallete.accentColor; diff --git a/submodules/LegacyComponents/LegacyComponents/TGPassportAttachMenu.m b/submodules/LegacyComponents/LegacyComponents/TGPassportAttachMenu.m index cfe5081397..fe894de053 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGPassportAttachMenu.m +++ b/submodules/LegacyComponents/LegacyComponents/TGPassportAttachMenu.m @@ -84,7 +84,7 @@ }; [itemViews addObject:carouselItem]; - TGMenuSheetButtonItemView *galleryItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.ChoosePhoto") type:TGMenuSheetButtonTypeDefault action:^ + TGMenuSheetButtonItemView *galleryItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.ChoosePhoto") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; if (strongController == nil) @@ -101,7 +101,7 @@ if (iosMajorVersion() >= 8 && intent != TGPassportAttachIntentSelfie) { - TGMenuSheetButtonItemView *icloudItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Conversation.FileICloudDrive") type:TGMenuSheetButtonTypeDefault action:^ + TGMenuSheetButtonItemView *icloudItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Conversation.FileICloudDrive") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; if (strongController == nil) @@ -124,7 +124,7 @@ } carouselItem.remainingHeight = TGMenuSheetButtonItemViewHeight * (itemViews.count - 1); - TGMenuSheetButtonItemView *cancelItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel action:^ + TGMenuSheetButtonItemView *cancelItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; if (strongController == nil) diff --git a/submodules/LegacyComponents/LegacyComponents/TGPhotoCropController.m b/submodules/LegacyComponents/LegacyComponents/TGPhotoCropController.m index 1f2dbdfd46..7e3847d2b7 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGPhotoCropController.m +++ b/submodules/LegacyComponents/LegacyComponents/TGPhotoCropController.m @@ -625,8 +625,8 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original"; }; NSMutableArray *items = [[NSMutableArray alloc] init]; - [items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioOriginal") type:TGMenuSheetButtonTypeDefault action:^{ action(TGPhotoCropOriginalAspectRatio); }]]; - [items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioSquare") type:TGMenuSheetButtonTypeDefault action:^{ action(@"1.0"); }]]; + [items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioOriginal") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^{ action(TGPhotoCropOriginalAspectRatio); }]]; + [items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioSquare") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^{ action(@"1.0"); }]]; CGSize croppedImageSize = _cropView.cropRect.size; if (_cropView.cropOrientation == UIImageOrientationLeft || _cropView.cropOrientation == UIImageOrientationRight) @@ -666,10 +666,10 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original"; ratio = heightComponent / widthComponent; - [items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:[NSString stringWithFormat:@"%d:%d", (int)widthComponent, (int)heightComponent] type:TGMenuSheetButtonTypeDefault action:^{ action([NSString stringWithFormat:@"%f", ratio]); }]]; + [items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:[NSString stringWithFormat:@"%d:%d", (int)widthComponent, (int)heightComponent] type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^{ action([NSString stringWithFormat:@"%f", ratio]); }]]; } - [items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel action:^ + [items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; if (strongController != nil) diff --git a/submodules/LegacyComponents/LegacyComponents/TGPhotoEditorController.m b/submodules/LegacyComponents/LegacyComponents/TGPhotoEditorController.m index ecab32e66d..0233810df4 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGPhotoEditorController.m +++ b/submodules/LegacyComponents/LegacyComponents/TGPhotoEditorController.m @@ -1341,7 +1341,7 @@ NSArray *items = @ [ - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.DiscardChanges") type:TGMenuSheetButtonTypeDefault action:^ + [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.DiscardChanges") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; if (strongController == nil) @@ -1352,7 +1352,7 @@ dismiss(); }]; }], - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel action:^ + [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; if (strongController != nil) diff --git a/submodules/LegacyComponents/LegacyComponents/TGPhotoEditorSliderView.m b/submodules/LegacyComponents/LegacyComponents/TGPhotoEditorSliderView.m index 1703bca957..22bd9ee45d 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGPhotoEditorSliderView.m +++ b/submodules/LegacyComponents/LegacyComponents/TGPhotoEditorSliderView.m @@ -279,8 +279,10 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f; - (void)setTrackColor:(UIColor *)trackColor { - _trackColor = trackColor; - [self setNeedsDisplay]; + if (_trackColor == nil || ![_trackColor isEqual:trackColor]) { + _trackColor = trackColor; + [self setNeedsDisplay]; + } } - (UIColor *)startColor diff --git a/submodules/LegacyComponents/LegacyComponents/TGPhotoPaintController.m b/submodules/LegacyComponents/LegacyComponents/TGPhotoPaintController.m index 664256614b..e8a25a309f 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGPhotoPaintController.m +++ b/submodules/LegacyComponents/LegacyComponents/TGPhotoPaintController.m @@ -550,7 +550,7 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f; __weak TGPhotoPaintController *weakSelf = self; NSArray *items = @ [ - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Paint.ClearConfirm") type:TGMenuSheetButtonTypeDestructive action:^ + [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Paint.ClearConfirm") type:TGMenuSheetButtonTypeDestructive fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; if (strongController == nil) @@ -570,7 +570,7 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f; [strongController dismissAnimated:true manual:false completion:nil]; }], - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel action:^ + [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ { __strong TGMenuSheetController *strongController = weakController; if (strongController != nil) diff --git a/submodules/LegacyComponents/LegacyComponents/TGPhotoVideoEditor.h b/submodules/LegacyComponents/LegacyComponents/TGPhotoVideoEditor.h index ff8cfd650c..666a17ec58 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGPhotoVideoEditor.h +++ b/submodules/LegacyComponents/LegacyComponents/TGPhotoVideoEditor.h @@ -2,6 +2,6 @@ @interface TGPhotoVideoEditor : NSObject -+ (void)presentWithContext:(id)context controller:(TGViewController *)controller caption:(NSString *)caption entities:(NSArray *)entities withItem:(id)item recipientName:(NSString *)recipientName completion:(void (^)(id, TGMediaEditingContext *))completion; ++ (void)presentWithContext:(id)context controller:(TGViewController *)controller caption:(NSString *)caption entities:(NSArray *)entities withItem:(id)item recipientName:(NSString *)recipientName completion:(void (^)(id, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed; @end diff --git a/submodules/LegacyComponents/LegacyComponents/TGPhotoVideoEditor.m b/submodules/LegacyComponents/LegacyComponents/TGPhotoVideoEditor.m index ee6a2bc03d..666333a543 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGPhotoVideoEditor.m +++ b/submodules/LegacyComponents/LegacyComponents/TGPhotoVideoEditor.m @@ -10,7 +10,7 @@ @implementation TGPhotoVideoEditor -+ (void)presentWithContext:(id)context controller:(TGViewController *)controller caption:(NSString *)caption entities:(NSArray *)entities withItem:(id)item recipientName:(NSString *)recipientName completion:(void (^)(id, TGMediaEditingContext *))completion ++ (void)presentWithContext:(id)context controller:(TGViewController *)controller caption:(NSString *)caption entities:(NSArray *)entities withItem:(id)item recipientName:(NSString *)recipientName completion:(void (^)(id, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed { id windowManager = [context makeOverlayWindowManager]; id windowContext = [windowManager context]; @@ -78,13 +78,15 @@ if (completion != nil) completion(item.asset, editingContext); - [UIView animateWithDuration:0.3f delay:0.0f options:(7 << 16) animations:^ + [strongController dismissWhenReadyAnimated:true]; + + /*[UIView animateWithDuration:0.3f delay:0.0f options:(7 << 16) animations:^ { strongController.view.frame = CGRectOffset(strongController.view.frame, 0, strongController.view.frame.size.height); } completion:^(__unused BOOL finished) { [strongController dismiss]; - }]; + }];*/ }; galleryController.beginTransitionIn = ^UIView *(__unused TGMediaPickerGalleryItem *item, __unused TGModernGalleryItemView *itemView) @@ -107,6 +109,9 @@ if ([window isKindOfClass:[TGOverlayControllerWindow class]]) [window dismiss]; } + if (dismissed) { + dismissed(); + } }; TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:windowManager parentController:controller contentController:galleryController]; diff --git a/submodules/LegacyComponents/LegacyComponents/TGSecretTimerMenu.m b/submodules/LegacyComponents/LegacyComponents/TGSecretTimerMenu.m index b14fcf5dce..1398251a35 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGSecretTimerMenu.m +++ b/submodules/LegacyComponents/LegacyComponents/TGSecretTimerMenu.m @@ -37,7 +37,7 @@ __weak TGMenuSheetController *weakController = controller; __weak TGSecretTimerPickerItemView *weakTimerItem = timerItem; - TGMenuSheetButtonItemView *doneItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Done") type:TGMenuSheetButtonTypeSend action:^ + TGMenuSheetButtonItemView *doneItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Done") type:TGMenuSheetButtonTypeSend fontSize:20.0 action:^ { __strong TGSecretTimerPickerItemView *strongTimerItem = weakTimerItem; if (strongTimerItem != nil) diff --git a/submodules/LegacyComponents/LegacyComponents/TGVideoCameraMovieRecorder.h b/submodules/LegacyComponents/LegacyComponents/TGVideoCameraMovieRecorder.h index a78b7baae3..19f9f1a4b5 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGVideoCameraMovieRecorder.h +++ b/submodules/LegacyComponents/LegacyComponents/TGVideoCameraMovieRecorder.h @@ -18,7 +18,7 @@ - (void)appendVideoPixelBuffer:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime; - (void)appendAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer; -- (void)finishRecording; +- (void)finishRecording:(void(^)())completed; - (NSTimeInterval)videoDuration; diff --git a/submodules/LegacyComponents/LegacyComponents/TGVideoCameraMovieRecorder.m b/submodules/LegacyComponents/LegacyComponents/TGVideoCameraMovieRecorder.m index 0be65e1b85..e236e9ae00 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGVideoCameraMovieRecorder.m +++ b/submodules/LegacyComponents/LegacyComponents/TGVideoCameraMovieRecorder.m @@ -105,7 +105,7 @@ typedef enum { if (_status != TGMovieRecorderStatusIdle) return; - [self transitionToStatus:TGMovieRecorderStatusPreparingToRecord error:nil]; + [self transitionToStatus:TGMovieRecorderStatusPreparingToRecord error:nil completed:nil]; } dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^ @@ -138,9 +138,9 @@ typedef enum { @synchronized (self) { if (error || !succeed) - [self transitionToStatus:TGMovieRecorderStatusFailed error:error]; + [self transitionToStatus:TGMovieRecorderStatusFailed error:error completed:nil]; else - [self transitionToStatus:TGMovieRecorderStatusRecording error:nil]; + [self transitionToStatus:TGMovieRecorderStatusRecording error:nil completed:nil]; } } } ); @@ -169,8 +169,9 @@ typedef enum { [self appendSampleBuffer:sampleBuffer ofMediaType:AVMediaTypeAudio]; } -- (void)finishRecording +- (void)finishRecording:(void(^)())completed { + printf("finishRecording %d\n", _status); @synchronized (self) { bool shouldFinishRecording = false; @@ -190,9 +191,10 @@ typedef enum { } if (shouldFinishRecording) - [self transitionToStatus:TGMovieRecorderStatusFinishingWaiting error:nil]; - else + [self transitionToStatus:TGMovieRecorderStatusFinishingWaiting error:nil completed:completed]; + else { return; + } } dispatch_async(_writingQueue, ^ @@ -201,10 +203,14 @@ typedef enum { { @synchronized (self) { - if (_status != TGMovieRecorderStatusFinishingWaiting) + if (_status != TGMovieRecorderStatusFinishingWaiting) { + if (completed) { + completed(); + } return; + } - [self transitionToStatus:TGMovieRecorderStatusFinishingCommiting error:nil]; + [self transitionToStatus:TGMovieRecorderStatusFinishingCommiting error:nil completed:nil]; } [_assetWriter finishWritingWithCompletionHandler:^ @@ -213,9 +219,9 @@ typedef enum { { NSError *error = _assetWriter.error; if (error) - [self transitionToStatus:TGMovieRecorderStatusFailed error:error]; + [self transitionToStatus:TGMovieRecorderStatusFailed error:error completed:completed]; else - [self transitionToStatus:TGMovieRecorderStatusFinished error:nil]; + [self transitionToStatus:TGMovieRecorderStatusFinished error:nil completed:completed]; } }]; } @@ -340,7 +346,7 @@ typedef enum { NSError *error = _assetWriter.error; @synchronized (self) { - [self transitionToStatus:TGMovieRecorderStatusFailed error:error]; + [self transitionToStatus:TGMovieRecorderStatusFailed error:error completed:nil]; } } } @@ -349,8 +355,10 @@ typedef enum { }); } -- (void)transitionToStatus:(TGMovieRecorderStatus)newStatus error:(NSError *)error +- (void)transitionToStatus:(TGMovieRecorderStatus)newStatus error:(NSError *)error completed:(void(^)())completed { + printf("recorder transitionToStatus %d\n", newStatus); + bool shouldNotifyDelegate = false; if (newStatus != _status) @@ -389,6 +397,7 @@ typedef enum { break; case TGMovieRecorderStatusFinished: + printf("TGMovieRecorderStatusFinished _delegate == nil = %d\n", (int)(_delegate == nil)); [_delegate movieRecorderDidFinishRecording:self]; break; @@ -399,9 +408,16 @@ typedef enum { default: break; } + if (completed) { + completed(); + } } }); - } + } else { + if (completed) { + completed(); + } + } } - (bool)setupAssetWriterAudioInputWithSourceFormatDescription:(CMFormatDescriptionRef)audioFormatDescription settings:(NSDictionary *)audioSettings diff --git a/submodules/LegacyComponents/LegacyComponents/TGVideoCameraPipeline.h b/submodules/LegacyComponents/LegacyComponents/TGVideoCameraPipeline.h index 46cbde9207..6d79b12348 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGVideoCameraPipeline.h +++ b/submodules/LegacyComponents/LegacyComponents/TGVideoCameraPipeline.h @@ -21,7 +21,7 @@ - (void)stopRunning; - (void)startRecording:(NSURL *)url preset:(TGMediaVideoConversionPreset)preset liveUpload:(bool)liveUpload; -- (void)stopRecording; +- (void)stopRecording:(void (^)())completed; - (CGAffineTransform)transformForOrientation:(AVCaptureVideoOrientation)orientation; diff --git a/submodules/LegacyComponents/LegacyComponents/TGVideoCameraPipeline.m b/submodules/LegacyComponents/LegacyComponents/TGVideoCameraPipeline.m index cf69baa75e..b788f7d53c 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGVideoCameraPipeline.m +++ b/submodules/LegacyComponents/LegacyComponents/TGVideoCameraPipeline.m @@ -111,6 +111,7 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16; - (void)dealloc { + printf("Camera pipeline dealloc\n"); [self destroyCaptureSession]; } @@ -134,7 +135,7 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16; { _running = false; - [self stopRecording]; + [self stopRecording:^{}]; [_captureSession stopRunning]; [self captureSessionDidStopRunning]; @@ -285,7 +286,7 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16; - (void)captureSessionDidStopRunning { - [self stopRecording]; + [self stopRecording:^{}]; [self destroyVideoPipeline]; } @@ -684,20 +685,29 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16; [recorder prepareToRecord]; } -- (void)stopRecording +- (void)stopRecording:(void (^)())completed { [[TGVideoCameraPipeline cameraQueue] dispatch:^ { @synchronized (self) { - if (_recordingStatus != TGVideoCameraRecordingStatusRecording) + if (_recordingStatus != TGVideoCameraRecordingStatusRecording) { + if (completed) { + completed(); + } return; + } [self transitionToRecordingStatus:TGVideoCameraRecordingStatusStoppingRecording error:nil]; } _resultDuration = _recorder.videoDuration; - [_recorder finishRecording]; + [_recorder finishRecording:^{ + __unused __auto_type description = [self description]; + if (completed) { + completed(); + } + }]; }]; } @@ -734,6 +744,8 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16; - (void)movieRecorderDidFinishRecording:(TGVideoCameraMovieRecorder *)__unused recorder { + printf("movieRecorderDidFinishRecording\n"); + @synchronized (self) { if (_recordingStatus != TGVideoCameraRecordingStatusStoppingRecording) @@ -750,6 +762,8 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16; - (void)transitionToRecordingStatus:(TGVideoCameraRecordingStatus)newStatus error:(NSError *)error { + printf("transitionToRecordingStatus %d\n", newStatus); + TGVideoCameraRecordingStatus oldStatus = _recordingStatus; _recordingStatus = newStatus; @@ -763,12 +777,16 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16; } else { + __strong id delegate = _delegate; if ((oldStatus == TGVideoCameraRecordingStatusStartingRecording) && (newStatus == TGVideoCameraRecordingStatusRecording)) - delegateCallbackBlock = ^{ [_delegate capturePipelineRecordingDidStart:self]; }; + delegateCallbackBlock = ^{ [delegate capturePipelineRecordingDidStart:self]; }; else if ((oldStatus == TGVideoCameraRecordingStatusRecording) && (newStatus == TGVideoCameraRecordingStatusStoppingRecording)) - delegateCallbackBlock = ^{ [_delegate capturePipelineRecordingWillStop:self]; }; + delegateCallbackBlock = ^{ [delegate capturePipelineRecordingWillStop:self]; }; else if ((oldStatus == TGVideoCameraRecordingStatusStoppingRecording) && (newStatus == TGVideoCameraRecordingStatusIdle)) - delegateCallbackBlock = ^{ [_delegate capturePipelineRecordingDidStop:self duration:_resultDuration liveUploadData:_liveUploadData thumbnailImage:_recordingThumbnail thumbnails:_thumbnails]; }; + delegateCallbackBlock = ^{ + printf("transitionToRecordingStatus delegateCallbackBlock _delegate == nil = %d\n", (int)(delegate == nil)); + [delegate capturePipelineRecordingDidStop:self duration:_resultDuration liveUploadData:_liveUploadData thumbnailImage:_recordingThumbnail thumbnails:_thumbnails]; + }; } if (delegateCallbackBlock != nil) diff --git a/submodules/LegacyComponents/LegacyComponents/TGVideoMessageCaptureController.h b/submodules/LegacyComponents/LegacyComponents/TGVideoMessageCaptureController.h index b9e76dc42c..0485923d6f 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGVideoMessageCaptureController.h +++ b/submodules/LegacyComponents/LegacyComponents/TGVideoMessageCaptureController.h @@ -19,14 +19,15 @@ @property (nonatomic, copy) id (^requestActivityHolder)(); @property (nonatomic, copy) void (^micLevel)(CGFloat level); -@property (nonatomic, copy) void(^finishedWithVideo)(NSURL *videoURL, UIImage *previewImage, NSUInteger fileSize, NSTimeInterval duration, CGSize dimensions, id liveUploadData, TGVideoEditAdjustments *adjustments); +@property (nonatomic, copy) void(^finishedWithVideo)(NSURL *videoURL, UIImage *previewImage, NSUInteger fileSize, NSTimeInterval duration, CGSize dimensions, id liveUploadData, TGVideoEditAdjustments *adjustments, bool, int32_t); @property (nonatomic, copy) void(^onDismiss)(bool isAuto); @property (nonatomic, copy) void(^onStop)(void); @property (nonatomic, copy) void(^onCancel)(void); @property (nonatomic, copy) void(^didDismiss)(void); @property (nonatomic, copy) void(^displaySlowmodeTooltip)(void); +@property (nonatomic, copy) void (^presentScheduleController)(void (^)(int32_t)); -- (instancetype)initWithContext:(id)context assets:(TGVideoMessageCaptureControllerAssets *)assets transitionInView:(UIView *(^)(void))transitionInView parentController:(TGViewController *)parentController controlsFrame:(CGRect)controlsFrame isAlreadyLocked:(bool (^)(void))isAlreadyLocked liveUploadInterface:(id)liveUploadInterface pallete:(TGModernConversationInputMicPallete *)pallete slowmodeTimestamp:(int32_t)slowmodeTimestamp slowmodeView:(UIView *(^)(void))slowmodeView; +- (instancetype)initWithContext:(id)context assets:(TGVideoMessageCaptureControllerAssets *)assets transitionInView:(UIView *(^)(void))transitionInView parentController:(TGViewController *)parentController controlsFrame:(CGRect)controlsFrame isAlreadyLocked:(bool (^)(void))isAlreadyLocked liveUploadInterface:(id)liveUploadInterface pallete:(TGModernConversationInputMicPallete *)pallete slowmodeTimestamp:(int32_t)slowmodeTimestamp slowmodeView:(UIView *(^)(void))slowmodeView canSendSilently:(bool)canSendSilently canSchedule:(bool)canSchedule reminder:(bool)reminder; - (void)buttonInteractionUpdate:(CGPoint)value; - (void)setLocked; diff --git a/submodules/LegacyComponents/LegacyComponents/TGVideoMessageCaptureController.m b/submodules/LegacyComponents/LegacyComponents/TGVideoMessageCaptureController.m index 7491c0aec7..0fbca0c6f4 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGVideoMessageCaptureController.m +++ b/submodules/LegacyComponents/LegacyComponents/TGVideoMessageCaptureController.m @@ -27,6 +27,9 @@ #import "TGColor.h" #import "TGImageUtils.h" +#import "TGMediaPickerSendActionSheetController.h" +#import "TGOverlayControllerWindow.h" + const NSTimeInterval TGVideoMessageMaximumDuration = 60.0; typedef enum @@ -129,6 +132,12 @@ typedef enum UIView * (^_slowmodeView)(void); TGVideoMessageCaptureControllerAssets *_assets; + + bool _canSendSilently; + bool _canSchedule; + bool _reminder; + + UIImpactFeedbackGenerator *_generator; } @property (nonatomic, copy) bool(^isAlreadyLocked)(void); @@ -137,7 +146,7 @@ typedef enum @implementation TGVideoMessageCaptureController -- (instancetype)initWithContext:(id)context assets:(TGVideoMessageCaptureControllerAssets *)assets transitionInView:(UIView *(^)(void))transitionInView parentController:(TGViewController *)parentController controlsFrame:(CGRect)controlsFrame isAlreadyLocked:(bool (^)(void))isAlreadyLocked liveUploadInterface:(id)liveUploadInterface pallete:(TGModernConversationInputMicPallete *)pallete slowmodeTimestamp:(int32_t)slowmodeTimestamp slowmodeView:(UIView *(^)(void))slowmodeView +- (instancetype)initWithContext:(id)context assets:(TGVideoMessageCaptureControllerAssets *)assets transitionInView:(UIView *(^)(void))transitionInView parentController:(TGViewController *)parentController controlsFrame:(CGRect)controlsFrame isAlreadyLocked:(bool (^)(void))isAlreadyLocked liveUploadInterface:(id)liveUploadInterface pallete:(TGModernConversationInputMicPallete *)pallete slowmodeTimestamp:(int32_t)slowmodeTimestamp slowmodeView:(UIView *(^)(void))slowmodeView canSendSilently:(bool)canSendSilently canSchedule:(bool)canSchedule reminder:(bool)reminder { self = [super initWithContext:context]; if (self != nil) @@ -148,6 +157,9 @@ typedef enum _liveUploadInterface = liveUploadInterface; _assets = assets; _pallete = pallete; + _canSendSilently = canSendSilently; + _canSchedule = canSchedule; + _reminder = reminder; _slowmodeTimestamp = slowmodeTimestamp; _slowmodeView = [slowmodeView copy]; @@ -189,6 +201,7 @@ typedef enum - (void)dealloc { + printf("Video controller dealloc\n"); [_thumbnailsDisposable dispose]; [[NSNotificationCenter defaultCenter] removeObserver:_didEnterBackgroundObserver]; [_activityDisposable dispose]; @@ -367,6 +380,13 @@ typedef enum return false; } }; + _controlsView.sendLongPressed = ^bool{ + __strong TGVideoMessageCaptureController *strongSelf = weakSelf; + if (strongSelf != nil) { + [strongSelf sendLongPressed]; + } + return true; + }; [self.view addSubview:_controlsView]; _separatorView = [[UIView alloc] initWithFrame:CGRectMake(controlsFrame.origin.x, controlsFrame.origin.y - TGScreenPixel, controlsFrame.size.width, TGScreenPixel)]; @@ -630,9 +650,11 @@ typedef enum return; [_activityDisposable dispose]; - [self stopRecording]; - - [self dismiss:false]; + [self stopRecording:^{ + TGDispatchOnMainThread(^{ + [self dismiss:false]; + }); + }]; } - (void)buttonInteractionUpdate:(CGPoint)value @@ -665,7 +687,7 @@ typedef enum _switchButton.userInteractionEnabled = false; [_activityDisposable dispose]; - [self stopRecording]; + [self stopRecording:^{}]; return true; } @@ -681,13 +703,72 @@ typedef enum } } - [self finishWithURL:_url dimensions:CGSizeMake(240.0f, 240.0f) duration:_duration liveUploadData:_liveUploadData thumbnailImage:_thumbnailImage]; + [self finishWithURL:_url dimensions:CGSizeMake(240.0f, 240.0f) duration:_duration liveUploadData:_liveUploadData thumbnailImage:_thumbnailImage isSilent:false scheduleTimestamp:0]; _automaticDismiss = true; [self dismiss:false]; return true; } +- (void)sendLongPressed { + if (iosMajorVersion() >= 10) { + if (_generator == nil) { + _generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium]; + } + [_generator impactOccurred]; + } + + bool effectiveHasSchedule = true; + + TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:_context isDark:self.pallete.isDark sendButtonFrame:[_controlsView convertRect:[_controlsView frameForSendButton] toView:nil] canSendSilently:_canSendSilently canSchedule:_canSchedule reminder:_reminder]; + __weak TGVideoMessageCaptureController *weakSelf = self; + controller.send = ^{ + __strong TGVideoMessageCaptureController *strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + + [strongSelf finishWithURL:strongSelf->_url dimensions:CGSizeMake(240.0f, 240.0f) duration:strongSelf->_duration liveUploadData:strongSelf->_liveUploadData thumbnailImage:strongSelf->_thumbnailImage isSilent:false scheduleTimestamp:0]; + + _automaticDismiss = true; + [strongSelf dismiss:false]; + }; + controller.sendSilently = ^{ + __strong TGVideoMessageCaptureController *strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + + [strongSelf finishWithURL:strongSelf->_url dimensions:CGSizeMake(240.0f, 240.0f) duration:strongSelf->_duration liveUploadData:strongSelf->_liveUploadData thumbnailImage:strongSelf->_thumbnailImage isSilent:true scheduleTimestamp:0]; + + _automaticDismiss = true; + [strongSelf dismiss:false]; + }; + controller.schedule = ^{ + __strong TGVideoMessageCaptureController *strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + + if (strongSelf.presentScheduleController) { + strongSelf.presentScheduleController(^(int32_t time) { + __strong TGVideoMessageCaptureController *strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + + [strongSelf finishWithURL:strongSelf->_url dimensions:CGSizeMake(240.0f, 240.0f) duration:strongSelf->_duration liveUploadData:strongSelf->_liveUploadData thumbnailImage:strongSelf->_thumbnailImage isSilent:false scheduleTimestamp:time]; + + _automaticDismiss = true; + [strongSelf dismiss:false]; + }); + } + }; + + TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:[_context makeOverlayWindowManager] parentController:self contentController:controller]; + controllerWindow.hidden = false; +} + - (void)unmutePressed { [self _updateMuted:false]; @@ -861,13 +942,13 @@ typedef enum [self startRecordingTimer]; } -- (void)stopRecording +- (void)stopRecording:(void (^)())completed { - [_capturePipeline stopRecording]; + [_capturePipeline stopRecording:completed]; [_buttonHandler ignoreEventsFor:1.0f andDisable:true]; } -- (void)finishWithURL:(NSURL *)url dimensions:(CGSize)dimensions duration:(NSTimeInterval)duration liveUploadData:(id )liveUploadData thumbnailImage:(UIImage *)thumbnailImage +- (void)finishWithURL:(NSURL *)url dimensions:(CGSize)dimensions duration:(NSTimeInterval)duration liveUploadData:(id )liveUploadData thumbnailImage:(UIImage *)thumbnailImage isSilent:(bool)isSilent scheduleTimestamp:(int32_t)scheduleTimestamp { if (duration < 1.0) _dismissed = true; @@ -923,7 +1004,7 @@ typedef enum } if (!_dismissed && self.finishedWithVideo != nil) - self.finishedWithVideo(url, image, fileSize, duration, dimensions, liveUploadData, adjustments); + self.finishedWithVideo(url, image, fileSize, duration, dimensions, liveUploadData, adjustments, isSilent, scheduleTimestamp); else [[NSFileManager defaultManager] removeItemAtURL:url error:NULL]; } @@ -1125,7 +1206,7 @@ typedef enum } else { - [self finishWithURL:_url dimensions:CGSizeMake(240.0f, 240.0f) duration:duration liveUploadData:liveUploadData thumbnailImage:thumbnailImage]; + [self finishWithURL:_url dimensions:CGSizeMake(240.0f, 240.0f) duration:duration liveUploadData:liveUploadData thumbnailImage:thumbnailImage isSilent:false scheduleTimestamp:0]; } } diff --git a/submodules/LegacyComponents/LegacyComponents/TGVideoMessageControls.h b/submodules/LegacyComponents/LegacyComponents/TGVideoMessageControls.h index ad4d12e676..92fa17e97f 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGVideoMessageControls.h +++ b/submodules/LegacyComponents/LegacyComponents/TGVideoMessageControls.h @@ -13,6 +13,7 @@ @property (nonatomic, copy) void (^cancel)(void); @property (nonatomic, copy) void (^deletePressed)(void); @property (nonatomic, copy) bool (^sendPressed)(void); +@property (nonatomic, copy) bool (^sendLongPressed)(void); @property (nonatomic, copy) bool(^isAlreadyLocked)(void); diff --git a/submodules/LegacyComponents/LegacyComponents/TGVideoMessageControls.m b/submodules/LegacyComponents/LegacyComponents/TGVideoMessageControls.m index 0479e0fcab..4062339754 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGVideoMessageControls.m +++ b/submodules/LegacyComponents/LegacyComponents/TGVideoMessageControls.m @@ -3,7 +3,6 @@ #import #import -//#import "TGModernConversationInputMicButton.h" #import #import @@ -41,6 +40,7 @@ static CGRect viewFrame(UIView *view) TGModernButton *_deleteButton; TGModernButton *_sendButton; + UILongPressGestureRecognizer *_longPressGestureRecognizer; int32_t _slowmodeTimestamp; UIView * (^_generateSlowmodeView)(void); @@ -373,6 +373,11 @@ static CGRect viewFrame(UIView *view) [_sendButton setImage:_assets.sendImage forState:UIControlStateNormal]; _sendButton.adjustsImageWhenHighlighted = false; [_sendButton addTarget:self action:@selector(sendButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + + _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(doneButtonLongPressed:)]; + _longPressGestureRecognizer.minimumPressDuration = 0.4; + [_sendButton addGestureRecognizer:_longPressGestureRecognizer]; + [self addSubview:_sendButton]; if (_slowmodeTimestamp != 0) { @@ -487,7 +492,16 @@ static CGRect viewFrame(UIView *view) _sendButton.userInteractionEnabled = true; } } - +} + +- (void)doneButtonLongPressed:(UILongPressGestureRecognizer *)gestureRecognizer +{ + if (gestureRecognizer.state == UIGestureRecognizerStateBegan) + { + if (self.sendLongPressed != nil) { + self.sendLongPressed(); + } + } } - (void)cancelPressed diff --git a/submodules/LegacyDataImport/Sources/LegacyChatImport.swift b/submodules/LegacyDataImport/Sources/LegacyChatImport.swift index 595b920e9b..aa9a853019 100644 --- a/submodules/LegacyDataImport/Sources/LegacyChatImport.swift +++ b/submodules/LegacyDataImport/Sources/LegacyChatImport.swift @@ -418,7 +418,7 @@ private func loadLegacyMessages(account: TemporaryAccount, basePath: String, acc } } - parsedMedia.append(TelegramMediaImage(imageId: mediaId, representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil)) + parsedMedia.append(TelegramMediaImage(imageId: mediaId, representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])) } else if let item = item as? TGVideoMediaAttachment { let mediaId = MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()) var representations: [TelegramMediaImageRepresentation] = [] diff --git a/submodules/LegacyDataImport/Sources/LegacyPreferencesImport.swift b/submodules/LegacyDataImport/Sources/LegacyPreferencesImport.swift index 021ddb6c76..485ccc22cc 100644 --- a/submodules/LegacyDataImport/Sources/LegacyPreferencesImport.swift +++ b/submodules/LegacyDataImport/Sources/LegacyPreferencesImport.swift @@ -178,16 +178,16 @@ func importLegacyPreferences(accountManager: AccountManager, account: TemporaryA //themeSpecificAccentColors: current.themeSpecificAccentColors //settings.themeAccentColor = presentationState.userInfo } - settings.chatWallpaper = .color(0xffffff) + settings.themeSpecificChatWallpapers[settings.theme.index] = .color(0xffffff) case 2: settings.theme = .builtin(.night) - settings.chatWallpaper = .color(0x00000) + settings.themeSpecificChatWallpapers[settings.theme.index] = .color(0x000000) case 3: settings.theme = .builtin(.nightAccent) - settings.chatWallpaper = .color(0x18222D) + settings.themeSpecificChatWallpapers[settings.theme.index] = .color(0x18222d) default: settings.theme = .builtin(.dayClassic) - settings.chatWallpaper = .builtin(WallpaperSettings()) + settings.themeSpecificChatWallpapers[settings.theme.index] = .builtin(WallpaperSettings()) } let fontSizeMap: [Int32: PresentationFontSize] = [ 14: .extraSmall, diff --git a/submodules/LegacyMediaPickerUI/BUCK b/submodules/LegacyMediaPickerUI/BUCK index a740c095be..85dce0dcfa 100644 --- a/submodules/LegacyMediaPickerUI/BUCK +++ b/submodules/LegacyMediaPickerUI/BUCK @@ -26,6 +26,7 @@ static_library( "//submodules/MimeTypes:MimeTypes", "//submodules/LocalMediaResources:LocalMediaResources", "//submodules/SearchPeerMembers:SearchPeerMembers", + "//submodules/SaveToCameraRoll:SaveToCameraRoll", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift index b4056dd51d..f55097b4a2 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift @@ -10,18 +10,62 @@ import TelegramPresentationData import DeviceAccess import AccountContext import LegacyUI +import SaveToCameraRoll -public struct LegacyAttachmentMenuMediaEditing: OptionSet { - public var rawValue: Int32 +public func defaultVideoPresetForContext(_ context: AccountContext) -> TGMediaVideoConversionPreset { + var networkType: NetworkType = .wifi + let _ = (context.account.networkType + |> take(1)).start(next: { value in + networkType = value + }) - public init(rawValue: Int32) { - self.rawValue = rawValue + let autodownloadSettings = context.sharedContext.currentAutodownloadSettings.with { $0 } + let presetSettings: AutodownloadPresetSettings + switch networkType { + case .wifi: + presetSettings = autodownloadSettings.highPreset + default: + presetSettings = autodownloadSettings.mediumPreset } - public static let imageOrVideo = LegacyAttachmentMenuMediaEditing(rawValue: 1 << 0) + let effectiveValue: Int + if presetSettings.videoUploadMaxbitrate == 0 { + effectiveValue = 0 + } else { + effectiveValue = Int(presetSettings.videoUploadMaxbitrate) * 5 / 100 + } + + switch effectiveValue { + case 0: + return TGMediaVideoConversionPresetCompressedMedium + case 1: + return TGMediaVideoConversionPresetCompressedVeryLow + case 2: + return TGMediaVideoConversionPresetCompressedLow + case 3: + return TGMediaVideoConversionPresetCompressedMedium + case 4: + return TGMediaVideoConversionPresetCompressedHigh + case 5: + return TGMediaVideoConversionPresetCompressedVeryHigh + default: + return TGMediaVideoConversionPresetCompressedMedium + } } -public func legacyAttachmentMenu(context: AccountContext, peer: Peer, editMediaOptions: LegacyAttachmentMenuMediaEditing?, saveEditedPhotos: Bool, allowGrouping: Bool, hasSchedule: Bool, canSendPolls: Bool, presentationData: PresentationData, parentController: LegacyController, recentlyUsedInlineBots: [Peer], initialCaption: String, openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openWebSearch: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, openPoll: @escaping () -> Void, presentSelectionLimitExceeded: @escaping () -> Void, presentCantSendMultipleFiles: @escaping () -> Void, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void) -> TGMenuSheetController { +public enum LegacyAttachmentMenuMediaEditing { + case none + case imageOrVideo(AnyMediaReference?) + case file +} + +public func legacyAttachmentMenu(context: AccountContext, peer: Peer, editMediaOptions: LegacyAttachmentMenuMediaEditing?, saveEditedPhotos: Bool, allowGrouping: Bool, hasSchedule: Bool, canSendPolls: Bool, presentationData: PresentationData, parentController: LegacyController, recentlyUsedInlineBots: [Peer], initialCaption: String, openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openWebSearch: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, openPoll: @escaping () -> Void, presentSelectionLimitExceeded: @escaping () -> Void, presentCantSendMultipleFiles: @escaping () -> Void, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, present: @escaping (ViewController, Any?) -> Void) -> TGMenuSheetController { + let defaultVideoPreset = defaultVideoPresetForContext(context) + UserDefaults.standard.set(defaultVideoPreset.rawValue as NSNumber, forKey: "TG_preferredVideoPreset_v0") + + let actionSheetTheme = ActionSheetControllerTheme(presentationData: presentationData) + let fontSize = floor(actionSheetTheme.baseFontSize * 20.0 / 17.0) + let isSecretChat = peer.id.namespace == Namespaces.Peer.SecretChat let controller = TGMenuSheetController(context: parentController.context, dark: false)! @@ -34,11 +78,19 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, editMediaO var editing = false var canSendImageOrVideo = false - var canEditCurrent = false - if let editMediaOptions = editMediaOptions, editMediaOptions.contains(.imageOrVideo) { + var canEditFile = false + var editCurrentMedia: AnyMediaReference? + if let editMediaOptions = editMediaOptions { + switch editMediaOptions { + case .none: + break + case let .imageOrVideo(anyReference): + editCurrentMedia = anyReference + case .file: + canEditFile = true + } canSendImageOrVideo = true editing = true - canEditCurrent = true } else { canSendImageOrVideo = true } @@ -59,8 +111,12 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, editMediaO carouselItemView = carouselItem carouselItem.suggestionContext = legacySuggestionContext(context: context, peerId: peer.id) carouselItem.recipientName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - carouselItem.cameraPressed = { [weak controller] cameraView in + carouselItem.cameraPressed = { [weak controller, weak parentController] cameraView in if let controller = controller { + if let parentController = parentController, parentController.context.currentlyInSplitView() { + return + } + DeviceAccess.authorizeAccess(to: .camera, presentationData: context.sharedContext.currentPresentationData.with { $0 }, present: context.sharedContext.presentGlobalController, openSettings: context.sharedContext.applicationBindings.openSettings, { value in if value { openCamera(cameraView, controller) @@ -78,6 +134,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, editMediaO carouselItem.hasSilentPosting = !isSecretChat } carouselItem.hasSchedule = hasSchedule + carouselItem.reminder = peer.id == context.account.peerId carouselItem.presentScheduleController = { done in presentSchedulePicker { time in done?(time) @@ -99,7 +156,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, editMediaO carouselItem.editingContext.setInitialCaption(initialCaption, entities: []) itemViews.append(carouselItem) - let galleryItem = TGMenuSheetButtonItemView(title: editing ? presentationData.strings.Conversation_EditingMessageMediaChange : presentationData.strings.AttachmentMenu_PhotoOrVideo, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in + let galleryItem = TGMenuSheetButtonItemView(title: editing ? presentationData.strings.Conversation_EditingMessageMediaChange : presentationData.strings.AttachmentMenu_PhotoOrVideo, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openGallery() })! @@ -117,7 +174,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, editMediaO } if !editing { - let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, action: {[weak controller] in + let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openFileGallery() })! @@ -125,30 +182,135 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, editMediaO underlyingViews.append(fileItem) } - if canEditCurrent { - let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, action: {[weak controller] in + if canEditFile { + let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openFileGallery() })! itemViews.append(fileItem) } + if let editCurrentMedia = editCurrentMedia { + let title: String + if editCurrentMedia.media is TelegramMediaImage { + title = presentationData.strings.Conversation_EditingMessageMediaEditCurrentPhoto + } else { + title = presentationData.strings.Conversation_EditingMessageMediaEditCurrentVideo + } + let editCurrentItem = TGMenuSheetButtonItemView(title: title, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in + controller?.dismiss(animated: true) + + let _ = (fetchMediaData(context: context, postbox: context.account.postbox, mediaReference: editCurrentMedia) + |> deliverOnMainQueue).start(next: { (value, isImage) in + guard case let .data(data) = value, data.complete else { + return + } + + let item: TGMediaEditableItem & TGMediaSelectableItem + if let image = UIImage(contentsOfFile: data.path) { + item = TGCameraCapturedPhoto(existing: image) + } else { + item = TGCameraCapturedVideo(url: URL(fileURLWithPath: data.path)) + } + + let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil) + legacyController.statusBar.statusBarStyle = .Ignore + legacyController.controllerLoaded = { [weak legacyController] in + legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true + } + + let emptyController = LegacyEmptyController(context: legacyController.context)! + emptyController.navigationBarShouldBeHidden = true + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + legacyController.bind(controller: navigationController) + + var hasTimer = false + var hasSilentPosting = false + if peer.id != context.account.peerId { + if peer is TelegramUser { + hasTimer = true + } + hasSilentPosting = true + } + let recipientName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + + legacyController.enableSizeClassSignal = true + + let presentationDisposable = context.sharedContext.presentationData.start(next: { [weak legacyController] presentationData in + if let legacyController = legacyController, let controller = legacyController.legacyController as? TGMenuSheetController { + controller.pallete = legacyMenuPaletteFromTheme(presentationData.theme) + } + }) + legacyController.disposables.add(presentationDisposable) + + present(legacyController, nil) + + TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: "", entities: [], withItem: item, recipientName: recipientName, completion: { result, editingContext in + let intent: TGMediaAssetsControllerIntent = TGMediaAssetsControllerSendMediaIntent + let signals = TGCameraController.resultSignals(for: nil, editingContext: editingContext, currentItem: result as! TGMediaSelectableItem, storeAssets: false, saveEditedPhotos: false, descriptionGenerator: legacyAssetPickerItemGenerator()) + sendMessagesWithSignals(signals, false, 0) + /* + [TGCameraController resultSignalsForSelectionContext:nil editingContext:editingContext currentItem:result storeAssets:false saveEditedPhotos:false descriptionGenerator:^id(id result, NSString *caption, NSArray *entities, NSString *hash) + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return nil; + + NSDictionary *desc = [strongSelf _descriptionForItem:result caption:caption entities:entities hash:hash allowRemoteCache:allowRemoteCache]; + return [strongSelf _descriptionForReplacingMedia:desc message:message]; + }]] + */ + //let signals = TGMediaAssetsController.resultSignals(for: nil, editingContext: editingContext, intent: intent, currentItem: result, storeAssets: true, useMediaCache: false, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: saveEditedPhotos) + //sendMessagesWithSignals(signals, silentPosting, scheduleTime) + }, dismissed: { [weak legacyController] in + legacyController?.dismiss() + }) + }) + /* + + + bool allowRemoteCache = [strongSelf->_companion controllerShouldCacheServerAssets]; + [TGPhotoVideoEditor presentWithContext:[TGLegacyComponentsContext shared] controller:strongSelf caption:text entities:entities withItem:item recipientName:[strongSelf->_companion title] completion:^(id result, TGMediaEditingContext *editingContext) + { + [strongSelf _asyncProcessMediaAssetSignals:[TGCameraController resultSignalsForSelectionContext:nil editingContext:editingContext currentItem:result storeAssets:false saveEditedPhotos:false descriptionGenerator:^id(id result, NSString *caption, NSArray *entities, NSString *hash) + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return nil; + + NSDictionary *desc = [strongSelf _descriptionForItem:result caption:caption entities:entities hash:hash allowRemoteCache:allowRemoteCache]; + return [strongSelf _descriptionForReplacingMedia:desc message:message]; + }]]; + [strongSelf endMessageEditing:true]; + }]; + */ + })! + itemViews.append(editCurrentItem) + } + if editMediaOptions == nil { - let locationItem = TGMenuSheetButtonItemView(title: presentationData.strings.Conversation_Location, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in + let locationItem = TGMenuSheetButtonItemView(title: presentationData.strings.Conversation_Location, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openMap() })! itemViews.append(locationItem) - if (peer is TelegramGroup || peer is TelegramChannel) && canSendMessagesToPeer(peer) && canSendPolls { - let pollItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_Poll, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in + var peerSupportsPolls = false + if peer is TelegramGroup || peer is TelegramChannel { + peerSupportsPolls = true + } else if let user = peer as? TelegramUser, let _ = user.botInfo { + peerSupportsPolls = true + } + if peerSupportsPolls && canSendMessagesToPeer(peer) && canSendPolls { + let pollItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_Poll, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openPoll() })! itemViews.append(pollItem) } - let contactItem = TGMenuSheetButtonItemView(title: presentationData.strings.Conversation_Contact, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in + let contactItem = TGMenuSheetButtonItemView(title: presentationData.strings.Conversation_Contact, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openContacts() })! @@ -162,7 +324,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, editMediaO let peer = recentlyUsedInlineBots[i] let addressName = peer.addressName if let addressName = addressName { - let botItem = TGMenuSheetButtonItemView(title: "@" + addressName, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in + let botItem = TGMenuSheetButtonItemView(title: "@" + addressName, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) selectRecentlyUsedInlineBot(peer) @@ -175,7 +337,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, editMediaO carouselItemView?.remainingHeight = TGMenuSheetButtonItemViewHeight * CGFloat(itemViews.count - 1) - let cancelItem = TGMenuSheetButtonItemView(title: presentationData.strings.Common_Cancel, type: TGMenuSheetButtonTypeCancel, action: { [weak controller] in + let cancelItem = TGMenuSheetButtonItemView(title: presentationData.strings.Common_Cancel, type: TGMenuSheetButtonTypeCancel, fontSize: actionSheetTheme.baseFontSize, action: { [weak controller] in controller?.dismiss(animated: true) })! itemViews.append(cancelItem) @@ -191,6 +353,9 @@ public func legacyMenuPaletteFromTheme(_ theme: PresentationTheme) -> TGMenuShee } public func presentLegacyPasteMenu(context: AccountContext, peer: Peer, saveEditedPhotos: Bool, allowGrouping: Bool, presentationData: PresentationData, images: [UIImage], sendMessagesWithSignals: @escaping ([Any]?) -> Void, present: (ViewController, Any?) -> Void, initialLayout: ContainerViewLayout? = nil) -> ViewController { + let defaultVideoPreset = defaultVideoPresetForContext(context) + UserDefaults.standard.set(defaultVideoPreset.rawValue as NSNumber, forKey: "TG_preferredVideoPreset_v0") + let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: initialLayout) legacyController.statusBar.statusBarStyle = .Ignore legacyController.controllerLoaded = { [weak legacyController] in diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index 8b52458385..24be4f12b1 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -13,6 +13,7 @@ import AccountContext import ImageCompression import MimeTypes import LocalMediaResources +import LegacyUI public func guessMimeTypeByFileExtension(_ ext: String) -> String { return TGMimeTypeMap.mimeType(forExtension: ext) ?? "application/binary" @@ -31,6 +32,7 @@ public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, co controller.hasSilentPosting = !isSecretChat } controller.hasSchedule = hasSchedule + controller.reminder = peer.id == context.account.peerId controller.presentScheduleController = { done in presentSchedulePicker { time in done?(time) @@ -54,6 +56,8 @@ public func legacyAssetPicker(context: AccountContext, presentationData: Present return Signal { subscriber in let intent = fileMode ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent + let defaultVideoPreset = defaultVideoPresetForContext(context) + UserDefaults.standard.set(defaultVideoPreset.rawValue as NSNumber, forKey: "TG_preferredVideoPreset_v0") DeviceAccess.authorizeAccess(to: .mediaLibrary(.send), presentationData: presentationData, present: context.sharedContext.presentGlobalController, openSettings: context.sharedContext.applicationBindings.openSettings, { value in if !value { @@ -131,13 +135,15 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, String?, [Any]?, String? } -// let stickers = (dict["stickers"] as? [TGDocumentMediaAttachment]).map { document -> FileMediaReference in -// if let sticker = stickerFromLegacyDocument(document) { -// return FileMediaReference.standalone(media: sticker) -// } -// } + let stickers = (dict["stickers"] as? [TGDocumentMediaAttachment])?.compactMap { document -> FileMediaReference? in + if let sticker = stickerFromLegacyDocument(document) { + return FileMediaReference.standalone(media: sticker) + } else { + return nil + } + } ?? [] var result: [AnyHashable : Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .image(image), thumbnail: thumbnail, caption: caption, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) + result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .image(image), thumbnail: thumbnail, caption: caption, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) return result } else if (dict["type"] as! NSString) == "cloudPhoto" { let asset = dict["asset"] as! TGMediaAsset @@ -213,7 +219,7 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, String?, [Any]?, String? asFile = true } - let url: String? = (dict["url"] as? String) ?? (dict["url"] as? URL)?.absoluteString + let url: String? = (dict["url"] as? String) ?? (dict["url"] as? URL)?.path if let url = url, let previewImage = dict["previewImage"] as? UIImage { let dimensions = previewImage.pixelSize() @@ -317,9 +323,24 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) - let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId) representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledSize), resource: resource)) - - let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil) + + var imageFlags: TelegramMediaImageFlags = [] + + var stickerFiles: [TelegramMediaFile] = [] + if !stickers.isEmpty { + for fileReference in stickers { + stickerFiles.append(fileReference.media) + } + } + var attributes: [MessageAttribute] = [] + + if !stickerFiles.isEmpty { + attributes.append(EmbeddedMediaStickersMessageAttribute(files: stickerFiles)) + imageFlags.insert(.hasStickers) + } + + let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: imageFlags) if let timer = item.timer, timer > 0 && timer <= 60 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) } @@ -336,7 +357,7 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) - let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier, uniqueId: arc4random64()) representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledSize), resource: resource)) - let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil) + let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) var attributes: [MessageAttribute] = [] if let timer = item.timer, timer > 0 && timer <= 60 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) diff --git a/submodules/LegacyUI/Sources/LegacyComponentsStickers.swift b/submodules/LegacyUI/Sources/LegacyComponentsStickers.swift index fd20b96cfe..15adb76b5f 100644 --- a/submodules/LegacyUI/Sources/LegacyComponentsStickers.swift +++ b/submodules/LegacyUI/Sources/LegacyComponentsStickers.swift @@ -8,7 +8,7 @@ import SwiftSignalKit import Display import StickerResources -func stickerFromLegacyDocument(_ documentAttachment: TGDocumentMediaAttachment) -> TelegramMediaFile? { +public func stickerFromLegacyDocument(_ documentAttachment: TGDocumentMediaAttachment) -> TelegramMediaFile? { if documentAttachment.isSticker() { for case let sticker as TGDocumentAttributeSticker in documentAttachment.attributes { var attributes: [TelegramMediaFileAttribute] = [] diff --git a/submodules/LegacyUI/Sources/LegacyController.swift b/submodules/LegacyUI/Sources/LegacyController.swift index c2c41c8c21..df58f2cbe3 100644 --- a/submodules/LegacyUI/Sources/LegacyController.swift +++ b/submodules/LegacyUI/Sources/LegacyController.swift @@ -173,6 +173,9 @@ public final class LegacyControllerContext: NSObject, LegacyComponentsContext { } public func currentlyInSplitView() -> Bool { + if let controller = self.controller as? LegacyController, let validLayout = controller.validLayout { + return validLayout.isNonExclusive + } return false } diff --git a/submodules/LegacyUI/Sources/TelegramInitializeLegacyComponents.swift b/submodules/LegacyUI/Sources/TelegramInitializeLegacyComponents.swift index 32df01e865..b77631410c 100644 --- a/submodules/LegacyUI/Sources/TelegramInitializeLegacyComponents.swift +++ b/submodules/LegacyUI/Sources/TelegramInitializeLegacyComponents.swift @@ -48,6 +48,13 @@ private final class LegacyComponentsAccessCheckerImpl: NSObject, LegacyComponent } public func checkPhotoAuthorizationStatus(for intent: TGPhotoAccessIntent, alertDismissCompletion: (() -> Void)!) -> Bool { + if let context = self.context { + DeviceAccess.authorizeAccess(to: .mediaLibrary(.send), presentationData: context.sharedContext.currentPresentationData.with { $0 }, present: context.sharedContext.presentGlobalController, openSettings: context.sharedContext.applicationBindings.openSettings, { value in + if !value { + alertDismissCompletion?() + } + }) + } return true } diff --git a/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift b/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift index 5d99fc9119..760f1bfddc 100644 --- a/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift +++ b/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift @@ -9,6 +9,7 @@ private let actionFont = Font.medium(13.0) public final class ListSectionHeaderNode: ASDisplayNode { private let label: ImmediateTextNode + private var actionButtonLabel: ImmediateTextNode? private var actionButton: HighlightableButtonNode? private var theme: PresentationTheme @@ -28,17 +29,26 @@ public final class ListSectionHeaderNode: ASDisplayNode { didSet { if (self.action != nil) != (self.actionButton != nil) { if let _ = self.action { + let actionButtonLabel = ImmediateTextNode() + self.addSubnode(actionButtonLabel) + self.actionButtonLabel = actionButtonLabel let actionButton = HighlightableButtonNode() self.addSubnode(actionButton) self.actionButton = actionButton actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) - } else if let actionButton = self.actionButton { - self.actionButton = nil - actionButton.removeFromSupernode() + } else { + if let actionButtonLabel = self.actionButtonLabel { + self.actionButtonLabel = nil + actionButtonLabel.removeFromSupernode() + } + if let actionButton = self.actionButton { + self.actionButton = nil + actionButton.removeFromSupernode() + } } } if let action = self.action { - self.actionButton?.setAttributedTitle(NSAttributedString(string: action, font: actionFont, textColor: self.theme.chatList.sectionHeaderTextColor), for: []) + self.actionButtonLabel?.attributedText = NSAttributedString(string: action, font: actionFont, textColor: self.theme.chatList.sectionHeaderTextColor) } if let (size, leftInset, rightInset) = self.validLayout { @@ -70,7 +80,7 @@ public final class ListSectionHeaderNode: ASDisplayNode { self.backgroundColor = theme.chatList.sectionHeaderFillColor if let action = self.action { - self.actionButton?.setAttributedTitle(NSAttributedString(string: action, font: actionFont, textColor: self.theme.chatList.sectionHeaderTextColor), for: []) + self.actionButtonLabel?.attributedText = NSAttributedString(string: action, font: actionFont, textColor: self.theme.chatList.sectionHeaderTextColor) } if let (size, leftInset, rightInset) = self.validLayout { @@ -84,8 +94,9 @@ public final class ListSectionHeaderNode: ASDisplayNode { let labelSize = self.label.updateLayout(CGSize(width: max(0.0, size.width - leftInset - rightInset - 18.0), height: size.height)) self.label.frame = CGRect(origin: CGPoint(x: leftInset + 16.0, y: 6.0 + UIScreenPixel), size: labelSize) - if let actionButton = self.actionButton { - let buttonSize = actionButton.measure(CGSize(width: size.width, height: size.height)) + if let actionButton = self.actionButton, let actionButtonLabel = self.actionButtonLabel { + let buttonSize = actionButtonLabel.updateLayout(CGSize(width: size.width, height: size.height)) + actionButtonLabel.frame = CGRect(origin: CGPoint(x: size.width - rightInset - 16.0 - buttonSize.width, y: 6.0 + UIScreenPixel), size: buttonSize) actionButton.frame = CGRect(origin: CGPoint(x: size.width - rightInset - 16.0 - buttonSize.width, y: 6.0 + UIScreenPixel), size: buttonSize) } } diff --git a/submodules/LiveLocationPositionNode/BUCK b/submodules/LiveLocationPositionNode/BUCK index c3a402be8b..f3bb9e3c8b 100644 --- a/submodules/LiveLocationPositionNode/BUCK +++ b/submodules/LiveLocationPositionNode/BUCK @@ -14,6 +14,8 @@ static_library( "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/AvatarNode:AvatarNode", "//submodules/AppBundle:AppBundle", + "//submodules/LocationResources:LocationResources", + "//submodules/AccountContext:AccountContext", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/LiveLocationPositionNode/Sources/ChatMessageLiveLocationPositionNode.swift b/submodules/LiveLocationPositionNode/Sources/ChatMessageLiveLocationPositionNode.swift index 616168e6b2..ce9b4ead41 100644 --- a/submodules/LiveLocationPositionNode/Sources/ChatMessageLiveLocationPositionNode.swift +++ b/submodules/LiveLocationPositionNode/Sources/ChatMessageLiveLocationPositionNode.swift @@ -7,7 +7,9 @@ import SyncCore import Postbox import TelegramPresentationData import AvatarNode +import LocationResources import AppBundle +import AccountContext private let avatarFont = avatarPlaceholderFont(size: 24.0) private let avatarBackgroundImage = UIImage(bundleImageName: "Chat/Message/LocationPin")?.precomposed() @@ -36,12 +38,31 @@ private func removePulseAnimations(layer: CALayer) { layer.removeAnimation(forKey: "pulse-opacity") } +private func chatBubbleMapPinImage(_ theme: PresentationTheme, color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 62.0, height: 74.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let shadowImage = UIImage(bundleImageName: "Chat/Message/LocationPinShadow"), let cgImage = shadowImage.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(), size: shadowImage.size)) + } + if let backgroundImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/LocationPinBackground"), color: color), let cgImage = backgroundImage.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(), size: backgroundImage.size)) + } + }) +} + public final class ChatMessageLiveLocationPositionNode: ASDisplayNode { + public enum Mode { + case liveLocation(Peer, Bool) + case location(TelegramMediaMap?) + } + private let backgroundNode: ASImageNode + private let iconNode: TransformImageNode private let avatarNode: AvatarNode private let pulseNode: ASImageNode private var pulseImage: UIImage? + private var venueType: String? override public init() { let isLayerBacked = !smartInvertColorsEnabled() @@ -51,6 +72,9 @@ public final class ChatMessageLiveLocationPositionNode: ASDisplayNode { self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displayWithoutProcessing = true + self.iconNode = TransformImageNode() + self.iconNode.isLayerBacked = true + self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode.isLayerBacked = isLayerBacked @@ -66,23 +90,32 @@ public final class ChatMessageLiveLocationPositionNode: ASDisplayNode { self.addSubnode(self.pulseNode) self.addSubnode(self.backgroundNode) + self.addSubnode(self.iconNode) self.addSubnode(self.avatarNode) } - public func asyncLayout() -> (_ account: Account, _ theme: PresentationTheme, _ peer: Peer?, _ liveActive: Bool?) -> (CGSize, () -> Void) { - let currentPulseImage = self.pulseImage + public func asyncLayout() -> (_ context: AccountContext, _ theme: PresentationTheme, _ mode: Mode) -> (CGSize, () -> Void) { + let iconLayout = self.iconNode.asyncLayout() - return { [weak self] account, theme, peer, liveActive in + let currentPulseImage = self.pulseImage + let currentVenueType = self.venueType + + return { [weak self] context, theme, mode in + var updatedVenueType: String? + let backgroundImage: UIImage? var hasPulse = false - if let _ = peer { - backgroundImage = avatarBackgroundImage - - if let liveActive = liveActive { - hasPulse = liveActive - } - } else { - backgroundImage = PresentationResourcesChat.chatBubbleMapPinImage(theme) + switch mode { + case let .liveLocation(_, active): + backgroundImage = avatarBackgroundImage + hasPulse = active + case let .location(location): + let venueType = location?.venue?.type ?? "" + let color = venueType.isEmpty ? theme.list.itemAccentColor : venueIconColor(type: venueType) + backgroundImage = chatBubbleMapPinImage(theme, color: color) + if currentVenueType != venueType { + updatedVenueType = venueType + } } let pulseImage: UIImage? @@ -99,19 +132,30 @@ public final class ChatMessageLiveLocationPositionNode: ASDisplayNode { } strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 62.0, height: 74.0)) strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 10.0, y: 9.0), size: CGSize(width: 42.0, height: 42.0)) - if let peer = peer { - strongSelf.avatarNode.setPeer(account: account, theme: theme, peer: peer) - strongSelf.avatarNode.isHidden = false - - if let liveActive = liveActive { - strongSelf.avatarNode.alpha = liveActive ? 1.0 : 0.6 - } else { - strongSelf.avatarNode.alpha = 1.0 - } - } else { - strongSelf.avatarNode.isHidden = true + switch mode { + case let .liveLocation(peer, active): + strongSelf.avatarNode.setPeer(context: context, theme: theme, peer: peer) + strongSelf.avatarNode.isHidden = false + strongSelf.iconNode.isHidden = true + strongSelf.avatarNode.alpha = active ? 1.0 : 0.6 + case let .location(location): + strongSelf.iconNode.isHidden = false + strongSelf.avatarNode.isHidden = true } + if let updatedVenueType = updatedVenueType { + strongSelf.venueType = updatedVenueType + strongSelf.iconNode.setSignal(venueIcon(postbox: context.account.postbox, type: updatedVenueType, background: false)) + } + + + let arguments = VenueIconArguments(defaultForegroundColor: theme.chat.inputPanel.actionControlForegroundColor) + let iconSize = CGSize(width: 44.0, height: 44.0) + let apply = iconLayout(TransformImageArguments(corners: ImageCorners(), imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), custom: arguments)) + apply() + + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 14.0), size: iconSize) + strongSelf.pulseImage = pulseImage strongSelf.pulseNode.image = pulseImage strongSelf.pulseNode.frame = CGRect(origin: CGPoint(x: floor((62.0 - 60.0) / 2.0), y: 34.0), size: CGSize(width: 60.0, height: 60.0)) diff --git a/submodules/LocalMediaResources/BUCK b/submodules/LocalMediaResources/BUCK index 6739a9b6cf..12b32376f0 100644 --- a/submodules/LocalMediaResources/BUCK +++ b/submodules/LocalMediaResources/BUCK @@ -15,6 +15,8 @@ static_library( frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", "$SDKROOT/System/Library/Frameworks/UIKit.framework", - "$SDKROOT/System/Library/Frameworks/Photos.framework", + ], + weak_frameworks = [ + "Photos", ], ) diff --git a/submodules/LocationResources/BUCK b/submodules/LocationResources/BUCK new file mode 100644 index 0000000000..be15ac9649 --- /dev/null +++ b/submodules/LocationResources/BUCK @@ -0,0 +1,21 @@ +load("//Config:buck_rule_macros.bzl", "static_library") + +static_library( + name = "LocationResources", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/TelegramCore:TelegramCore#shared", + "//submodules/SyncCore:SyncCore#shared", + "//submodules/Postbox:Postbox#shared", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared", + "//submodules/Display:Display#shared", + "//submodules/AppBundle:AppBundle", + ], + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/UIKit.framework", + "$SDKROOT/System/Library/Frameworks/MapKit.framework", + ], +) diff --git a/submodules/LocationResources/Info.plist b/submodules/LocationResources/Info.plist new file mode 100644 index 0000000000..e1fe4cfb7b --- /dev/null +++ b/submodules/LocationResources/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/submodules/MediaResources/Sources/MapResources.swift b/submodules/LocationResources/Sources/MapResources.swift similarity index 57% rename from submodules/MediaResources/Sources/MapResources.swift rename to submodules/LocationResources/Sources/MapResources.swift index d7ef9ea20a..b79c127d71 100644 --- a/submodules/MediaResources/Sources/MapResources.swift +++ b/submodules/LocationResources/Sources/MapResources.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import Display import Postbox import TelegramCore import SyncCore @@ -138,3 +139,75 @@ public func fetchMapSnapshotResource(resource: MapSnapshotMediaResource) -> Sign } } +public func chatMapSnapshotData(account: Account, resource: MapSnapshotMediaResource) -> Signal { + return Signal { subscriber in + let dataDisposable = account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: MapSnapshotMediaResourceRepresentation(), complete: true).start(next: { next in + if next.size != 0 { + subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) + } + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + dataDisposable.dispose() + } + } +} + +public func chatMapSnapshotImage(account: Account, resource: MapSnapshotMediaResource) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatMapSnapshotData(account: account, resource: resource) + + return signal |> map { fullSizeData in + return { arguments in + let context = DrawingContext(size: arguments.drawingSize, clear: true) + + var fullSizeImage: CGImage? + var imageOrientation: UIImage.Orientation = .up + if let fullSizeData = fullSizeData { + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + } + + if let fullSizeImage = fullSizeImage { + let drawingRect = arguments.drawingRect + var fittedSize = CGSize(width: CGFloat(fullSizeImage.width), height: CGFloat(fullSizeImage.height)).aspectFilled(drawingRect.size) + if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.width = arguments.boundingSize.width + } + if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.height = arguments.boundingSize.height + } + + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + context.withFlippedContext { c in + c.setBlendMode(.copy) + if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { + c.fill(arguments.drawingRect) + } + + c.setBlendMode(.copy) + + c.interpolationQuality = .medium + c.draw(fullSizeImage, in: fittedRect) + + c.setBlendMode(.normal) + } + } else { + context.withFlippedContext { c in + c.setBlendMode(.copy) + c.setFillColor((arguments.emptyColor ?? UIColor.white).cgColor) + c.fill(arguments.drawingRect) + + c.setBlendMode(.normal) + } + } + } + + addCorners(context, arguments: arguments) + + return context + } + } +} diff --git a/submodules/LocationResources/Sources/MediaResources.h b/submodules/LocationResources/Sources/MediaResources.h new file mode 100644 index 0000000000..4b7c6c71d2 --- /dev/null +++ b/submodules/LocationResources/Sources/MediaResources.h @@ -0,0 +1,19 @@ +// +// MediaResources.h +// MediaResources +// +// Created by Peter on 8/2/19. +// Copyright © 2019 Telegram Messenger LLP. All rights reserved. +// + +#import + +//! Project version number for MediaResources. +FOUNDATION_EXPORT double MediaResourcesVersionNumber; + +//! Project version string for MediaResources. +FOUNDATION_EXPORT const unsigned char MediaResourcesVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/submodules/LocationResources/Sources/VenueIconResources.swift b/submodules/LocationResources/Sources/VenueIconResources.swift new file mode 100644 index 0000000000..4384acda86 --- /dev/null +++ b/submodules/LocationResources/Sources/VenueIconResources.swift @@ -0,0 +1,222 @@ +import Foundation +import UIKit +import Display +import Postbox +import TelegramCore +import SyncCore +import SwiftSignalKit +import AppBundle + +public struct VenueIconResourceId: MediaResourceId { + public let type: String + + public init(type: String) { + self.type = type + } + + public var uniqueId: String { + return "venue-icon-\(self.type.replacingOccurrences(of: "/", with: "_"))" + } + + public var hashValue: Int { + return self.type.hashValue + } + + public func isEqual(to: MediaResourceId) -> Bool { + if let to = to as? VenueIconResourceId { + return self.type == to.type + } else { + return false + } + } +} + +public class VenueIconResource: TelegramMediaResource { + public let type: String + + public init(type: String) { + self.type = type + } + + public required init(decoder: PostboxDecoder) { + self.type = decoder.decodeStringForKey("t", orElse: "") + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeString(self.type, forKey: "t") + } + + public var id: MediaResourceId { + return VenueIconResourceId(type: self.type) + } + + public func isEqual(to: MediaResource) -> Bool { + if let to = to as? VenueIconResource { + return self.type == to.type + } else { + return false + } + } +} + +public func fetchVenueIconResource(account: Account, resource: VenueIconResource) -> Signal { + return Signal { subscriber in + subscriber.putNext(.reset) + + let url = "https://ss3.4sqi.net/img/categories_v2/\(resource.type)_88.png" + + let fetchDisposable = MetaDisposable() + fetchDisposable.set(fetchHttpResource(url: url).start(next: { next in + subscriber.putNext(next) + }, completed: { + subscriber.putCompletion() + })) + + return ActionDisposable { + fetchDisposable.dispose() + } + } +} + +private func venueIconData(postbox: Postbox, resource: MediaResource) -> Signal { + let resourceData = postbox.mediaBox.resourceData(resource) + + let signal = resourceData + |> take(1) + |> mapToSignal { maybeData -> Signal in + if maybeData.complete { + let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) + return .single((loadedData)) + } else { + let fetched = postbox.mediaBox.fetchedResource(resource, parameters: nil) + let data = Signal { subscriber in + let fetchedDisposable = fetched.start() + let resourceDisposable = resourceData.start(next: { next in + subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedDisposable.dispose() + resourceDisposable.dispose() + } + } + + return data + } + } |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs == nil && rhs == nil { + return true + } else { + return false + } + }) + + return signal +} + +private let randomColors = [UIColor(rgb: 0xe56cd5), UIColor(rgb: 0xf89440), UIColor(rgb: 0x9986ff), UIColor(rgb: 0x44b3f5), UIColor(rgb: 0x6dc139), UIColor(rgb: 0xff5d5a), UIColor(rgb: 0xf87aad), UIColor(rgb: 0x6e82b3), UIColor(rgb: 0xf5ba21)] + +private let venueColors: [String: UIColor] = [ + "building/medical": UIColor(rgb: 0x43b3f4), + "building/gym": UIColor(rgb: 0x43b3f4), + "arts_entertainment": UIColor(rgb: 0xe56dd6), + "travel/bedandbreakfast": UIColor(rgb: 0x9987ff), + "travel/hotel": UIColor(rgb: 0x9987ff), + "travel/hostel": UIColor(rgb: 0x9987ff), + "travel/resort": UIColor(rgb: 0x9987ff), + "building": UIColor(rgb: 0x6e81b2), + "education": UIColor(rgb: 0xa57348), + "event": UIColor(rgb: 0x959595), + "food": UIColor(rgb: 0xf7943f), + "education/cafeteria": UIColor(rgb: 0xf7943f), + "nightlife": UIColor(rgb: 0xe56dd6), + "travel/hotel_bar": UIColor(rgb: 0xe56dd6), + "parks_outdoors": UIColor(rgb: 0x6cc039), + "shops": UIColor(rgb: 0xffb300), + "travel": UIColor(rgb: 0x1c9fff), + "work": UIColor(rgb: 0xad7854), + "home": UIColor(rgb: 0x00aeef) +] + +public func venueIconColor(type: String) -> UIColor { + if type.isEmpty { + return UIColor(rgb: 0x008df2) + } + if let color = venueColors[type] { + return color + } + let generalType = type.components(separatedBy: "/").first ?? type + if let color = venueColors[generalType] { + return color + } + + let index = Int(abs(persistentHash32(type)) % Int32(randomColors.count)) + return randomColors[index] +} + +public struct VenueIconArguments: TransformImageCustomArguments { + let defaultForegroundColor: UIColor + + public init(defaultForegroundColor: UIColor) { + self.defaultForegroundColor = defaultForegroundColor + } + + public func serialized() -> NSArray { + let array = NSMutableArray() + array.add(self.defaultForegroundColor) + return array + } +} + +public func venueIcon(postbox: Postbox, type: String, background: Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let isBuiltinIcon = ["", "home", "work"].contains(type) + let data: Signal = isBuiltinIcon ? .single(nil) : venueIconData(postbox: postbox, resource: VenueIconResource(type: type)) + return data |> map { data in + return { arguments in + let context = DrawingContext(size: arguments.drawingSize, clear: true) + + var iconImage: UIImage? + if let data = data, let image = UIImage(data: data) { + iconImage = image + } + + let backgroundColor = venueIconColor(type: type) + let foregroundColor: UIColor + if type.isEmpty, let customArguments = arguments.custom as? VenueIconArguments { + foregroundColor = customArguments.defaultForegroundColor + } else { + foregroundColor = UIColor.white + } + + context.withFlippedContext { c in + if background { + c.setFillColor(backgroundColor.cgColor) + c.fillEllipse(in: CGRect(origin: CGPoint(), size: arguments.drawingRect.size)) + } + let boundsSize = CGSize(width: arguments.drawingRect.size.width - 4.0 * 2.0, height: arguments.drawingRect.size.height - 4.0 * 2.0) + if let image = iconImage, let cgImage = generateTintedImage(image: image, color: foregroundColor)?.cgImage { + let fittedSize = image.size.aspectFitted(boundsSize) + c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.width - fittedSize.width) / 2.0), y: floor((arguments.drawingRect.height - fittedSize.height) / 2.0)), size: fittedSize)) + } else if isBuiltinIcon { + let image: UIImage? + switch type { + case "": + image = UIImage(bundleImageName: "Chat/Message/LocationPinForeground") + case "home": + image = UIImage(bundleImageName: "Location/HomeIcon") + case "work": + image = UIImage(bundleImageName: "Location/WorkIcon") + default: + image = nil + } + if let image = image, let pinImage = generateTintedImage(image: image, color: foregroundColor), let cgImage = pinImage.cgImage { + let fittedSize = image.size.aspectFitted(boundsSize) + c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.width - fittedSize.width) / 2.0), y: floor((arguments.drawingRect.height - fittedSize.height) / 2.0)), size: fittedSize)) + } + } + } + + return context + } + } +} diff --git a/submodules/LocationUI/BUCK b/submodules/LocationUI/BUCK index 9745e8495f..9e3d6bbf1f 100644 --- a/submodules/LocationUI/BUCK +++ b/submodules/LocationUI/BUCK @@ -17,12 +17,29 @@ static_library( "//submodules/ShareController:ShareController", "//submodules/AccountContext:AccountContext", "//submodules/OpenInExternalAppUI:OpenInExternalAppUI", + "//submodules/ItemListUI:ItemListUI", "//submodules/LegacyUI:LegacyUI", "//submodules/AppBundle:AppBundle", + "//submodules/TelegramStringFormatting:TelegramStringFormatting", + "//submodules/LocationResources:LocationResources", + "//submodules/ListSectionHeaderNode:ListSectionHeaderNode", + "//submodules/SegmentedControlNode:SegmentedControlNode", + "//submodules/Geocoding:Geocoding", + "//submodules/ItemListVenueItem:ItemListVenueItem", + "//submodules/MergeLists:MergeLists", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/SearchBarNode:SearchBarNode", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/DeviceAccess:DeviceAccess", + "//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader", + "//submodules/PhoneNumberFormat:PhoneNumberFormat", + "//submodules/PersistentStringHash:PersistentStringHash", + "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", "$SDKROOT/System/Library/Frameworks/UIKit.framework", "$SDKROOT/System/Library/Frameworks/CoreLocation.framework", + "$SDKROOT/System/Library/Frameworks/MapKit.framework", ], ) diff --git a/submodules/LocationUI/Sources/CachedGeocodes.swift b/submodules/LocationUI/Sources/CachedGeocodes.swift new file mode 100644 index 0000000000..f21c09f237 --- /dev/null +++ b/submodules/LocationUI/Sources/CachedGeocodes.swift @@ -0,0 +1,73 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import SyncCore +import TelegramUIPreferences +import PersistentStringHash +import AccountContext +import Geocoding + +public final class CachedGeocode: PostboxCoding { + public let latitude: Double + public let longitude: Double + + public init(latitude: Double, longitude: Double) { + self.latitude = latitude + self.longitude = longitude + } + + public init(decoder: PostboxDecoder) { + self.latitude = decoder.decodeDoubleForKey("lat", orElse: 0.0) + self.longitude = decoder.decodeDoubleForKey("lon", orElse: 0.0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeDouble(self.latitude, forKey: "lat") + encoder.encodeDouble(self.longitude, forKey: "lon") + } +} + +private func cachedGeocode(postbox: Postbox, address: DeviceContactAddressData) -> Signal { + return postbox.transaction { transaction -> CachedGeocode? in + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: Int64(bitPattern: address.string.persistentHashValue)) + if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: ApplicationSpecificItemCacheCollectionId.cachedGeocodes, key: key)) as? CachedGeocode { + return entry + } else { + return nil + } + } +} + +private let collectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 10, highWaterItemCount: 20) + +private func updateCachedGeocode(postbox: Postbox, address: DeviceContactAddressData, latitude: Double, longitude: Double) -> Signal<(Double, Double), NoError> { + return postbox.transaction { transaction -> (Double, Double) in + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: Int64(bitPattern: address.string.persistentHashValue)) + let id = ItemCacheEntryId(collectionId: ApplicationSpecificItemCacheCollectionId.cachedGeocodes, key: key) + transaction.putItemCacheEntry(id: id, entry: CachedGeocode(latitude: latitude, longitude: longitude), collectionSpec: collectionSpec) + return (latitude, longitude) + } +} + +public func geocodeAddress(postbox: Postbox, address: DeviceContactAddressData) -> Signal<(Double, Double)?, NoError> { + return cachedGeocode(postbox: postbox, address: address) + |> mapToSignal { cached -> Signal<(Double, Double)?, NoError> in + if let cached = cached { + return .single((cached.latitude, cached.longitude)) + } else { + return geocodeLocation(dictionary: address.dictionary) + |> mapToSignal { coordinate in + if let (latitude, longitude) = coordinate { + return updateCachedGeocode(postbox: postbox, address: address, latitude: latitude, longitude: longitude) + |> map(Optional.init) + } else { + return .single(nil) + } + } + } + } +} diff --git a/submodules/LocationUI/Sources/LegacyLocationController.swift b/submodules/LocationUI/Sources/LegacyLocationController.swift index 704e863537..473b1f5708 100644 --- a/submodules/LocationUI/Sources/LegacyLocationController.swift +++ b/submodules/LocationUI/Sources/LegacyLocationController.swift @@ -11,6 +11,7 @@ import ShareController import LegacyUI import OpenInExternalAppUI import AppBundle +import LocationResources private func generateClearIcon(color: UIColor) -> UIImage? { return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color) @@ -142,6 +143,7 @@ public func legacyLocationController(message: Message?, mapMedia: TelegramMediaM legacyController.navigationPresentation = .modal let controller: TGLocationViewController + let venueColor = mapMedia.venue?.type.flatMap { venueIconColor(type: $0) } if let message = message { let legacyMessage = makeLegacyMessage(message) let legacyAuthor: AnyObject? = message.author.flatMap(makeLegacyPeer) @@ -199,9 +201,12 @@ public func legacyLocationController(message: Message?, mapMedia: TelegramMediaM controller.setLiveLocationsSignal(.single(freezeLocations)) } else { controller.setLiveLocationsSignal(updatedLocations) + if message.flags.contains(.Incoming) { + context.account.viewTracker.updateSeenLiveLocationForMessageIds(messageIds: Set([message.id])) + } } } else { - controller = TGLocationViewController(context: legacyController.context, message: legacyMessage, peer: legacyAuthor)! + controller = TGLocationViewController(context: legacyController.context, message: legacyMessage, peer: legacyAuthor, color: venueColor)! controller.receivingPeer = message.peers[message.id.peerId].flatMap(makeLegacyPeer) controller.setLiveLocationsSignal(updatedLocations) } @@ -218,7 +223,7 @@ public func legacyLocationController(message: Message?, mapMedia: TelegramMediaM let attachment = TGLocationMediaAttachment() attachment.latitude = mapMedia.latitude attachment.longitude = mapMedia.longitude - controller = TGLocationViewController(context: legacyController.context, locationAttachment: attachment, peer: nil) + controller = TGLocationViewController(context: legacyController.context, locationAttachment: attachment, peer: nil, color: venueColor) } controller.remainingTimeForMessage = { message in diff --git a/submodules/LocationUI/Sources/LocationActionListItem.swift b/submodules/LocationUI/Sources/LocationActionListItem.swift new file mode 100644 index 0000000000..1e7d8556e7 --- /dev/null +++ b/submodules/LocationUI/Sources/LocationActionListItem.swift @@ -0,0 +1,324 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Postbox +import Display +import SwiftSignalKit +import TelegramCore +import SyncCore +import TelegramPresentationData +import ItemListUI +import LocationResources +import AppBundle + +public enum LocationActionListItemIcon: Equatable { + case location + case liveLocation + case stopLiveLocation + case venue(TelegramMediaMap) + + public static func ==(lhs: LocationActionListItemIcon, rhs: LocationActionListItemIcon) -> Bool { + switch lhs { + case .location: + if case .location = rhs { + return true + } else { + return false + } + case .liveLocation: + if case .liveLocation = rhs { + return true + } else { + return false + } + case .stopLiveLocation: + if case .stopLiveLocation = rhs { + return true + } else { + return false + } + case let .venue(lhsVenue): + if case let .venue(rhsVenue) = rhs, lhsVenue.venue?.id == rhsVenue.venue?.id { + return true + } else { + return false + } + } + } +} + +private func generateLocationIcon(theme: PresentationTheme) -> UIImage { + return generateImage(CGSize(width: 40.0, height: 40.0)) { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.chat.inputPanel.actionControlFillColor.cgColor) + context.fillEllipse(in: 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) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Location/SendLocationIcon"), color: theme.chat.inputPanel.actionControlForegroundColor) { + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)) + } + }! +} + +private func generateLiveLocationIcon(theme: PresentationTheme) -> UIImage { + return generateImage(CGSize(width: 40.0, height: 40.0)) { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor(rgb: 0x6cc139).cgColor) + context.fillEllipse(in: 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) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Location/SendLiveLocationIcon"), color: .white) { + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)) + } + }! +} + +final class LocationActionListItem: ListViewItem { + let presentationData: ItemListPresentationData + let account: Account + let title: String + let subtitle: String + let icon: LocationActionListItemIcon + let action: () -> Void + let highlighted: (Bool) -> Void + + public init(presentationData: ItemListPresentationData, account: Account, title: String, subtitle: String, icon: LocationActionListItemIcon, action: @escaping () -> Void, highlighted: @escaping (Bool) -> Void = { _ in }) { + self.presentationData = presentationData + self.account = account + self.title = title + self.subtitle = subtitle + self.icon = icon + self.action = action + self.highlighted = highlighted + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = LocationActionListItemNode() + let makeLayout = node.asyncLayout() + let (nodeLayout, nodeApply) = makeLayout(self, params, nextItem is LocationActionListItem) + node.contentSize = nodeLayout.contentSize + node.insets = nodeLayout.insets + + completion(node, nodeApply) + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? LocationActionListItemNode { + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params, nextItem is LocationActionListItem) + Queue.mainQueue().async { + completion(nodeLayout, { info in + apply().1(info) + }) + } + } + } + } + } + + public var selectable: Bool { + return true + } + + public func selected(listView: ListView) { + listView.clearHighlightAnimated(false) + self.action() + } +} + +final class LocationActionListItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let separatorNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private var titleNode: TextNode? + private var subtitleNode: TextNode? + private let iconNode: ASImageNode + private let venueIconNode: TransformImageNode + + private var item: LocationActionListItem? + private var layoutParams: ListViewItemLayoutParams? + + required init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + + self.venueIconNode = TransformImageNode() + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.iconNode) + self.addSubnode(self.venueIconNode) + } + + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + if let item = self.item { + let makeLayout = self.asyncLayout() + let (nodeLayout, nodeApply) = makeLayout(item, params, nextItem is LocationActionListItem) + self.contentSize = nodeLayout.contentSize + self.insets = nodeLayout.insets + let _ = nodeApply() + } + } + + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + self.item?.highlighted(highlighted) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode) + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + func asyncLayout() -> (_ item: LocationActionListItem, _ params: ListViewItemLayoutParams, _ hasSeparator: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, (ListViewItemApply) -> Void)) { + let currentItem = self.item + + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) + let iconLayout = self.venueIconNode.asyncLayout() + + return { [weak self] item, params, hasSeparator in + let leftInset: CGFloat = 65.0 + params.leftInset + let rightInset: CGFloat = params.rightInset + let verticalInset: CGFloat = 8.0 + let iconSize: CGFloat = 40.0 + + let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize) + let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + + let titleAttributedString = NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let subtitleAttributedString = NSAttributedString(string: item.subtitle, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let titleSpacing: CGFloat = 1.0 + let bottomInset: CGFloat = hasSeparator ? 0.0 : 4.0 + let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + bottomInset) + let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets()) + + return (nodeLayout, { [weak self] in + var updatedTheme: PresentationTheme? + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + var updatedIcon: LocationActionListItemIcon? + if currentItem?.icon != item.icon || updatedTheme != nil { + updatedIcon = item.icon + } + + return (nil, { _ in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + if let _ = updatedTheme { + strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + } + + if let updatedIcon = updatedIcon { + switch updatedIcon { + case .location: + strongSelf.iconNode.isHidden = false + strongSelf.venueIconNode.isHidden = true + strongSelf.iconNode.image = generateLocationIcon(theme: item.presentationData.theme) + case .liveLocation, .stopLiveLocation: + strongSelf.iconNode.isHidden = false + strongSelf.venueIconNode.isHidden = true + strongSelf.iconNode.image = generateLiveLocationIcon(theme: item.presentationData.theme) + case let .venue(venue): + strongSelf.iconNode.isHidden = true + strongSelf.venueIconNode.isHidden = false + strongSelf.venueIconNode.setSignal(venueIcon(postbox: item.account.postbox, type: venue.venue?.type ?? "", background: true)) + } + } + + let iconApply = iconLayout(TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: iconSize, height: iconSize), boundingSize: CGSize(width: iconSize, height: iconSize), intrinsicInsets: UIEdgeInsets())) + iconApply() + + let titleNode = titleApply() + if strongSelf.titleNode == nil { + strongSelf.titleNode = titleNode + strongSelf.addSubnode(titleNode) + } + + let subtitleNode = subtitleApply() + if strongSelf.subtitleNode == nil { + strongSelf.subtitleNode = subtitleNode + strongSelf.addSubnode(subtitleNode) + } + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size) + titleNode.frame = titleFrame + + let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size) + subtitleNode.frame = subtitleFrame + + let separatorHeight = UIScreenPixel + let topHighlightInset: CGFloat = separatorHeight + + let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize)) + strongSelf.iconNode.frame = iconNodeFrame + strongSelf.venueIconNode.frame = iconNodeFrame + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: contentSize.width, height: contentSize.height + topHighlightInset)) + strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width, height: separatorHeight)) + strongSelf.separatorNode.isHidden = !hasSeparator + } + }) + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) + } +} diff --git a/submodules/LocationUI/Sources/LocationAnnotation.swift b/submodules/LocationUI/Sources/LocationAnnotation.swift new file mode 100644 index 0000000000..4dd1184bac --- /dev/null +++ b/submodules/LocationUI/Sources/LocationAnnotation.swift @@ -0,0 +1,581 @@ +import Foundation +import UIKit +import MapKit +import Display +import SwiftSignalKit +import Postbox +import SyncCore +import TelegramCore +import AvatarNode +import AppBundle +import TelegramPresentationData +import LocationResources +import AccountContext + +let locationPinReuseIdentifier = "locationPin" + +private func generateSmallBackgroundImage(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 56.0, height: 56.0)) { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setShadow(offset: CGSize(), blur: 4.0, color: UIColor(rgb: 0x000000, alpha: 0.5).cgColor) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(x: 16.0, y: 16.0, width: 24.0, height: 24.0)) + + context.setShadow(offset: CGSize(), blur: 0.0, color: nil) + context.setFillColor(color.cgColor) + context.fillEllipse(in: CGRect(x: 17.0 + UIScreenPixel, y: 17.0 + UIScreenPixel, width: 22.0 - 2.0 * UIScreenPixel, height: 22.0 - 2.0 * UIScreenPixel)) + } +} + +class LocationPinAnnotation: NSObject, MKAnnotation { + let context: AccountContext + let theme: PresentationTheme + var coordinate: CLLocationCoordinate2D + let location: TelegramMediaMap? + let peer: Peer? + let forcedSelection: Bool + + var title: String? = "" + var subtitle: String? = "" + + init(context: AccountContext, theme: PresentationTheme, peer: Peer) { + self.context = context + self.theme = theme + self.location = nil + self.peer = peer + self.coordinate = kCLLocationCoordinate2DInvalid + self.forcedSelection = false + super.init() + } + + init(context: AccountContext, theme: PresentationTheme, location: TelegramMediaMap, forcedSelection: Bool = false) { + self.context = context + self.theme = theme + self.location = location + self.peer = nil + self.coordinate = location.coordinate + self.forcedSelection = forcedSelection + super.init() + } + + var id: String { + if let peer = self.peer { + return "\(peer.id.toInt64())" + } else if let venueId = self.location?.venue?.id { + return venueId + } else { + return String(format: "%.5f_%.5f", self.coordinate.latitude, self.coordinate.longitude) + } + } +} + +class LocationPinAnnotationLayer: CALayer { + var customZPosition: CGFloat? + + override var zPosition: CGFloat { + get { + if let zPosition = self.customZPosition { + return zPosition + } else { + return super.zPosition + } + } set { + super.zPosition = newValue + } + } +} + +class LocationPinAnnotationView: MKAnnotationView { + let shadowNode: ASImageNode + let backgroundNode: ASImageNode + let smallNode: ASImageNode + let iconNode: TransformImageNode + let smallIconNode: TransformImageNode + let dotNode: ASImageNode + var avatarNode: AvatarNode? + var strokeLabelNode: ImmediateTextNode? + var labelNode: ImmediateTextNode? + + var initialized = false + var appeared = false + var animating = false + + override class var layerClass: AnyClass { + return LocationPinAnnotationLayer.self + } + + func setZPosition(_ zPosition: CGFloat?) { + if let layer = self.layer as? LocationPinAnnotationLayer { + layer.customZPosition = zPosition + } + } + + init(annotation: LocationPinAnnotation) { + self.shadowNode = ASImageNode() + self.shadowNode.image = UIImage(bundleImageName: "Location/PinShadow") + if let image = self.shadowNode.image { + self.shadowNode.bounds = CGRect(origin: CGPoint(), size: image.size) + } + + self.backgroundNode = ASImageNode() + self.backgroundNode.image = UIImage(bundleImageName: "Location/PinBackground") + if let image = self.backgroundNode.image { + self.backgroundNode.bounds = CGRect(origin: CGPoint(), size: image.size) + } + + self.smallNode = ASImageNode() + self.smallNode.image = UIImage(bundleImageName: "Location/PinSmallBackground") + if let image = self.smallNode.image { + self.smallNode.bounds = CGRect(origin: CGPoint(), size: image.size) + } + + self.iconNode = TransformImageNode() + self.iconNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 60.0, height: 60.0)) + + self.smallIconNode = TransformImageNode() + self.smallIconNode.frame = CGRect(origin: CGPoint(x: 15.0, y: 15.0), size: CGSize(width: 26.0, height: 26.0)) + + self.dotNode = ASImageNode() + self.dotNode.image = generateFilledCircleImage(diameter: 6.0, color: annotation.theme.list.itemAccentColor) + if let image = self.dotNode.image { + self.dotNode.bounds = CGRect(origin: CGPoint(), size: image.size) + } + + super.init(annotation: annotation, reuseIdentifier: locationPinReuseIdentifier) + + self.addSubnode(self.dotNode) + + self.addSubnode(self.shadowNode) + self.shadowNode.addSubnode(self.backgroundNode) + self.backgroundNode.addSubnode(self.iconNode) + + self.addSubnode(self.smallNode) + self.smallNode.addSubnode(self.smallIconNode) + + self.annotation = annotation + } + + var defaultZPosition: CGFloat { + if let annotation = self.annotation as? LocationPinAnnotation { + if annotation.forcedSelection { + return 0.0 + } else if let venueType = annotation.location?.venue?.type, ["home", "work"].contains(venueType) { + return -0.5 + } else { + return -1.0 + } + } else { + return -1.0 + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var annotation: MKAnnotation? { + didSet { + if let annotation = self.annotation as? LocationPinAnnotation { + if let peer = annotation.peer { + self.iconNode.isHidden = true + self.dotNode.isHidden = true + self.backgroundNode.image = UIImage(bundleImageName: "Location/PinBackground") + + self.setPeer(context: annotation.context, theme: annotation.theme, peer: peer) + self.setSelected(true, animated: false) + } else if let location = annotation.location { + let venueType = annotation.location?.venue?.type ?? "" + let color = venueType.isEmpty ? annotation.theme.list.itemAccentColor : venueIconColor(type: venueType) + self.backgroundNode.image = generateTintedImage(image: UIImage(bundleImageName: "Location/PinBackground"), color: color) + self.iconNode.setSignal(venueIcon(postbox: annotation.context.account.postbox, type: venueType, background: false)) + self.smallIconNode.setSignal(venueIcon(postbox: annotation.context.account.postbox, type: venueType, background: false)) + self.smallNode.image = generateSmallBackgroundImage(color: color) + self.dotNode.image = generateFilledCircleImage(diameter: 6.0, color: color) + + self.dotNode.isHidden = false + + if !self.isSelected { + self.dotNode.alpha = 0.0 + self.shadowNode.isHidden = true + self.smallNode.isHidden = false + } + + if annotation.forcedSelection { + self.setSelected(true, animated: false) + } + + if self.initialized && !self.appeared { + self.appeared = true + self.animateAppearance() + } + } + } + } + } + + override func prepareForReuse() { + self.smallNode.isHidden = true + self.backgroundNode.isHidden = false + self.appeared = false + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + if let annotation = self.annotation as? LocationPinAnnotation { + if annotation.forcedSelection && !selected { + return + } + } + + if animated { + self.layoutSubviews() + + self.animating = true + if selected { + let avatarSnapshot = self.avatarNode?.view.snapshotContentTree() + if let avatarSnapshot = avatarSnapshot, let avatarNode = self.avatarNode { + self.smallNode.view.addSubview(avatarSnapshot) + avatarSnapshot.layer.transform = avatarNode.transform + avatarSnapshot.center = CGPoint(x: self.smallNode.frame.width / 2.0, y: self.smallNode.frame.height / 2.0) + + avatarNode.transform = CATransform3DIdentity + self.backgroundNode.addSubnode(avatarNode) + avatarNode.position = CGPoint(x: self.backgroundNode.frame.width / 2.0, y: self.backgroundNode.frame.height / 2.0 - 5.0) + } + + self.shadowNode.position = CGPoint(x: self.shadowNode.position.x, y: self.shadowNode.position.y + self.shadowNode.frame.height / 2.0) + self.shadowNode.anchorPoint = CGPoint(x: 0.5, y: 1.0) + self.shadowNode.isHidden = false + self.shadowNode.transform = CATransform3DMakeScale(0.1, 0.1, 1.0) + + UIView.animate(withDuration: 0.35, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.5, options: [], animations: { + self.smallNode.transform = CATransform3DMakeScale(0.001, 0.001, 1.0) + self.shadowNode.transform = CATransform3DIdentity + + if self.dotNode.isHidden { + self.smallNode.alpha = 0.0 + } + }) { _ in + self.animating = false + + self.shadowNode.anchorPoint = CGPoint(x: 0.5, y: 0.5) + + self.smallNode.isHidden = true + self.smallNode.transform = CATransform3DIdentity + + if let avatarNode = self.avatarNode { + self.addSubnode(avatarNode) + avatarSnapshot?.removeFromSuperview() + } + } + + self.dotNode.alpha = 1.0 + self.dotNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + if let annotation = self.annotation as? LocationPinAnnotation, let venue = annotation.location?.venue { + var textColor = UIColor.black + var strokeTextColor = UIColor.white + if #available(iOS 13.0, *) { + if self.traitCollection.userInterfaceStyle == .dark { + textColor = .white + strokeTextColor = .black + } + } + let strokeLabelNode = ImmediateTextNode() + strokeLabelNode.displaysAsynchronously = false + strokeLabelNode.isUserInteractionEnabled = false + strokeLabelNode.attributedText = NSAttributedString(string: venue.title, font: Font.medium(10), textColor: strokeTextColor) + strokeLabelNode.maximumNumberOfLines = 2 + strokeLabelNode.textAlignment = .center + strokeLabelNode.truncationType = .end + strokeLabelNode.textStroke = (strokeTextColor, 2.0 - UIScreenPixel) + self.strokeLabelNode = strokeLabelNode + self.addSubnode(strokeLabelNode) + + let labelNode = ImmediateTextNode() + labelNode.displaysAsynchronously = false + labelNode.isUserInteractionEnabled = false + labelNode.attributedText = NSAttributedString(string: venue.title, font: Font.medium(10), textColor: textColor) + labelNode.maximumNumberOfLines = 2 + labelNode.textAlignment = .center + labelNode.truncationType = .end + self.labelNode = labelNode + self.addSubnode(labelNode) + + var size = labelNode.updateLayout(CGSize(width: 120.0, height: CGFloat.greatestFiniteMagnitude)) + size.height += 2.0 + labelNode.bounds = CGRect(origin: CGPoint(), size: size) + labelNode.position = CGPoint(x: 0.0, y: 10.0 + floor(size.height / 2.0)) + + var strokeSize = strokeLabelNode.updateLayout(CGSize(width: 120.0, height: CGFloat.greatestFiniteMagnitude)) + strokeSize.height += 2.0 + strokeLabelNode.bounds = CGRect(origin: CGPoint(), size: strokeSize) + strokeLabelNode.position = CGPoint(x: 0.0, y: 10.0 + floor(strokeSize.height / 2.0)) + + strokeLabelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } else { + self.strokeLabelNode?.removeFromSupernode() + self.strokeLabelNode = nil + self.labelNode?.removeFromSupernode() + self.labelNode = nil + } + } else { + let avatarSnapshot = self.avatarNode?.view.snapshotContentTree() + if let avatarSnapshot = avatarSnapshot, let avatarNode = self.avatarNode { + self.backgroundNode.view.addSubview(avatarSnapshot) + avatarSnapshot.layer.transform = avatarNode.transform + avatarSnapshot.center = CGPoint(x: self.backgroundNode.frame.width / 2.0, y: self.backgroundNode.frame.height / 2.0 - 5.0) + + avatarNode.transform = CATransform3DMakeScale(0.64, 0.64, 1.0) + self.smallNode.addSubnode(avatarNode) + avatarNode.position = CGPoint(x: self.smallNode.frame.width / 2.0, y: self.smallNode.frame.height / 2.0) + } + + self.smallNode.isHidden = false + self.smallNode.transform = CATransform3DMakeScale(0.01, 0.01, 1.0) + + self.shadowNode.position = CGPoint(x: self.shadowNode.position.x, y: self.shadowNode.position.y + self.shadowNode.frame.height / 2.0) + self.shadowNode.anchorPoint = CGPoint(x: 0.5, y: 1.0) + + UIView.animate(withDuration: 0.35, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.5, options: [], animations: { + self.smallNode.transform = CATransform3DIdentity + self.shadowNode.transform = CATransform3DMakeScale(0.1, 0.1, 1.0) + + if self.dotNode.isHidden { + self.smallNode.alpha = 1.0 + } + }) { _ in + self.animating = false + + self.shadowNode.anchorPoint = CGPoint(x: 0.5, y: 0.5) + + self.shadowNode.isHidden = true + self.shadowNode.transform = CATransform3DIdentity + + if let avatarNode = self.avatarNode { + self.addSubnode(avatarNode) + avatarSnapshot?.removeFromSuperview() + } + } + + let previousAlpha = self.dotNode.alpha + self.dotNode.alpha = 0.0 + self.dotNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2) + + if let labelNode = self.labelNode { + self.labelNode = nil + labelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + labelNode.removeFromSupernode() + }) + + if let strokeLabelNode = self.strokeLabelNode { + self.strokeLabelNode = nil + strokeLabelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + strokeLabelNode.removeFromSupernode() + }) + } + } + } + } else { + self.smallNode.isHidden = selected + self.shadowNode.isHidden = !selected + self.dotNode.alpha = selected ? 1.0 : 0.0 + self.smallNode.alpha = 1.0 + + if !selected { + self.labelNode?.removeFromSupernode() + self.labelNode = nil + self.strokeLabelNode?.removeFromSupernode() + self.strokeLabelNode = nil + } + + self.layoutSubviews() + } + } + + func setPeer(context: AccountContext, theme: PresentationTheme, peer: Peer) { + let avatarNode: AvatarNode + if let currentAvatarNode = self.avatarNode { + avatarNode = currentAvatarNode + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 24.0)) + avatarNode.isLayerBacked = false + avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 55.0, height: 55.0)) + avatarNode.position = CGPoint() + self.avatarNode = avatarNode + self.addSubnode(avatarNode) + } + + avatarNode.setPeer(context: context, theme: theme, peer: peer) + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if let labelNode = self.labelNode { + var textColor = UIColor.black + var strokeTextColor = UIColor.white + if #available(iOS 13.0, *) { + if self.traitCollection.userInterfaceStyle == .dark { + textColor = .white + strokeTextColor = .black + } + } + labelNode.attributedText = NSAttributedString(string: labelNode.attributedText?.string ?? "", font: Font.medium(10), textColor: textColor) + let _ = labelNode.updateLayout(CGSize(width: 120.0, height: CGFloat.greatestFiniteMagnitude)) + + if let strokeLabelNode = self.strokeLabelNode { + strokeLabelNode.attributedText = NSAttributedString(string: labelNode.attributedText?.string ?? "", font: Font.bold(10), textColor: strokeTextColor) + let _ = strokeLabelNode.updateLayout(CGSize(width: 120.0, height: CGFloat.greatestFiniteMagnitude)) + } + } + } + + var isRaised = false + func setRaised(_ raised: Bool, animated: Bool, completion: @escaping () -> Void = {}) { + guard raised != self.isRaised else { + return + } + + self.isRaised = raised + self.shadowNode.layer.removeAllAnimations() + + if animated { + self.animating = true + + if raised { + let previousPosition = self.shadowNode.position + self.shadowNode.position = CGPoint(x: UIScreenPixel, y: -66.0) + self.shadowNode.layer.animatePosition(from: previousPosition, to: self.shadowNode.position, duration: 0.2, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring) { finished in + self.animating = false + if finished { + completion() + } + } + } else { + UIView.animate(withDuration: 0.2, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, options: [.allowAnimatedContent], animations: { + self.shadowNode.position = CGPoint(x: UIScreenPixel, y: -36.0) + }) { finished in + self.animating = false + if finished { + completion() + } + } + } + } else { + self.shadowNode.position = CGPoint(x: UIScreenPixel, y: raised ? -66.0 : -36.0) + completion() + } + } + + func setCustom(_ custom: Bool, animated: Bool) { + if let annotation = self.annotation as? LocationPinAnnotation { + self.iconNode.setSignal(venueIcon(postbox: annotation.context.account.postbox, type: "", background: false)) + } + + if let avatarNode = self.avatarNode { + self.backgroundNode.addSubnode(avatarNode) + avatarNode.position = CGPoint(x: self.backgroundNode.frame.width / 2.0, y: self.backgroundNode.frame.height / 2.0 - 5.0) + } + self.shadowNode.position = CGPoint(x: UIScreenPixel, y: -36.0) + self.backgroundNode.position = CGPoint(x: self.shadowNode.frame.width / 2.0, y: self.shadowNode.frame.height / 2.0) + self.iconNode.position = CGPoint(x: self.shadowNode.frame.width / 2.0, y: self.shadowNode.frame.height / 2.0 - 5.0) + + let transition = { + let color: UIColor + if custom, let annotation = self.annotation as? LocationPinAnnotation { + color = annotation.theme.list.itemAccentColor + } else { + color = .white + } + self.backgroundNode.image = generateTintedImage(image: UIImage(bundleImageName: "Location/PinBackground"), color: color) + self.avatarNode?.isHidden = custom + self.iconNode.isHidden = !custom + } + + let completion = { + if !custom, let avatarNode = self.avatarNode { + self.addSubnode(avatarNode) + } + } + + if animated { + self.animating = true + Queue.mainQueue().after(0.01) { + UIView.transition(with: self.backgroundNode.view, duration: 0.2, options: [.transitionCrossDissolve, .allowAnimatedContent], animations: { + transition() + }) { finished in + completion() + self.animating = false + } + } + + } else { + transition() + completion() + } + self.setNeedsLayout() + + self.dotNode.isHidden = !custom + } + + func animateAppearance() { + guard let annotation = self.annotation as? LocationPinAnnotation, annotation.location != nil && !annotation.forcedSelection else { + return + } + + self.smallNode.transform = CATransform3DMakeScale(0.1, 0.1, 1.0) + + let avatarNodeTransform = self.avatarNode?.transform + self.avatarNode?.transform = CATransform3DMakeScale(0.1, 0.1, 1.0) + UIView.animate(withDuration: 0.55, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.5, options: [], animations: { + self.smallNode.transform = CATransform3DIdentity + if let avatarNodeTransform = avatarNodeTransform { + self.avatarNode?.transform = avatarNodeTransform + } + }) { _ in + } + } + + override func layoutSubviews() { + super.layoutSubviews() + + guard !self.animating else { + return + } + + self.dotNode.position = CGPoint() + self.smallNode.position = CGPoint() + self.shadowNode.position = CGPoint(x: UIScreenPixel, y: self.isRaised ? -66.0 : -36.0) + self.backgroundNode.position = CGPoint(x: self.shadowNode.frame.width / 2.0, y: self.shadowNode.frame.height / 2.0) + self.iconNode.position = CGPoint(x: self.shadowNode.frame.width / 2.0, y: self.shadowNode.frame.height / 2.0 - 5.0) + + let smallIconLayout = self.smallIconNode.asyncLayout() + let smallIconApply = smallIconLayout(TransformImageArguments(corners: ImageCorners(), imageSize: self.smallIconNode.bounds.size, boundingSize: self.smallIconNode.bounds.size, intrinsicInsets: UIEdgeInsets())) + smallIconApply() + + var arguments: VenueIconArguments? + if let annotation = self.annotation as? LocationPinAnnotation { + arguments = VenueIconArguments(defaultForegroundColor: annotation.theme.chat.inputPanel.actionControlForegroundColor) + } + + let iconLayout = self.iconNode.asyncLayout() + let iconApply = iconLayout(TransformImageArguments(corners: ImageCorners(), imageSize: self.iconNode.bounds.size, boundingSize: self.iconNode.bounds.size, intrinsicInsets: UIEdgeInsets(), custom: arguments)) + iconApply() + + if let avatarNode = self.avatarNode { + avatarNode.position = self.isSelected ? CGPoint(x: UIScreenPixel, y: -41.0) : CGPoint() + avatarNode.transform = self.isSelected ? CATransform3DIdentity : CATransform3DMakeScale(0.64, 0.64, 1.0) + avatarNode.view.superview?.bringSubviewToFront(avatarNode.view) + } + + if !self.appeared { + self.appeared = true + self.initialized = true + self.animateAppearance() + } + } +} diff --git a/submodules/LocationUI/Sources/LocationAttributionItem.swift b/submodules/LocationUI/Sources/LocationAttributionItem.swift new file mode 100644 index 0000000000..594ef371c8 --- /dev/null +++ b/submodules/LocationUI/Sources/LocationAttributionItem.swift @@ -0,0 +1,116 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Postbox +import Display +import SwiftSignalKit +import TelegramPresentationData +import ListSectionHeaderNode +import ItemListUI +import AppBundle + +class LocationAttributionItem: ListViewItem { + let presentationData: ItemListPresentationData + + public init(presentationData: ItemListPresentationData) { + self.presentationData = presentationData + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = LocationAttributionItemNode() + let makeLayout = node.asyncLayout() + let (nodeLayout, nodeApply) = makeLayout(self, params) + node.contentSize = nodeLayout.contentSize + node.insets = nodeLayout.insets + + completion(node, nodeApply) + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? LocationAttributionItemNode { + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params) + Queue.mainQueue().async { + completion(nodeLayout, { info in + apply().1(info) + }) + } + } + } + } + } + + public var selectable: Bool { + return false + } +} + +private class LocationAttributionItemNode: ListViewItemNode { + private var imageNode: ASImageNode + + private var item: LocationAttributionItem? + private var layoutParams: ListViewItemLayoutParams? + + required init() { + self.imageNode = ASImageNode() + self.imageNode.displaysAsynchronously = false + self.imageNode.displayWithoutProcessing = true + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.imageNode) + } + + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + if let item = self.item { + let makeLayout = self.asyncLayout() + let (nodeLayout, nodeApply) = makeLayout(item, params) + self.contentSize = nodeLayout.contentSize + self.insets = nodeLayout.insets + let _ = nodeApply() + } + } + + func asyncLayout() -> (_ item: LocationAttributionItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> (Signal?, (ListViewItemApply) -> Void)) { + let currentItem = self.item + + return { [weak self] item, params in + let contentSize = CGSize(width: params.width, height: 55.0) + let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets()) + + return (nodeLayout, { [weak self] in + var updatedTheme: PresentationTheme? + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + return (nil, { _ in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + if let _ = updatedTheme { + strongSelf.imageNode.image = generateTintedImage(image: UIImage(bundleImageName: "Location/FoursquareAttribution"), color: item.presentationData.theme.list.itemSecondaryTextColor) + } + + if let image = strongSelf.imageNode.image { + strongSelf.imageNode.frame = CGRect(x: floor((params.width - image.size.width) / 2.0), y: 0.0, width: image.size.width, height: image.size.height) + } + } + }) + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) + } +} diff --git a/submodules/LocationUI/Sources/LocationInfoListItem.swift b/submodules/LocationUI/Sources/LocationInfoListItem.swift new file mode 100644 index 0000000000..cefe3c3ee4 --- /dev/null +++ b/submodules/LocationUI/Sources/LocationInfoListItem.swift @@ -0,0 +1,264 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Postbox +import Display +import SwiftSignalKit +import TelegramCore +import SyncCore +import TelegramPresentationData +import ItemListUI +import LocationResources +import AppBundle +import SolidRoundedButtonNode + +final class LocationInfoListItem: ListViewItem { + let presentationData: ItemListPresentationData + let account: Account + let location: TelegramMediaMap + let address: String? + let distance: String? + let eta: String? + let action: () -> Void + let getDirections: () -> Void + + public init(presentationData: ItemListPresentationData, account: Account, location: TelegramMediaMap, address: String?, distance: String?, eta: String?, action: @escaping () -> Void, getDirections: @escaping () -> Void) { + self.presentationData = presentationData + self.account = account + self.location = location + self.address = address + self.distance = distance + self.eta = eta + self.action = action + self.getDirections = getDirections + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = LocationInfoListItemNode() + let makeLayout = node.asyncLayout() + let (nodeLayout, nodeApply) = makeLayout(self, params) + node.contentSize = nodeLayout.contentSize + node.insets = nodeLayout.insets + + completion(node, nodeApply) + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? LocationInfoListItemNode { + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params) + Queue.mainQueue().async { + completion(nodeLayout, { info in + apply().1(info) + }) + } + } + } + } + } + + public var selectable: Bool { + return false + } +} + +final class LocationInfoListItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private var titleNode: TextNode? + private var subtitleNode: TextNode? + private let venueIconNode: TransformImageNode + private let buttonNode: HighlightableButtonNode + private var directionsButtonNode: SolidRoundedButtonNode? + + private var item: LocationInfoListItem? + private var layoutParams: ListViewItemLayoutParams? + + required init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.buttonNode = HighlightableButtonNode() + self.venueIconNode = TransformImageNode() + self.venueIconNode.isUserInteractionEnabled = false + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.buttonNode) + self.addSubnode(self.venueIconNode) + + self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.titleNode?.layer.removeAnimation(forKey: "opacity") + strongSelf.titleNode?.alpha = 0.4 + strongSelf.subtitleNode?.layer.removeAnimation(forKey: "opacity") + strongSelf.subtitleNode?.alpha = 0.4 + strongSelf.venueIconNode.layer.removeAnimation(forKey: "opacity") + strongSelf.venueIconNode.alpha = 0.4 + } else { + strongSelf.titleNode?.alpha = 1.0 + strongSelf.titleNode?.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.subtitleNode?.alpha = 1.0 + strongSelf.subtitleNode?.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.venueIconNode.alpha = 1.0 + strongSelf.venueIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + if let item = self.item { + let makeLayout = self.asyncLayout() + let (nodeLayout, nodeApply) = makeLayout(item, params) + self.contentSize = nodeLayout.contentSize + self.insets = nodeLayout.insets + let _ = nodeApply() + } + } + + func asyncLayout() -> (_ item: LocationInfoListItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> (Signal?, (ListViewItemApply) -> Void)) { + let currentItem = self.item + + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) + let iconLayout = self.venueIconNode.asyncLayout() + + return { [weak self] item, params in + let leftInset: CGFloat = 75.0 + params.leftInset + let rightInset: CGFloat = params.rightInset + let verticalInset: CGFloat = 14.0 + let iconSize: CGFloat = 48.0 + let inset: CGFloat = 15.0 + + let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize) + let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + + let title: String + let subtitle: String + var subtitleComponents: [String] = [] + + if let venue = item.location.venue { + title = venue.title + } else { + title = item.presentationData.strings.Map_Location + } + + if let address = item.address { + subtitleComponents.append(address) + } + if let distance = item.distance { + subtitleComponents.append(distance) + } + + subtitle = subtitleComponents.joined(separator: " • ") + + let titleAttributedString = NSAttributedString(string: title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let subtitleAttributedString = NSAttributedString(string: subtitle, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let titleSpacing: CGFloat = 1.0 + let bottomInset: CGFloat = 4.0 + let contentSize = CGSize(width: params.width, height: max(126.0, verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + bottomInset)) + let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets()) + + return (nodeLayout, { [weak self] in + var updatedTheme: PresentationTheme? + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + var updatedLocation: TelegramMediaMap? + if currentItem?.location.venue?.id != item.location.venue?.id || updatedTheme != nil { + updatedLocation = item.location + } + + return (nil, { _ in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + if let _ = updatedTheme { + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor + strongSelf.directionsButtonNode?.updateTheme(SolidRoundedButtonTheme(theme: item.presentationData.theme)) + } + + if let updatedLocation = updatedLocation { + strongSelf.venueIconNode.setSignal(venueIcon(postbox: item.account.postbox, type: updatedLocation.venue?.type ?? "", background: true)) + } + + let iconApply = iconLayout(TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: iconSize, height: iconSize), boundingSize: CGSize(width: iconSize, height: iconSize), intrinsicInsets: UIEdgeInsets())) + iconApply() + + let titleNode = titleApply() + if strongSelf.titleNode == nil { + titleNode.isUserInteractionEnabled = false + strongSelf.titleNode = titleNode + strongSelf.addSubnode(titleNode) + } + + let subtitleNode = subtitleApply() + if strongSelf.subtitleNode == nil { + subtitleNode.isUserInteractionEnabled = false + strongSelf.subtitleNode = subtitleNode + strongSelf.addSubnode(subtitleNode) + } + + let directionsButtonNode: SolidRoundedButtonNode + if let currentDirectionsButtonNode = strongSelf.directionsButtonNode { + directionsButtonNode = currentDirectionsButtonNode + } else { + directionsButtonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: item.presentationData.theme), height: 50.0, cornerRadius: 10.0) + directionsButtonNode.title = item.presentationData.strings.Map_Directions + directionsButtonNode.pressed = { + item.getDirections() + } + strongSelf.addSubnode(directionsButtonNode) + strongSelf.directionsButtonNode = directionsButtonNode + } + directionsButtonNode.subtitle = item.eta + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size) + titleNode.frame = titleFrame + + let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size) + subtitleNode.frame = subtitleFrame + + let separatorHeight = UIScreenPixel + let topHighlightInset: CGFloat = separatorHeight + + let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + inset, y: 10.0), size: CGSize(width: iconSize, height: iconSize)) + strongSelf.venueIconNode.frame = iconNodeFrame + + let directionsWidth = contentSize.width - inset * 2.0 + let directionsHeight = directionsButtonNode.updateLayout(width: directionsWidth, transition: .immediate) + directionsButtonNode.frame = CGRect(x: inset, y: iconNodeFrame.maxY + 14.0, width: directionsWidth, height: directionsHeight) + + strongSelf.buttonNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: 72.0) + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height)) + } + }) + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) + } + + @objc private func buttonPressed() { + self.item?.action() + } +} diff --git a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift new file mode 100644 index 0000000000..817b6fa7a7 --- /dev/null +++ b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift @@ -0,0 +1,181 @@ +import Foundation +import Display +import TelegramPresentationData +import AppBundle + +private let panelInset: CGFloat = 4.0 +private let panelSize = CGSize(width: 46.0, height: 90.0) + +private func generateBackgroundImage(theme: PresentationTheme) -> UIImage? { + let cornerRadius: CGFloat = 9.0 + return generateImage(CGSize(width: (cornerRadius + panelInset) * 2.0, height: (cornerRadius + panelInset) * 2.0)) { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setShadow(offset: CGSize(), blur: 10.0, color: UIColor(rgb: 0x000000, alpha: 0.2).cgColor) + context.setFillColor(theme.rootController.navigationBar.backgroundColor.cgColor) + let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: panelInset, y: panelInset), size: CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0)), cornerRadius: cornerRadius) + context.addPath(path.cgPath) + context.fillPath() + }?.stretchableImage(withLeftCapWidth: Int(cornerRadius + panelInset), topCapHeight: Int(cornerRadius + panelInset)) +} + +private func generateShadowImage(theme: PresentationTheme, highlighted: Bool) -> UIImage? { + return generateImage(CGSize(width: 26.0, height: 14.0)) { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setShadow(offset: CGSize(), blur: 10.0, color: UIColor(rgb: 0x000000, alpha: 0.2).cgColor) + context.setFillColor(highlighted ? theme.list.itemHighlightedBackgroundColor.cgColor : theme.list.plainBackgroundColor.cgColor) + let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 0.0, y: 4.0), size: CGSize(width: 26.0, height: 20.0)), cornerRadius: 9.0) + context.addPath(path.cgPath) + context.fillPath() + }?.stretchableImage(withLeftCapWidth: 13, topCapHeight: 0) +} + +final class LocationMapHeaderNode: ASDisplayNode { + private var presentationData: PresentationData + private let toggleMapModeSelection: () -> Void + private let goToUserLocation: () -> Void + private let showPlacesInThisArea: () -> Void + + private var displayingPlacesButton = false + + let mapNode: LocationMapNode + private let optionsBackgroundNode: ASImageNode + private let optionsSeparatorNode: ASDisplayNode + private let infoButtonNode: HighlightableButtonNode + private let locationButtonNode: HighlightableButtonNode + private let placesBackgroundNode: ASImageNode + private let placesButtonNode: HighlightableButtonNode + private let shadowNode: ASImageNode + + private var validLayout: (ContainerViewLayout, CGFloat, CGFloat, CGFloat, CGSize)? + + init(presentationData: PresentationData, toggleMapModeSelection: @escaping () -> Void, goToUserLocation: @escaping () -> Void, showPlacesInThisArea: @escaping () -> Void = {}) { + self.presentationData = presentationData + self.toggleMapModeSelection = toggleMapModeSelection + self.goToUserLocation = goToUserLocation + self.showPlacesInThisArea = showPlacesInThisArea + + self.mapNode = LocationMapNode() + + self.optionsBackgroundNode = ASImageNode() + self.optionsBackgroundNode.contentMode = .scaleToFill + self.optionsBackgroundNode.displaysAsynchronously = false + self.optionsBackgroundNode.displayWithoutProcessing = true + self.optionsBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme) + self.optionsBackgroundNode.isUserInteractionEnabled = true + + self.optionsSeparatorNode = ASDisplayNode() + self.optionsSeparatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor + + self.infoButtonNode = HighlightableButtonNode() + self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal) + self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .selected) + self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: [.selected, .highlighted]) + + self.locationButtonNode = HighlightableButtonNode() + self.locationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/TrackIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal) + + self.placesBackgroundNode = ASImageNode() + self.placesBackgroundNode.contentMode = .scaleToFill + self.placesBackgroundNode.displaysAsynchronously = false + self.placesBackgroundNode.displayWithoutProcessing = true + self.placesBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme) + self.placesBackgroundNode.isUserInteractionEnabled = true + + self.placesButtonNode = HighlightableButtonNode() + self.placesButtonNode.setTitle(presentationData.strings.Map_PlacesInThisArea, with: Font.regular(17.0), with: presentationData.theme.rootController.navigationBar.buttonColor, for: .normal) + + self.shadowNode = ASImageNode() + self.shadowNode.contentMode = .scaleToFill + self.shadowNode.displaysAsynchronously = false + self.shadowNode.displayWithoutProcessing = true + self.shadowNode.image = generateShadowImage(theme: presentationData.theme, highlighted: false) + + super.init() + + self.clipsToBounds = true + + self.addSubnode(self.mapNode) + self.addSubnode(self.optionsBackgroundNode) + self.optionsBackgroundNode.addSubnode(self.optionsSeparatorNode) + self.optionsBackgroundNode.addSubnode(self.infoButtonNode) + self.optionsBackgroundNode.addSubnode(self.locationButtonNode) + self.addSubnode(self.placesBackgroundNode) + self.placesBackgroundNode.addSubnode(self.placesButtonNode) + self.addSubnode(self.shadowNode) + + self.infoButtonNode.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside) + self.locationButtonNode.addTarget(self, action: #selector(self.locationPressed), forControlEvents: .touchUpInside) + self.placesButtonNode.addTarget(self, action: #selector(self.placesPressed), forControlEvents: .touchUpInside) + } + + func updateState(mapMode: LocationMapMode, displayingMapModeOptions: Bool, displayingPlacesButton: Bool, animated: Bool) { + self.mapNode.mapMode = mapMode + self.infoButtonNode.isSelected = displayingMapModeOptions + + let updateLayout = self.displayingPlacesButton != displayingPlacesButton + self.displayingPlacesButton = displayingPlacesButton + + if updateLayout, let (layout, navigationBarHeight, topPadding, offset, size) = self.validLayout { + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .spring) : .immediate + self.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: topPadding, offset: offset, size: size, transition: transition) + } + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + + self.optionsBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme) + self.optionsSeparatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor + self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal) + self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .selected) + self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: [.selected, .highlighted]) + self.locationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/TrackIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal) + self.placesBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme) + self.shadowNode.image = generateShadowImage(theme: presentationData.theme, highlighted: false) + } + + func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, topPadding: CGFloat, offset: CGFloat, size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = (layout, navigationBarHeight, topPadding, offset, size) + + let mapHeight: CGFloat = floor(layout.size.height * 1.5) + let mapFrame = CGRect(x: 0.0, y: floorToScreenPixels((size.height - mapHeight + navigationBarHeight) / 2.0) + offset, width: size.width, height: mapHeight) + transition.updateFrame(node: self.mapNode, frame: mapFrame) + self.mapNode.updateLayout(size: mapFrame.size) + + let inset: CGFloat = 6.0 + + let placesButtonSize = CGSize(width: 180.0 + panelInset * 2.0, height: 45.0 + panelInset * 2.0) + let placesButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - placesButtonSize.width) / 2.0), y: self.displayingPlacesButton ? navigationBarHeight + topPadding + inset : 0.0), size: placesButtonSize) + transition.updateFrame(node: self.placesBackgroundNode, frame: placesButtonFrame) + transition.updateFrame(node: self.placesButtonNode, frame: CGRect(origin: CGPoint(), size: placesButtonSize)) + + transition.updateFrame(node: self.shadowNode, frame: CGRect(x: 0.0, y: size.height - 14.0, width: size.width, height: 14.0)) + + transition.updateFrame(node: self.optionsBackgroundNode, frame: CGRect(x: size.width - inset - panelSize.width - panelInset * 2.0, y: navigationBarHeight + topPadding + inset, width: panelSize.width + panelInset * 2.0, height: panelSize.height + panelInset * 2.0)) + transition.updateFrame(node: self.infoButtonNode, frame: CGRect(x: panelInset, y: panelInset, width: panelSize.width, height: panelSize.height / 2.0)) + transition.updateFrame(node: self.locationButtonNode, frame: CGRect(x: panelInset, y: panelInset + panelSize.height / 2.0, width: panelSize.width, height: panelSize.height / 2.0)) + transition.updateFrame(node: self.optionsSeparatorNode, frame: CGRect(x: panelInset, y: panelInset + panelSize.height / 2.0, width: panelSize.width, height: UIScreenPixel)) + + let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + let optionsAlpha: CGFloat = size.height > 160.0 + navigationBarHeight ? 1.0 : 0.0 + alphaTransition.updateAlpha(node: self.optionsBackgroundNode, alpha: optionsAlpha) + } + + func updateHighlight(_ highlighted: Bool) { + self.shadowNode.image = generateShadowImage(theme: self.presentationData.theme, highlighted: highlighted) + } + + @objc private func infoPressed() { + self.toggleMapModeSelection() + } + + @objc private func locationPressed() { + self.goToUserLocation() + } + + @objc private func placesPressed() { + self.showPlacesInThisArea() + } +} diff --git a/submodules/LocationUI/Sources/LocationMapNode.swift b/submodules/LocationUI/Sources/LocationMapNode.swift new file mode 100644 index 0000000000..eebbdaa76c --- /dev/null +++ b/submodules/LocationUI/Sources/LocationMapNode.swift @@ -0,0 +1,415 @@ +import Foundation +import Display +import SwiftSignalKit +import MapKit + +let defaultMapSpan = MKCoordinateSpan(latitudeDelta: 0.016, longitudeDelta: 0.016) +let viewMapSpan = MKCoordinateSpan(latitudeDelta: 0.008, longitudeDelta: 0.008) +private let pinOffset = CGPoint(x: 0.0, y: 33.0) + +public enum LocationMapMode { + case map + case sattelite + case hybrid + + var mapType: MKMapType { + switch self { + case .sattelite: + return .satellite + case .hybrid: + return .hybrid + default: + return .standard + } + } +} + +private class PickerAnnotationContainerView: UIView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if result == self { + return nil + } + return result + } +} + +private class LocationMapView: MKMapView, UIGestureRecognizerDelegate { + var customHitTest: ((CGPoint) -> Bool)? + private var allowSelectionChanges = true + + @objc override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let customHitTest = self.customHitTest, customHitTest(gestureRecognizer.location(in: self)) { + return false + } + return self.allowSelectionChanges + } + + public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + let pointInside = super.point(inside: point, with: event) + if !pointInside { + return pointInside + } + + for annotation in self.annotations(in: self.visibleMapRect) where annotation is LocationPinAnnotation { + guard let view = self.view(for: annotation as! MKAnnotation) else { + continue + } + if view.frame.insetBy(dx: -16.0, dy: -16.0).contains(point) { + self.allowSelectionChanges = true + return true + } + } + self.allowSelectionChanges = false + + return pointInside + } +} + +final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { + private let locationPromise = Promise(nil) + + private let pickerAnnotationContainerView: PickerAnnotationContainerView + private weak var userLocationAnnotationView: MKAnnotationView? + + private let pinDisposable = MetaDisposable() + + private var mapView: LocationMapView? { + return self.view as? LocationMapView + } + + var returnedToUserLocation = true + var ignoreRegionChanges = false + var isDragging = false + var beganInteractiveDragging: (() -> Void)? + var endedInteractiveDragging: ((CLLocationCoordinate2D) -> Void)? + + var annotationSelected: ((LocationPinAnnotation?) -> Void)? + var userLocationAnnotationSelected: (() -> Void)? + + override init() { + self.pickerAnnotationContainerView = PickerAnnotationContainerView() + self.pickerAnnotationContainerView.isHidden = true + + super.init() + + self.setViewBlock({ + return LocationMapView() + }) + } + + override func didLoad() { + super.didLoad() + + self.mapView?.interactiveTransitionGestureRecognizerTest = { p in + if p.x > 44.0 { + return true + } else { + return false + } + } + self.mapView?.delegate = self + self.mapView?.mapType = self.mapMode.mapType + self.mapView?.isRotateEnabled = self.isRotateEnabled + self.mapView?.showsUserLocation = true + self.mapView?.showsPointsOfInterest = false + self.mapView?.customHitTest = { [weak self] point in + guard let strongSelf = self, let annotationView = strongSelf.customUserLocationAnnotationView else { + return false + } + + if let annotationRect = annotationView.superview?.convert(annotationView.frame.insetBy(dx: -16.0, dy: -16.0), to: strongSelf.mapView), annotationRect.contains(point) { + strongSelf.userLocationAnnotationSelected?() + return true + } + + return false + } + + self.view.addSubview(self.pickerAnnotationContainerView) + } + + var isRotateEnabled: Bool = true { + didSet { + self.mapView?.isRotateEnabled = self.isRotateEnabled + } + } + + var mapMode: LocationMapMode = .map { + didSet { + self.mapView?.mapType = self.mapMode.mapType + } + } + + func setMapCenter(coordinate: CLLocationCoordinate2D, span: MKCoordinateSpan = defaultMapSpan, offset: CGPoint = CGPoint(), isUserLocation: Bool = false, hidePicker: Bool = false, animated: Bool = false) { + let region = MKCoordinateRegion(center: coordinate, span: span) + self.ignoreRegionChanges = true + if offset == CGPoint() { + self.mapView?.setRegion(region, animated: animated) + } else { + let mapRect = MKMapRect(region: region) + self.mapView?.setVisibleMapRect(mapRect, edgePadding: UIEdgeInsets(top: offset.y, left: offset.x, bottom: 0.0, right: 0.0), animated: animated) + } + self.ignoreRegionChanges = false + + if isUserLocation { + if !self.returnedToUserLocation { + self.returnedToUserLocation = true + self.pickerAnnotationView?.setRaised(true, animated: true) + } + } else if self.hasPickerAnnotation, let customUserLocationAnnotationView = self.customUserLocationAnnotationView, customUserLocationAnnotationView.isHidden, hidePicker { + self.pickerAnnotationContainerView.isHidden = true + customUserLocationAnnotationView.setSelected(false, animated: false) + customUserLocationAnnotationView.isHidden = false + customUserLocationAnnotationView.animateAppearance() + } + } + + func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { + guard !self.ignoreRegionChanges, let scrollView = mapView.subviews.first, let gestureRecognizers = scrollView.gestureRecognizers else { + return + } + + for gestureRecognizer in gestureRecognizers { + if gestureRecognizer.state == .began || gestureRecognizer.state == .ended { + self.isDragging = true + self.returnedToUserLocation = false + self.beganInteractiveDragging?() + + self.switchToPicking(raise: true, animated: true) + break + } + } + } + + func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { + let wasDragging = self.isDragging + if self.isDragging { + self.isDragging = false + if let coordinate = self.mapCenterCoordinate { + self.endedInteractiveDragging?(coordinate) + } + } + + if let pickerAnnotationView = self.pickerAnnotationView { + if pickerAnnotationView.isRaised && (wasDragging || self.returnedToUserLocation) { + self.schedulePin(wasDragging: wasDragging) + } + } + } + + func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) { + guard let location = userLocation.location else { + return + } + userLocation.title = "" + self.locationPromise.set(.single(location)) + } + + func mapView(_ mapView: MKMapView, didFailToLocateUserWithError error: Error) { + self.locationPromise.set(.single(nil)) + } + + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + if annotation === mapView.userLocation { + return nil + } + + if let annotation = annotation as? LocationPinAnnotation { + var view = mapView.dequeueReusableAnnotationView(withIdentifier: locationPinReuseIdentifier) + if view == nil { + view = LocationPinAnnotationView(annotation: annotation) + } + return view + } + + return nil + } + + func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) { + for view in views { + if view.annotation is MKUserLocation { + self.userLocationAnnotationView = view + if let annotationView = self.customUserLocationAnnotationView { + view.addSubview(annotationView) + } + } else if let view = view as? LocationPinAnnotationView { + view.setZPosition(view.defaultZPosition) + } + } + } + + func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { + guard let annotation = view.annotation as? LocationPinAnnotation else { + return + } + + if let view = view as? LocationPinAnnotationView { + view.setZPosition(nil) + } + + self.annotationSelected?(annotation) + + if let annotationView = self.customUserLocationAnnotationView, annotationView.isSelected { + annotationView.setSelected(false, animated: true) + } + } + + func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) { + if let view = view as? LocationPinAnnotationView { + Queue.mainQueue().after(0.2) { + view.setZPosition(view.defaultZPosition) + } + } + + Queue.mainQueue().after(0.05) { + if mapView.selectedAnnotations.isEmpty { + if !self.isDragging { + self.annotationSelected?(nil) + } + if let annotationView = self.customUserLocationAnnotationView, !annotationView.isSelected { + annotationView.setSelected(true, animated: true) + } + } + } + } + + var userLocation: Signal { + return self.locationPromise.get() + } + + var mapCenterCoordinate: CLLocationCoordinate2D? { + guard let mapView = self.mapView else { + return nil + } + return mapView.convert(CGPoint(x: (mapView.frame.width + pinOffset.x) / 2.0, y: (mapView.frame.height + pinOffset.y) / 2.0), toCoordinateFrom: mapView) + } + + func resetAnnotationSelection() { + guard let mapView = self.mapView else { + return + } + for annotation in mapView.selectedAnnotations { + mapView.deselectAnnotation(annotation, animated: true) + } + } + + var pickerAnnotationView: LocationPinAnnotationView? = nil + var hasPickerAnnotation: Bool = false { + didSet { + if self.hasPickerAnnotation, let annotation = self.userLocationAnnotation { + let pickerAnnotationView = LocationPinAnnotationView(annotation: annotation) + pickerAnnotationView.center = CGPoint(x: self.pickerAnnotationContainerView.frame.width / 2.0, y: self.pickerAnnotationContainerView.frame.height / 2.0 + 16.0) + self.pickerAnnotationContainerView.addSubview(pickerAnnotationView) + self.pickerAnnotationView = pickerAnnotationView + } else { + self.pickerAnnotationView?.removeFromSuperview() + self.pickerAnnotationView = nil + } + } + } + + func switchToPicking(raise: Bool = false, animated: Bool) { + guard self.hasPickerAnnotation else { + return + } + + self.customUserLocationAnnotationView?.isHidden = true + self.pickerAnnotationContainerView.isHidden = false + if let pickerAnnotationView = self.pickerAnnotationView, !pickerAnnotationView.isRaised { + pickerAnnotationView.setCustom(true, animated: animated) + if raise { + pickerAnnotationView.setRaised(true, animated: animated) + } + } + self.resetAnnotationSelection() + self.resetScheduledPin() + } + + var customUserLocationAnnotationView: LocationPinAnnotationView? = nil + var userLocationAnnotation: LocationPinAnnotation? = nil { + didSet { + if let annotation = self.userLocationAnnotation { + let annotationView = LocationPinAnnotationView(annotation: annotation) + annotationView.frame = annotationView.frame.offsetBy(dx: 21.0, dy: 22.0) + if let parentView = self.userLocationAnnotationView { + parentView.addSubview(annotationView) + } + self.customUserLocationAnnotationView = annotationView + + self.pickerAnnotationView?.annotation = annotation + } else { + self.customUserLocationAnnotationView?.removeFromSuperview() + self.customUserLocationAnnotationView = nil + } + } + } + + var annotations: [LocationPinAnnotation] = [] { + didSet { + guard let mapView = self.mapView else { + return + } + + var dict: [String: LocationPinAnnotation] = [:] + for annotation in self.annotations { + dict[annotation.id] = annotation + } + + var annotationsToRemove = Set() + for annotation in mapView.annotations { + guard let annotation = annotation as? LocationPinAnnotation else { + continue + } + + if let updatedAnnotation = dict[annotation.id] { + annotation.coordinate = updatedAnnotation.coordinate + dict[annotation.id] = nil + } else { + annotationsToRemove.insert(annotation) + } + } + + mapView.removeAnnotations(Array(annotationsToRemove)) + mapView.addAnnotations(Array(dict.values)) + } + } + + private func schedulePin(wasDragging: Bool) { + let timeout: Double = wasDragging ? 0.38 : 0.05 + + let signal: Signal = .complete() + |> delay(timeout, queue: Queue.mainQueue()) + self.pinDisposable.set(signal.start(completed: { [weak self] in + guard let strongSelf = self, let pickerAnnotationView = strongSelf.pickerAnnotationView else { + return + } + + pickerAnnotationView.setRaised(false, animated: true) { [weak self] in + guard let strongSelf = self else { + return + } + + if strongSelf.returnedToUserLocation { + strongSelf.pickerAnnotationContainerView.isHidden = true + strongSelf.customUserLocationAnnotationView?.isHidden = false + } + } + + if strongSelf.returnedToUserLocation { + pickerAnnotationView.setCustom(false, animated: true) + } + })) + } + + private func resetScheduledPin() { + self.pinDisposable.set(nil) + } + + func updateLayout(size: CGSize) { + self.pickerAnnotationContainerView.frame = CGRect(x: 0.0, y: floorToScreenPixels((size.height - size.width) / 2.0), width: size.width, height: size.width) + if let pickerAnnotationView = self.pickerAnnotationView { + pickerAnnotationView.center = CGPoint(x: self.pickerAnnotationContainerView.frame.width / 2.0, y: self.pickerAnnotationContainerView.frame.height / 2.0) + } + } +} diff --git a/submodules/LocationUI/Sources/LocationOptionsNode.swift b/submodules/LocationUI/Sources/LocationOptionsNode.swift new file mode 100644 index 0000000000..9b48138e96 --- /dev/null +++ b/submodules/LocationUI/Sources/LocationOptionsNode.swift @@ -0,0 +1,65 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import SegmentedControlNode + +final class LocationOptionsNode: ASDisplayNode { + private var presentationData: PresentationData + + private let backgroundNode: ASDisplayNode + private let separatorNode: ASDisplayNode + private let segmentedControlNode: SegmentedControlNode + + init(presentationData: PresentationData, updateMapMode: @escaping (LocationMapMode) -> Void) { + self.presentationData = presentationData + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor + + self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: self.presentationData.theme), items: [SegmentedControlItem(title: self.presentationData.strings.Map_Map), SegmentedControlItem(title: self.presentationData.strings.Map_Satellite), SegmentedControlItem(title: self.presentationData.strings.Map_Hybrid)], selectedIndex: 0) + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.segmentedControlNode) + + self.segmentedControlNode.selectedIndexChanged = { [weak self] index in + guard let strongSelf = self else { + return + } + switch index { + case 0: + updateMapMode(.map) + case 1: + updateMapMode(.sattelite) + case 2: + updateMapMode(.hybrid) + default: + break + } + } + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + self.backgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + self.separatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor + self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.presentationData.theme)) + } + + func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(node: self.separatorNode, frame: CGRect(x: 0.0, y: size.height, width: size.width, height: UIScreenPixel)) + + let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: size.width - 16.0), transition: .immediate) + self.segmentedControlNode.frame = CGRect(origin: CGPoint(x: floor((size.width - controlSize.width) / 2.0), y: 0.0), size: controlSize) + } +} diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift new file mode 100644 index 0000000000..1a150f9c18 --- /dev/null +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -0,0 +1,332 @@ +import Foundation +import UIKit +import Display +import LegacyComponents +import TelegramCore +import SyncCore +import Postbox +import SwiftSignalKit +import TelegramPresentationData +import AccountContext +import AppBundle +import CoreLocation +import PresentationDataUtils +import DeviceAccess + +public enum LocationPickerMode { + case share(peer: Peer?, selfPeer: Peer?, hasLiveLocation: Bool) + case pick +} + +class LocationPickerInteraction { + let sendLocation: (CLLocationCoordinate2D) -> Void + let sendLiveLocation: (CLLocationCoordinate2D) -> Void + let sendVenue: (TelegramMediaMap) -> Void + let toggleMapModeSelection: () -> Void + let updateMapMode: (LocationMapMode) -> Void + let goToUserLocation: () -> Void + let goToCoordinate: (CLLocationCoordinate2D) -> Void + let openSearch: () -> Void + let updateSearchQuery: (String) -> Void + let dismissSearch: () -> Void + let dismissInput: () -> Void + let updateSendActionHighlight: (Bool) -> Void + let openHomeWorkInfo: () -> Void + let showPlacesInThisArea: () -> Void + + init(sendLocation: @escaping (CLLocationCoordinate2D) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, openHomeWorkInfo: @escaping () -> Void, showPlacesInThisArea: @escaping ()-> Void) { + self.sendLocation = sendLocation + self.sendLiveLocation = sendLiveLocation + self.sendVenue = sendVenue + self.toggleMapModeSelection = toggleMapModeSelection + self.updateMapMode = updateMapMode + self.goToUserLocation = goToUserLocation + self.goToCoordinate = goToCoordinate + self.openSearch = openSearch + self.updateSearchQuery = updateSearchQuery + self.dismissSearch = dismissSearch + self.dismissInput = dismissInput + self.updateSendActionHighlight = updateSendActionHighlight + self.openHomeWorkInfo = openHomeWorkInfo + self.showPlacesInThisArea = showPlacesInThisArea + } +} + +public final class LocationPickerController: ViewController { + private var controllerNode: LocationPickerControllerNode { + return self.displayNode as! LocationPickerControllerNode + } + + private let context: AccountContext + private let mode: LocationPickerMode + private let completion: (TelegramMediaMap, String?) -> Void + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private var searchNavigationContentNode: LocationSearchNavigationContentNode? + private var isSearchingDisposable = MetaDisposable() + + private let locationManager = LocationManager() + private var permissionDisposable: Disposable? + + private var interaction: LocationPickerInteraction? + + public init(context: AccountContext, mode: LocationPickerMode, completion: @escaping (TelegramMediaMap, String?) -> Void) { + self.context = context + self.mode = mode + self.completion = completion + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme).withUpdatedSeparatorColor(.clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) + + self.navigationPresentation = .modal + + self.title = self.presentationData.strings.Map_ChooseLocationTitle + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.searchPressed)) + self.navigationItem.rightBarButtonItem?.accessibilityLabel = self.presentationData.strings.Common_Search + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + guard let strongSelf = self, strongSelf.presentationData.theme !== presentationData.theme else { + return + } + strongSelf.presentationData = presentationData + + strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: strongSelf.presentationData.theme).withUpdatedSeparatorColor(.clear), strings: NavigationBarStrings(presentationStrings: strongSelf.presentationData.strings))) + strongSelf.searchNavigationContentNode?.updatePresentationData(strongSelf.presentationData) + strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(strongSelf.presentationData.theme), style: .plain, target: strongSelf, action: #selector(strongSelf.searchPressed)) + + if strongSelf.isNodeLoaded { + strongSelf.controllerNode.updatePresentationData(presentationData) + } + }) + + let locationWithTimeout: (CLLocationCoordinate2D, Int32?) -> TelegramMediaMap = { coordinate, timeout in + return TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: timeout) + } + + self.interaction = LocationPickerInteraction(sendLocation: { [weak self] coordinate in + guard let strongSelf = self else { + return + } + strongSelf.completion(locationWithTimeout(coordinate, nil), nil) + strongSelf.dismiss() + }, sendLiveLocation: { [weak self] coordinate in + guard let strongSelf = self else { + return + } + DeviceAccess.authorizeAccess(to: .location(.live), locationManager: strongSelf.locationManager, presentationData: strongSelf.presentationData, present: { c, a in + strongSelf.present(c, in: .window(.root), with: a) + }, openSettings: { + strongSelf.context.sharedContext.applicationBindings.openSettings() + }) { [weak self] authorized in + guard let strongSelf = self, authorized else { + return + } + let controller = ActionSheetController(presentationData: strongSelf.presentationData) + var title = strongSelf.presentationData.strings.Map_LiveLocationGroupDescription + if case let .share(peer, _, _) = strongSelf.mode, let receiver = peer as? TelegramUser { + title = strongSelf.presentationData.strings.Map_LiveLocationPrivateDescription(receiver.compactDisplayTitle).0 + } + controller.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: title), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor15Minutes, color: .accent, action: { [weak self, weak controller] in + controller?.dismissAnimated() + if let strongSelf = self { + strongSelf.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 15 * 60), nil) + strongSelf.dismiss() + } + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor1Hour, color: .accent, action: { [weak self, weak controller] in + controller?.dismissAnimated() + if let strongSelf = self { + strongSelf.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 60 * 60 - 1), nil) + strongSelf.dismiss() + } + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor8Hours, color: .accent, action: { [weak self, weak controller] in + controller?.dismissAnimated() + if let strongSelf = self { + strongSelf.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 8 * 60 * 60), nil) + strongSelf.dismiss() + } + }) + ]), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak controller] in + controller?.dismissAnimated() + }) + ]) + ]) + strongSelf.present(controller, in: .window(.root)) + } + }, sendVenue: { [weak self] venue in + guard let strongSelf = self else { + return + } + let venueType = venue.venue?.type ?? "" + if ["home", "work"].contains(venueType) { + completion(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil), nil) + } else { + completion(venue, nil) + } + strongSelf.dismiss() + }, toggleMapModeSelection: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.updateState { state in + var state = state + state.displayingMapModeOptions = !state.displayingMapModeOptions + return state + } + }, updateMapMode: { [weak self] mode in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.updateState { state in + var state = state + state.mapMode = mode + state.displayingMapModeOptions = false + return state + } + }, goToUserLocation: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.goToUserLocation() + }, goToCoordinate: { [weak self] coordinate in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.updateState { state in + var state = state + state.displayingMapModeOptions = false + state.selectedLocation = .location(coordinate, nil) + state.searchingVenuesAround = false + return state + } + }, openSearch: { [weak self] in + guard let strongSelf = self, let interaction = strongSelf.interaction, let navigationBar = strongSelf.navigationBar else { + return + } + strongSelf.controllerNode.updateState { state in + var state = state + state.displayingMapModeOptions = false + return state + } + let contentNode = LocationSearchNavigationContentNode(presentationData: strongSelf.presentationData, interaction: interaction) + strongSelf.searchNavigationContentNode = contentNode + navigationBar.setContentNode(contentNode, animated: true) + let isSearching = strongSelf.controllerNode.activateSearch(navigationBar: navigationBar) + contentNode.activate() + + strongSelf.isSearchingDisposable.set((isSearching + |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self, let searchNavigationContentNode = strongSelf.searchNavigationContentNode { + searchNavigationContentNode.updateActivity(value) + } + })) + }, updateSearchQuery: { [weak self] query in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.searchContainerNode?.searchTextUpdated(text: query) + }, dismissSearch: { [weak self] in + guard let strongSelf = self, let navigationBar = strongSelf.navigationBar else { + return + } + strongSelf.isSearchingDisposable.set(nil) + strongSelf.searchNavigationContentNode?.deactivate() + strongSelf.searchNavigationContentNode = nil + navigationBar.setContentNode(nil, animated: true) + strongSelf.controllerNode.deactivateSearch() + }, dismissInput: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.searchNavigationContentNode?.deactivate() + }, updateSendActionHighlight: { [weak self] highlighted in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.updateSendActionHighlight(highlighted) + }, openHomeWorkInfo: { [weak self] in + guard let strongSelf = self else { + return + } + let controller = textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.Map_HomeAndWorkTitle, text: strongSelf.presentationData.strings.Map_HomeAndWorkInfo, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]) + strongSelf.present(controller, in: .window(.root)) + }, showPlacesInThisArea: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.requestPlacesAtSelectedLocation() + }) + + self.scrollToTop = { [weak self] in + if let strongSelf = self { + strongSelf.controllerNode.scrollToTop() + } + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + self.permissionDisposable?.dispose() + self.isSearchingDisposable.dispose() + } + + override public func loadDisplayNode() { + super.loadDisplayNode() + guard let interaction = self.interaction else { + return + } + + self.displayNode = LocationPickerControllerNode(context: self.context, presentationData: self.presentationData, mode: self.mode, interaction: interaction) + self.displayNodeDidLoad() + + self.permissionDisposable = (DeviceAccess.authorizationStatus(subject: .location(.send)) + |> deliverOnMainQueue).start(next: { [weak self] next in + guard let strongSelf = self else { + return + } + switch next { + case .notDetermined: + DeviceAccess.authorizeAccess(to: .location(.send), locationManager: strongSelf.locationManager, presentationData: strongSelf.presentationData, present: { c, a in + strongSelf.present(c, in: .window(.root), with: a) + }, openSettings: { + strongSelf.context.sharedContext.applicationBindings.openSettings() + }) + case .denied: + strongSelf.controllerNode.updateState { state in + var state = state + state.forceSelection = true + return state + } + default: + break + } + }) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition) + } + + @objc private func cancelPressed() { + self.dismiss() + } + + @objc private func searchPressed() { + self.interaction?.openSearch() + } +} diff --git a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift new file mode 100644 index 0000000000..64a1d42783 --- /dev/null +++ b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift @@ -0,0 +1,901 @@ +import Foundation +import UIKit +import Display +import LegacyComponents +import TelegramCore +import SyncCore +import Postbox +import SwiftSignalKit +import MergeLists +import ItemListUI +import ItemListVenueItem +import ActivityIndicator +import TelegramPresentationData +import AccountContext +import AppBundle +import CoreLocation +import Geocoding +import PhoneNumberFormat + +private struct LocationPickerTransaction { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let isLoading: Bool + let isEmpty: Bool + let crossFade: Bool +} + +private enum LocationPickerEntryId: Hashable { + case location + case liveLocation + case header + case venue(String) + case attribution +} + +private enum LocationPickerEntry: Comparable, Identifiable { + case location(PresentationTheme, String, String, TelegramMediaMap?, CLLocationCoordinate2D?) + case liveLocation(PresentationTheme, String, String, CLLocationCoordinate2D?) + case header(PresentationTheme, String) + case venue(PresentationTheme, TelegramMediaMap, Int) + case attribution(PresentationTheme) + + var stableId: LocationPickerEntryId { + switch self { + case .location: + return .location + case .liveLocation: + return .liveLocation + case .header: + return .header + case let .venue(_, venue, _): + return .venue(venue.venue?.id ?? "") + case .attribution: + return .attribution + } + } + + static func ==(lhs: LocationPickerEntry, rhs: LocationPickerEntry) -> Bool { + switch lhs { + case let .location(lhsTheme, lhsTitle, lhsSubtitle, lhsVenue, lhsCoordinate): + if case let .location(rhsTheme, rhsTitle, rhsSubtitle, rhsVenue, rhsCoordinate) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsVenue?.venue?.id == rhsVenue?.venue?.id, lhsCoordinate == rhsCoordinate { + return true + } else { + return false + } + case let .liveLocation(lhsTheme, lhsTitle, lhsSubtitle, lhsCoordinate): + if case let .liveLocation(rhsTheme, rhsTitle, rhsSubtitle, rhsCoordinate) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsCoordinate == rhsCoordinate { + return true + } else { + return false + } + case let .header(lhsTheme, lhsTitle): + if case let .header(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle { + return true + } else { + return false + } + case let .venue(lhsTheme, lhsVenue, lhsIndex): + if case let .venue(rhsTheme, rhsVenue, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsVenue.venue?.id == rhsVenue.venue?.id, lhsIndex == rhsIndex { + return true + } else { + return false + } + case let .attribution(lhsTheme): + if case let .attribution(rhsTheme) = rhs, lhsTheme === rhsTheme { + return true + } else { + return false + } + } + } + + static func <(lhs: LocationPickerEntry, rhs: LocationPickerEntry) -> Bool { + switch lhs { + case .location: + switch rhs { + case .location: + return false + case .liveLocation, .header, .venue, .attribution: + return true + } + case .liveLocation: + switch rhs { + case .location, .liveLocation: + return false + case .header, .venue, .attribution: + return true + } + case .header: + switch rhs { + case .location, .liveLocation, .header: + return false + case .venue, .attribution: + return true + } + case let .venue(_, _, lhsIndex): + switch rhs { + case .location, .liveLocation, .header: + return false + case let .venue(_, _, rhsIndex): + return lhsIndex < rhsIndex + case .attribution: + return true + } + case .attribution: + return false + } + } + + func item(account: Account, presentationData: PresentationData, interaction: LocationPickerInteraction?) -> ListViewItem { + switch self { + case let .location(theme, title, subtitle, venue, coordinate): + let icon: LocationActionListItemIcon + if let venue = venue { + icon = .venue(venue) + } else { + icon = .location + } + return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: account, title: title, subtitle: subtitle, icon: icon, action: { + if let venue = venue { + interaction?.sendVenue(venue) + } else if let coordinate = coordinate { + interaction?.sendLocation(coordinate) + } + }, highlighted: { highlighted in + interaction?.updateSendActionHighlight(highlighted) + }) + case let .liveLocation(theme, title, subtitle, coordinate): + return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: account, title: title, subtitle: subtitle, icon: .liveLocation, action: { + if let coordinate = coordinate { + interaction?.sendLiveLocation(coordinate) + } + }) + case let .header(theme, title): + return LocationSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title) + case let .venue(theme, venue, _): + let venueType = venue.venue?.type ?? "" + return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), account: account, venue: venue, style: .plain, action: { + interaction?.sendVenue(venue) + }, infoAction: ["home", "work"].contains(venueType) ? { + interaction?.openHomeWorkInfo() + } : nil) + case let .attribution(theme): + return LocationAttributionItem(presentationData: ItemListPresentationData(presentationData)) + } + } +} + +private func preparedTransition(from fromEntries: [LocationPickerEntry], to toEntries: [LocationPickerEntry], isLoading: Bool, isEmpty: Bool, crossFade: Bool, account: Account, presentationData: PresentationData, interaction: LocationPickerInteraction?) -> LocationPickerTransaction { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, interaction: interaction), directionHint: nil) } + + return LocationPickerTransaction(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, crossFade: crossFade) +} + +enum LocationPickerLocation: Equatable { + case none + case selecting + case location(CLLocationCoordinate2D, String?) + case venue(TelegramMediaMap) + + var isCustom: Bool { + switch self { + case .selecting, .location: + return true + default: + return false + } + } + + public static func ==(lhs: LocationPickerLocation, rhs: LocationPickerLocation) -> Bool { + switch lhs { + case .none: + if case .none = rhs { + return true + } else { + return false + } + case .selecting: + if case .selecting = rhs { + return true + } else { + return false + } + case let .location(lhsCoordinate, lhsAddress): + if case let .location(rhsCoordinate, rhsAddress) = rhs, lhsCoordinate == rhsCoordinate, lhsAddress == rhsAddress { + return true + } else { + return false + } + case let .venue(lhsVenue): + if case let .venue(rhsVenue) = rhs, lhsVenue.venue?.id == rhsVenue.venue?.id { + return true + } else { + return false + } + + } + } +} + +struct LocationPickerState { + var mapMode: LocationMapMode + var displayingMapModeOptions: Bool + var selectedLocation: LocationPickerLocation + var forceSelection: Bool + var searchingVenuesAround: Bool + + init() { + self.mapMode = .map + self.displayingMapModeOptions = false + self.selectedLocation = .none + self.forceSelection = false + self.searchingVenuesAround = false + } +} + +final class LocationPickerControllerNode: ViewControllerTracingNode { + private let context: AccountContext + private var presentationData: PresentationData + private let presentationDataPromise: Promise + private let mode: LocationPickerMode + private let interaction: LocationPickerInteraction + + private let listNode: ListView + private let emptyResultsTextNode: ImmediateTextNode + private let headerNode: LocationMapHeaderNode + private let activityIndicator: ActivityIndicator + private let shadeNode: ASDisplayNode + private let innerShadeNode: ASDisplayNode + + private let optionsNode: LocationOptionsNode + private(set) var searchContainerNode: LocationSearchContainerNode? + + private var enqueuedTransitions: [LocationPickerTransaction] = [] + + private var disposable: Disposable? + private var state: LocationPickerState + private let statePromise: Promise + private var geocodingDisposable = MetaDisposable() + + private let searchVenuesPromise = Promise() + + private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? + private var listOffset: CGFloat? + + init(context: AccountContext, presentationData: PresentationData, mode: LocationPickerMode, interaction: LocationPickerInteraction) { + self.context = context + self.presentationData = presentationData + self.presentationDataPromise = Promise(presentationData) + self.mode = mode + self.interaction = interaction + + self.state = LocationPickerState() + self.statePromise = Promise(self.state) + + self.listNode = ListView() + self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3) + self.listNode.verticalScrollIndicatorFollowsOverscroll = true + + self.emptyResultsTextNode = ImmediateTextNode() + self.emptyResultsTextNode.maximumNumberOfLines = 0 + self.emptyResultsTextNode.textAlignment = .center + self.emptyResultsTextNode.isHidden = true + + self.headerNode = LocationMapHeaderNode(presentationData: presentationData, toggleMapModeSelection: interaction.toggleMapModeSelection, goToUserLocation: interaction.goToUserLocation, showPlacesInThisArea: interaction.showPlacesInThisArea) + self.headerNode.mapNode.isRotateEnabled = false + + self.optionsNode = LocationOptionsNode(presentationData: presentationData, updateMapMode: interaction.updateMapMode) + + self.activityIndicator = ActivityIndicator(type: .custom(self.presentationData.theme.list.itemSecondaryTextColor, 22.0, 1.0, false)) + + self.shadeNode = ASDisplayNode() + self.shadeNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.shadeNode.alpha = 0.0 + self.innerShadeNode = ASDisplayNode() + self.innerShadeNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + super.init() + + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + self.addSubnode(self.listNode) + self.addSubnode(self.headerNode) + self.addSubnode(self.optionsNode) + self.listNode.addSubnode(self.activityIndicator) + self.listNode.addSubnode(self.emptyResultsTextNode) + self.shadeNode.addSubnode(self.innerShadeNode) + self.addSubnode(self.shadeNode) + + let userLocation: Signal = self.headerNode.mapNode.userLocation + + let personalAddresses = self.context.account.postbox.peerView(id: self.context.account.peerId) + |> mapToSignal { view -> Signal<(DeviceContactAddressData?, DeviceContactAddressData?)?, NoError> in + if let user = peerViewMainPeer(view) as? TelegramUser, let phoneNumber = user.phone { + return ((context.sharedContext.contactDataManager?.basicData() ?? .single([:])) |> take(1)) + |> mapToSignal { basicData -> Signal in + var stableId: String? + let queryPhoneNumber = formatPhoneNumber(phoneNumber) + outer: for (id, data) in basicData { + for phoneNumber in data.phoneNumbers { + if formatPhoneNumber(phoneNumber.value) == queryPhoneNumber { + stableId = id + break outer + } + } + } + if let stableId = stableId { + return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil)) + |> take(1) + |> map { extendedData -> DeviceContactExtendedData? in + return extendedData + } + } else { + return .single(nil) + } + } + |> map { extendedData -> (DeviceContactAddressData?, DeviceContactAddressData?)? in + if let extendedData = extendedData { + var homeAddress: DeviceContactAddressData? + var workAddress: DeviceContactAddressData? + for address in extendedData.addresses { + if address.label == "_$!!$_" { + homeAddress = address + } else if address.label == "_$!!$_" { + workAddress = address + } + } + return (homeAddress, workAddress) + } else { + return nil + } + } + } else { + return .single(nil) + } + } + + let personalVenues: Signal<[TelegramMediaMap]?, NoError> = .single(nil) + |> then( + personalAddresses + |> mapToSignal { homeAndWorkAddresses -> Signal<[TelegramMediaMap]?, NoError> in + if let (homeAddress, workAddress) = homeAndWorkAddresses { + let home: Signal<(Double, Double)?, NoError> + let work: Signal<(Double, Double)?, NoError> + if let address = homeAddress { + home = geocodeAddress(postbox: context.account.postbox, address: address) + } else { + home = .single(nil) + } + if let address = workAddress { + work = geocodeAddress(postbox: context.account.postbox, address: address) + } else { + work = .single(nil) + } + return combineLatest(home, work) + |> map { homeCoordinate, workCoordinate -> [TelegramMediaMap]? in + var venues: [TelegramMediaMap] = [] + if let (latitude, longitude) = homeCoordinate, let address = homeAddress { + venues.append(TelegramMediaMap(latitude: latitude, longitude: longitude, geoPlace: nil, venue: MapVenue(title: presentationData.strings.Map_Home, address: address.displayString, provider: nil, id: "home", type: "home"), liveBroadcastingTimeout: nil)) + } + if let (latitude, longitude) = workCoordinate, let address = workAddress { + venues.append(TelegramMediaMap(latitude: latitude, longitude: longitude, geoPlace: nil, venue: MapVenue(title: presentationData.strings.Map_Work, address: address.displayString, provider: nil, id: "work", type: "work"), liveBroadcastingTimeout: nil)) + } + return venues + } + } else { + return .single(nil) + } + } + ) + + let venues: Signal<[TelegramMediaMap]?, NoError> = .single(nil) + |> then( + throttledUserLocation(userLocation) + |> mapToSignal { location -> Signal<[TelegramMediaMap]?, NoError> in + if let location = location, location.horizontalAccuracy > 0 { + return combineLatest(nearbyVenues(account: context.account, latitude: location.coordinate.latitude, longitude: location.coordinate.longitude), personalVenues) + |> map { nearbyVenues, personalVenues -> [TelegramMediaMap]? in + var resultVenues: [TelegramMediaMap] = [] + if let personalVenues = personalVenues { + for venue in personalVenues { + let venueLocation = CLLocation(latitude: venue.latitude, longitude: venue.longitude) + if venueLocation.distance(from: location) <= 1000 { + resultVenues.append(venue) + } + } + } + resultVenues.append(contentsOf: nearbyVenues) + return resultVenues + } + } else { + return .single(nil) + } + } + ) + + let foundVenues: Signal<([TelegramMediaMap], CLLocation)?, NoError> = .single(nil) + |> then( + self.searchVenuesPromise.get() + |> distinctUntilChanged + |> mapToSignal { coordinate -> Signal<([TelegramMediaMap], CLLocation)?, NoError> in + if let coordinate = coordinate { + return (.single(nil) + |> then( + nearbyVenues(account: context.account, latitude: coordinate.latitude, longitude: coordinate.longitude) + |> map { venues -> ([TelegramMediaMap], CLLocation)? in + return (venues, CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)) + } + )) + } else { + return .single(nil) + } + } + ) + + let previousState = Atomic(value: self.state) + let previousUserLocation = Atomic(value: nil) + let previousAnnotations = Atomic<[LocationPinAnnotation]>(value: []) + let previousEntries = Atomic<[LocationPickerEntry]?>(value: nil) + + self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), userLocation, venues, foundVenues) + |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, userLocation, venues, foundVenuesAndLocation in + if let strongSelf = self { + let (foundVenues, foundVenuesLocation) = foundVenuesAndLocation ?? (nil, nil) + + var entries: [LocationPickerEntry] = [] + switch state.selectedLocation { + case let .location(coordinate, address): + let title: String + switch strongSelf.mode { + case .share: + title = presentationData.strings.Map_SendThisLocation + case .pick: + title = presentationData.strings.Map_SetThisLocation + } + entries.append(.location(presentationData.theme, title, address ?? presentationData.strings.Map_Locating, nil, coordinate)) + case .selecting: + let title: String + switch strongSelf.mode { + case .share: + title = presentationData.strings.Map_SendThisLocation + case .pick: + title = presentationData.strings.Map_SetThisLocation + } + entries.append(.location(presentationData.theme, title, presentationData.strings.Map_Locating, nil, nil)) + case let .venue(venue): + let title: String + switch strongSelf.mode { + case .share: + title = presentationData.strings.Map_SendThisPlace + case .pick: + title = presentationData.strings.Map_SetThisPlace + } + entries.append(.location(presentationData.theme, title, venue.venue?.title ?? "", venue, venue.coordinate)) + case .none: + let title: String + switch strongSelf.mode { + case .share: + title = presentationData.strings.Map_SendMyCurrentLocation + case .pick: + title = presentationData.strings.Map_SetThisLocation + } + entries.append(.location(presentationData.theme, title, (userLocation?.horizontalAccuracy).flatMap { presentationData.strings.Map_AccurateTo(stringForDistance(strings: presentationData.strings, distance: $0)).0 } ?? presentationData.strings.Map_Locating, nil, userLocation?.coordinate)) + } + + if case .share(_, _, true) = mode { + entries.append(.liveLocation(presentationData.theme, presentationData.strings.Map_ShareLiveLocation, presentationData.strings.Map_ShareLiveLocationHelp, userLocation?.coordinate)) + } + + entries.append(.header(presentationData.theme, presentationData.strings.Map_ChooseAPlace.uppercased())) + + var displayedVenues = foundVenues != nil || state.searchingVenuesAround ? foundVenues : venues + if let venues = displayedVenues { + var index: Int = 0 + for venue in venues { + entries.append(.venue(presentationData.theme, venue, index)) + index += 1 + } + if !venues.isEmpty { + entries.append(.attribution(presentationData.theme)) + } + } + let previousEntries = previousEntries.swap(entries) + let previousState = previousState.swap(state) + + var crossFade = false + if previousEntries?.count != entries.count || previousState.selectedLocation != state.selectedLocation { + crossFade = true + } + + let transition = preparedTransition(from: previousEntries ?? [], to: entries, isLoading: displayedVenues == nil, isEmpty: displayedVenues?.isEmpty ?? false, crossFade: crossFade, account: context.account, presentationData: presentationData, interaction: strongSelf.interaction) + strongSelf.enqueueTransition(transition) + + var displayingPlacesButton = false + let previousUserLocation = previousUserLocation.swap(userLocation) + switch state.selectedLocation { + case .none: + if let userLocation = userLocation { + strongSelf.headerNode.mapNode.setMapCenter(coordinate: userLocation.coordinate, isUserLocation: true, animated: previousUserLocation != nil) + } + strongSelf.headerNode.mapNode.resetAnnotationSelection() + case .selecting: + strongSelf.headerNode.mapNode.resetAnnotationSelection() + case let .location(coordinate, address): + var updateMap = false + switch previousState.selectedLocation { + case .none, .venue: + updateMap = true + case let .location(previousCoordinate, address): + if previousCoordinate != coordinate { + updateMap = true + } + default: + break + } + if updateMap { + strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, isUserLocation: false, hidePicker: false, animated: true) + strongSelf.headerNode.mapNode.switchToPicking(animated: false) + } + + if address != nil { + if foundVenues == nil && !state.searchingVenuesAround { + displayingPlacesButton = true + } else if let previousLocation = foundVenuesLocation { + let currentLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) + if currentLocation.distance(from: previousLocation) > 300 { + displayingPlacesButton = true + } + } + } + case let .venue(venue): + strongSelf.headerNode.mapNode.setMapCenter(coordinate: venue.coordinate, hidePicker: true, animated: true) + } + + strongSelf.headerNode.updateState(mapMode: state.mapMode, displayingMapModeOptions: state.displayingMapModeOptions, displayingPlacesButton: displayingPlacesButton, animated: true) + + let annotations: [LocationPinAnnotation] + if let venues = displayedVenues { + annotations = venues.compactMap { LocationPinAnnotation(context: context, theme: presentationData.theme, location: $0) } + } else { + annotations = [] + } + let previousAnnotations = previousAnnotations.swap(annotations) + if annotations != previousAnnotations { + strongSelf.headerNode.mapNode.annotations = annotations + } + + if let (layout, navigationBarHeight) = strongSelf.validLayout { + var updateLayout = false + var transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring) + + if previousState.displayingMapModeOptions != state.displayingMapModeOptions { + updateLayout = true + } else if previousState.selectedLocation.isCustom != state.selectedLocation.isCustom { + updateLayout = true + } else if previousState.searchingVenuesAround != state.searchingVenuesAround { + updateLayout = true + } + + if updateLayout { + strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationBarHeight, transition: transition) + } + } + + if case let .location(coordinate, address) = state.selectedLocation, address == nil { + strongSelf.geocodingDisposable.set((reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) + |> deliverOnMainQueue).start(next: { [weak self] placemark in + if let strongSelf = self { + var address = placemark?.fullAddress ?? "" + if address.isEmpty { + address = presentationData.strings.Map_Unknown + } + strongSelf.updateState { state in + var state = state + state.selectedLocation = .location(coordinate, address) + return state + } + } + })) + } else { + strongSelf.geocodingDisposable.set(nil) + } + } + }) + + if case let .share(_, selfPeer, _) = self.mode, let peer = selfPeer { + self.headerNode.mapNode.userLocationAnnotation = LocationPinAnnotation(context: context, theme: self.presentationData.theme, peer: peer) + self.headerNode.mapNode.hasPickerAnnotation = true + } + + self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in + guard let strongSelf = self, let (layout, navigationBarHeight) = strongSelf.validLayout, strongSelf.listNode.scrollEnabled else { + return + } + let overlap: CGFloat = 6.0 + strongSelf.listOffset = max(0.0, offset) + let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(0.0, offset + overlap))) + listTransition.updateFrame(node: strongSelf.headerNode, frame: headerFrame) + strongSelf.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, offset: 0.0, size: headerFrame.size, transition: listTransition) + strongSelf.layoutActivityIndicator(transition: listTransition) + } + + self.listNode.beganInteractiveDragging = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateState { state in + var state = state + state.displayingMapModeOptions = false + return state + } + } + + self.headerNode.mapNode.beganInteractiveDragging = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateState { state in + var state = state + state.displayingMapModeOptions = false + state.selectedLocation = .selecting + state.searchingVenuesAround = false + return state + } + } + + self.headerNode.mapNode.endedInteractiveDragging = { [weak self] coordinate in + guard let strongSelf = self else { + return + } + strongSelf.updateState { state in + var state = state + if case .selecting = state.selectedLocation { + state.selectedLocation = .location(coordinate, nil) + state.searchingVenuesAround = false + } + return state + } + } + + self.headerNode.mapNode.annotationSelected = { [weak self] annotation in + guard let strongSelf = self else { + return + } + strongSelf.updateState { state in + var state = state + state.displayingMapModeOptions = false + state.selectedLocation = annotation?.location.flatMap { .venue($0) } ?? .none + if annotation == nil { + state.searchingVenuesAround = false + } + return state + } + } + + self.headerNode.mapNode.userLocationAnnotationSelected = { [weak self] in + if let strongSelf = self { + strongSelf.goToUserLocation() + } + } + } + + deinit { + self.disposable?.dispose() + self.geocodingDisposable.dispose() + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + self.presentationDataPromise.set(.single(presentationData)) + + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.headerNode.updatePresentationData(self.presentationData) + self.optionsNode.updatePresentationData(self.presentationData) + self.shadeNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.innerShadeNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.searchContainerNode?.updatePresentationData(self.presentationData) + } + + func updateState(_ f: (LocationPickerState) -> LocationPickerState) { + self.state = f(self.state) + self.statePromise.set(.single(self.state)) + } + + private func enqueueTransition(_ transition: LocationPickerTransaction) { + self.enqueuedTransitions.append(transition) + + if let _ = self.validLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + guard let layout = self.validLayout, let transition = self.enqueuedTransitions.first else { + return + } + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + if transition.crossFade { + options.insert(.AnimateCrossfade) + } + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.activityIndicator.isHidden = !transition.isLoading + strongSelf.emptyResultsTextNode.isHidden = transition.isLoading || !transition.isEmpty + + strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.Map_NoPlacesNearby, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + + strongSelf.layoutActivityIndicator(transition: .immediate) + } + }) + } + + func activateSearch(navigationBar: NavigationBar) -> Signal { + guard let (layout, navigationBarHeight) = self.validLayout, self.searchContainerNode == nil, let coordinate = self.headerNode.mapNode.mapCenterCoordinate else { + return .complete() + } + + let searchContainerNode = LocationSearchContainerNode(context: self.context, coordinate: coordinate, interaction: self.interaction) + self.insertSubnode(searchContainerNode, belowSubnode: navigationBar) + self.searchContainerNode = searchContainerNode + + searchContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + + self.containerLayoutUpdated(layout, navigationHeight: navigationBarHeight, transition: .immediate) + + return searchContainerNode.isSearching + } + + func deactivateSearch() { + guard let searchContainerNode = self.searchContainerNode else { + return + } + searchContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak searchContainerNode] _ in + searchContainerNode?.removeFromSupernode() + }) + self.searchContainerNode = nil + } + + func scrollToTop() { + if let searchContainerNode = self.searchContainerNode { + searchContainerNode.scrollToTop() + } else { + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } + } + + private func layoutActivityIndicator(transition: ContainedViewLayoutTransition) { + guard let (layout, navigationHeight) = self.validLayout else { + return + } + + let topInset: CGFloat = floor((layout.size.height - navigationHeight) / 2.0 + navigationHeight) + let headerHeight: CGFloat + if let listOffset = self.listOffset { + headerHeight = max(0.0, listOffset) + } else { + headerHeight = topInset + } + + let indicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0)) + let actionsInset: CGFloat = 148.0 + transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: headerHeight + actionsInset + floor((layout.size.height - headerHeight - actionsInset - indicatorSize.height - layout.intrinsicInsets.bottom) / 2.0)), size: indicatorSize)) + + let padding: CGFloat = 16.0 + let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) + transition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - emptyTextSize.width) / 2.0), y: headerHeight + actionsInset + floor((layout.size.height - headerHeight - actionsInset - emptyTextSize.height - layout.intrinsicInsets.bottom) / 2.0)), size: emptyTextSize)) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let isFirstLayout = self.validLayout == nil + self.validLayout = (layout, navigationHeight) + + let isPickingLocation = (self.state.selectedLocation.isCustom || self.state.forceSelection) && !self.state.searchingVenuesAround + let optionsHeight: CGFloat = 38.0 + var actionHeight: CGFloat? + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? LocationActionListItemNode { + if actionHeight == nil { + actionHeight = itemNode.frame.height + } + } + } + + let topInset: CGFloat = floor((layout.size.height - navigationHeight) / 2.0 + navigationHeight) + let overlap: CGFloat = 6.0 + let headerHeight: CGFloat + if isPickingLocation, let actionHeight = actionHeight { + self.listOffset = topInset + headerHeight = layout.size.height - actionHeight - layout.intrinsicInsets.bottom + overlap - 2.0 + } else if let listOffset = self.listOffset { + headerHeight = max(0.0, listOffset + overlap) + } else { + headerHeight = topInset + overlap + } + let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: headerHeight)) + transition.updateFrame(node: self.headerNode, frame: headerFrame) + + self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationHeight, topPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, offset: 0.0, size: headerFrame.size, transition: transition) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let scrollToItem: ListViewScrollToItem? + if isPickingLocation { + scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: curve, directionHint: .Up) + } else { + scrollToItem = nil + } + + let insets = UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), scrollIndicatorInsets: UIEdgeInsets(top: topInset + 3.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.scrollEnabled = !isPickingLocation + + var listFrame: CGRect = CGRect(origin: CGPoint(), size: layout.size) + if isPickingLocation { + listFrame.origin.y = headerHeight - topInset - overlap + } + transition.updateFrame(node: self.listNode, frame: listFrame) + transition.updateAlpha(node: self.shadeNode, alpha: isPickingLocation ? 1.0 : 0.0) + transition.updateFrame(node: self.shadeNode, frame: CGRect(x: 0.0, y: listFrame.minY + topInset + (actionHeight ?? 0.0) - 3.0, width: layout.size.width, height: 10000.0)) + self.shadeNode.isUserInteractionEnabled = isPickingLocation + self.innerShadeNode.frame = CGRect(x: 0.0, y: 4.0, width: layout.size.width, height: 10000.0) + self.innerShadeNode.alpha = layout.intrinsicInsets.bottom > 0.0 ? 1.0 : 0.0 + + self.layoutActivityIndicator(transition: transition) + + if isFirstLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + + let optionsOffset: CGFloat = self.state.displayingMapModeOptions ? navigationHeight : navigationHeight - optionsHeight + let optionsFrame = CGRect(x: 0.0, y: optionsOffset, width: layout.size.width, height: optionsHeight) + transition.updateFrame(node: self.optionsNode, frame: optionsFrame) + self.optionsNode.updateLayout(size: optionsFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) + + if let searchContainerNode = self.searchContainerNode { + searchContainerNode.frame = CGRect(origin: CGPoint(), size: layout.size) + searchContainerNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationHeight, transition: transition) + } + } + + func updateSendActionHighlight(_ highlighted: Bool) { + self.headerNode.updateHighlight(highlighted) + self.shadeNode.backgroundColor = highlighted ? self.presentationData.theme.list.itemHighlightedBackgroundColor : self.presentationData.theme.list.plainBackgroundColor + } + + func goToUserLocation() { + self.searchVenuesPromise.set(.single(nil)) + self.updateState { state in + var state = state + state.displayingMapModeOptions = false + state.selectedLocation = .none + state.searchingVenuesAround = false + return state + } + } + + func requestPlacesAtSelectedLocation() { + if case let .location(coordinate, _) = self.state.selectedLocation { + self.headerNode.mapNode.setMapCenter(coordinate: coordinate, animated: true) + self.searchVenuesPromise.set(.single(coordinate)) + self.updateState { state in + var state = state + state.searchingVenuesAround = true + return state + } + } + } +} diff --git a/submodules/LocationUI/Sources/LocationSearchContainerNode.swift b/submodules/LocationUI/Sources/LocationSearchContainerNode.swift new file mode 100644 index 0000000000..aaac3717f6 --- /dev/null +++ b/submodules/LocationUI/Sources/LocationSearchContainerNode.swift @@ -0,0 +1,329 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import TelegramUIPreferences +import MergeLists +import AccountContext +import ItemListVenueItem +import ItemListUI +import MapKit +import Geocoding +import ChatListSearchItemHeader + +private struct LocationSearchEntry: Identifiable, Comparable { + let index: Int + let theme: PresentationTheme + let location: TelegramMediaMap + let title: String? + let distance: Double + + var stableId: String { + return self.location.venue?.id ?? "" + } + + static func ==(lhs: LocationSearchEntry, rhs: LocationSearchEntry) -> Bool { + if lhs.index != rhs.index { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.location.venue?.id != rhs.location.venue?.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.distance != rhs.distance { + return false + } + return true + } + + static func <(lhs: LocationSearchEntry, rhs: LocationSearchEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(account: Account, presentationData: PresentationData, sendVenue: @escaping (TelegramMediaMap) -> Void) -> ListViewItem { + let venue = self.location + let header: ChatListSearchItemHeader + let subtitle: String? + if let _ = venue.venue { + header = ChatListSearchItemHeader(type: .nearbyVenues, theme: presentationData.theme, strings: presentationData.strings) + subtitle = nil + } else { + header = ChatListSearchItemHeader(type: .mapAddress, theme: presentationData.theme, strings: presentationData.strings) + subtitle = presentationData.strings.Map_DistanceAway(stringForDistance(strings: presentationData.strings, distance: self.distance)).0 + } + return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), account: account, venue: self.location, title: self.title, subtitle: subtitle, style: .plain, action: { + sendVenue(venue) + }, header: header) + } +} + +struct LocationSearchContainerTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let query: String + let isSearching: Bool + let isEmpty: Bool +} + +private func locationSearchContainerPreparedTransition(from fromEntries: [LocationSearchEntry], to toEntries: [LocationSearchEntry], query: String, isSearching: Bool, isEmpty: Bool, account: Account, presentationData: PresentationData, sendVenue: @escaping (TelegramMediaMap) -> Void) -> LocationSearchContainerTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, sendVenue: sendVenue), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, sendVenue: sendVenue), directionHint: nil) } + + return LocationSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, query: query, isSearching: isSearching, isEmpty: isEmpty) +} + +final class LocationSearchContainerNode: ASDisplayNode { + private let context: AccountContext + private let interaction: LocationPickerInteraction + + private let dimNode: ASDisplayNode + public let listNode: ListView + private let emptyResultsTitleNode: ImmediateTextNode + private let emptyResultsTextNode: ImmediateTextNode + + private let searchQuery = Promise() + private let searchDisposable = MetaDisposable() + + private var presentationData: PresentationData + private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> + + private var validLayout: (ContainerViewLayout, CGFloat)? + private var enqueuedTransitions: [LocationSearchContainerTransition] = [] + + private let _isSearching = ValuePromise(false, ignoreRepeated: true) + var isSearching: Signal { + return self._isSearching.get() + } + + public init(context: AccountContext, coordinate: CLLocationCoordinate2D, interaction: LocationPickerInteraction) { + self.context = context + self.interaction = interaction + + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings)) + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) + self.listNode = ListView() + self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.listNode.isHidden = true + + self.emptyResultsTitleNode = ImmediateTextNode() + self.emptyResultsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.SharedMedia_SearchNoResults, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor) + self.emptyResultsTitleNode.textAlignment = .center + self.emptyResultsTitleNode.isHidden = true + + self.emptyResultsTextNode = ImmediateTextNode() + self.emptyResultsTextNode.maximumNumberOfLines = 0 + self.emptyResultsTextNode.textAlignment = .center + self.emptyResultsTextNode.isHidden = true + + super.init() + + self.backgroundColor = nil + self.isOpaque = false + + self.addSubnode(self.dimNode) + self.addSubnode(self.listNode) + + self.addSubnode(self.emptyResultsTitleNode) + self.addSubnode(self.emptyResultsTextNode) + + self.listNode.isHidden = true + + let currentLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) + let themeAndStringsPromise = self.themeAndStringsPromise + + let isSearching = self._isSearching + let searchItems = self.searchQuery.get() + |> mapToSignal { query -> Signal in + if let query = query, !query.isEmpty { + return (.complete() |> delay(0.6, queue: Queue.mainQueue())) + |> then(.single(query)) + } else { + return .single(query) + } + } + |> mapToSignal { query -> Signal<([LocationSearchEntry], String)?, NoError> in + if let query = query, !query.isEmpty { + let foundVenues = nearbyVenues(account: context.account, latitude: coordinate.latitude, longitude: coordinate.longitude, query: query) + |> afterCompleted { + isSearching.set(false) + } + let foundPlacemarks = geocodeLocation(address: query) + return combineLatest(foundVenues, foundPlacemarks, themeAndStringsPromise.get()) + |> delay(0.1, queue: Queue.concurrentDefaultQueue()) + |> beforeStarted { + isSearching.set(true) + } + |> map { venues, placemarks, themeAndStrings -> ([LocationSearchEntry], String) in + var entries: [LocationSearchEntry] = [] + var index: Int = 0 + + if let placemarks = placemarks { + for placemark in placemarks { + guard let placemarkLocation = placemark.location else { + continue + } + let location = TelegramMediaMap(latitude: placemarkLocation.coordinate.latitude, longitude: placemarkLocation.coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil) + + entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: location, title: placemark.name ?? "Name", distance: placemarkLocation.distance(from: currentLocation))) + + index += 1 + } + } + + for venue in venues { + entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: venue, title: nil, distance: 0.0)) + index += 1 + } + return (entries, query) + } + } else { + return .single(nil) + |> afterCompleted { + isSearching.set(true) + } + } + } + + let previousSearchItems = Atomic<[LocationSearchEntry]>(value: []) + self.searchDisposable.set((searchItems + |> deliverOnMainQueue).start(next: { [weak self] itemsAndQuery in + if let strongSelf = self { + let (items, query) = itemsAndQuery ?? (nil, "") + let previousItems = previousSearchItems.swap(items ?? []) + let transition = locationSearchContainerPreparedTransition(from: previousItems, to: items ?? [], query: query, isSearching: items != nil, isEmpty: items?.isEmpty ?? false, account: context.account, presentationData: strongSelf.presentationData, sendVenue: { venue in self?.listNode.clearHighlightAnimated(true) + if let _ = venue.venue { + self?.interaction.sendVenue(venue) + } else { + self?.interaction.goToCoordinate(venue.coordinate) + self?.interaction.dismissSearch() + } + }) + strongSelf.enqueueTransition(transition) + } + })) + + self.listNode.beganInteractiveDragging = { [weak self] in + self?.interaction.dismissInput() + } + } + + deinit { + self.searchDisposable.dispose() + } + + func scrollToTop() { + if !self.listNode.isHidden { + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } + } + + override func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + self.themeAndStringsPromise.set(.single((presentationData.theme, presentationData.strings))) + self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + } + + func searchTextUpdated(text: String) { + if text.isEmpty { + self.searchQuery.set(.single(nil)) + } else { + self.searchQuery.set(.single(text)) + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let hadValidLayout = self.validLayout != nil + self.validLayout = (layout, navigationBarHeight) + + let topInset = navigationBarHeight + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) + + self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + let padding: CGFloat = 16.0 + let emptyTitleSize = self.emptyResultsTitleNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) + let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) + + let insets = layout.insets(options: [.input]) + let emptyTextSpacing: CGFloat = 8.0 + let emptyTotalHeight = emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing + let emptyTitleY = navigationBarHeight + floorToScreenPixels((layout.size.height - navigationBarHeight - max(insets.bottom, layout.intrinsicInsets.bottom) - emptyTotalHeight) / 2.0) + + transition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyTitleY), size: emptyTitleSize)) + transition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyTitleY + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) + + if !hadValidLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func enqueueTransition(_ transition: LocationSearchContainerTransition) { + self.enqueuedTransitions.append(transition) + + if self.validLayout != nil { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + if let transition = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + options.insert(.PreferSynchronousDrawing) + options.insert(.PreferSynchronousResourceLoading) + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.listNode.isHidden = !transition.isSearching + strongSelf.dimNode.isHidden = transition.isSearching + + strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.Map_SearchNoResultsDescription(transition.query).0, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + + let emptyResults = transition.isSearching && transition.isEmpty + strongSelf.emptyResultsTitleNode.isHidden = !emptyResults + strongSelf.emptyResultsTextNode.isHidden = !emptyResults + + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + }) + } + } + + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.interaction.dismissSearch() + } + } +} diff --git a/submodules/LocationUI/Sources/LocationSearchNavigationContentNode.swift b/submodules/LocationUI/Sources/LocationSearchNavigationContentNode.swift new file mode 100644 index 0000000000..56db5556a5 --- /dev/null +++ b/submodules/LocationUI/Sources/LocationSearchNavigationContentNode.swift @@ -0,0 +1,65 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import SearchBarNode + +private let searchBarFont = Font.regular(17.0) + +final class LocationSearchNavigationContentNode: NavigationBarContentNode { + private var presentationData: PresentationData + + private let searchBar: SearchBarNode + private let interaction: LocationPickerInteraction + + init(presentationData: PresentationData, interaction: LocationPickerInteraction) { + self.presentationData = presentationData + self.interaction = interaction + + self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: false), strings: presentationData.strings, fieldStyle: .modern) + self.searchBar.placeholderString = NSAttributedString(string: presentationData.strings.Map_Search, font: searchBarFont, textColor: presentationData.theme.rootController.navigationSearchBar.inputPlaceholderTextColor) + + super.init() + + self.addSubnode(self.searchBar) + + self.searchBar.cancel = { [weak self] in + self?.searchBar.deactivate(clear: false) + self?.interaction.dismissSearch() + } + self.searchBar.textUpdated = { [weak self] query in + self?.interaction.updateSearchQuery(query) + } + } + + override var nominalHeight: CGFloat { + return 56.0 + } + + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { + let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 56.0)) + self.searchBar.frame = searchBarFrame + self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition) + } + + func activate() { + self.searchBar.activate() + } + + func deactivate() { + self.searchBar.deactivate(clear: false) + } + + func updateActivity(_ activity: Bool) { + self.searchBar.activity = activity + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: false), strings: presentationData.strings) + } +} diff --git a/submodules/LocationUI/Sources/LocationSectionHeaderItem.swift b/submodules/LocationUI/Sources/LocationSectionHeaderItem.swift new file mode 100644 index 0000000000..8674611913 --- /dev/null +++ b/submodules/LocationUI/Sources/LocationSectionHeaderItem.swift @@ -0,0 +1,120 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Postbox +import Display +import SwiftSignalKit +import TelegramPresentationData +import ListSectionHeaderNode +import ItemListUI + +class LocationSectionHeaderItem: ListViewItem { + let presentationData: ItemListPresentationData + let title: String + + public init(presentationData: ItemListPresentationData, title: String) { + self.presentationData = presentationData + self.title = title + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = LocationSectionHeaderItemNode() + let makeLayout = node.asyncLayout() + let (nodeLayout, nodeApply) = makeLayout(self, params) + node.contentSize = nodeLayout.contentSize + node.insets = nodeLayout.insets + + completion(node, nodeApply) + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? LocationSectionHeaderItemNode { + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params) + Queue.mainQueue().async { + completion(nodeLayout, { info in + apply().1(info) + }) + } + } + } + } + } + + public var selectable: Bool { + return false + } +} + +private class LocationSectionHeaderItemNode: ListViewItemNode { + private var headerNode: ListSectionHeaderNode? + + private var item: LocationSectionHeaderItem? + private var layoutParams: ListViewItemLayoutParams? + + required init() { + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + } + + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + if let item = self.item { + let makeLayout = self.asyncLayout() + let (nodeLayout, nodeApply) = makeLayout(item, params) + self.contentSize = nodeLayout.contentSize + self.insets = nodeLayout.insets + let _ = nodeApply() + } + } + + func asyncLayout() -> (_ item: LocationSectionHeaderItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> (Signal?, (ListViewItemApply) -> Void)) { + let currentItem = self.item + + return { [weak self] item, params in + let contentSize = CGSize(width: params.width, height: 28.0) + let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets()) + + return (nodeLayout, { [weak self] in + var updatedTheme: PresentationTheme? + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + return (nil, { _ in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + let headerNode: ListSectionHeaderNode + if let currentHeaderNode = strongSelf.headerNode { + headerNode = currentHeaderNode + + if let _ = updatedTheme { + headerNode.updateTheme(theme: item.presentationData.theme) + } + } else { + headerNode = ListSectionHeaderNode(theme: item.presentationData.theme) + headerNode.title = item.title + strongSelf.addSubnode(headerNode) + strongSelf.headerNode = headerNode + } + + headerNode.frame = CGRect(origin: CGPoint(), size: contentSize) + headerNode.updateLayout(size: contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + } + }) + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) + } +} diff --git a/submodules/LocationUI/Sources/LocationUtils.swift b/submodules/LocationUI/Sources/LocationUtils.swift new file mode 100644 index 0000000000..8c1a3dce47 --- /dev/null +++ b/submodules/LocationUI/Sources/LocationUtils.swift @@ -0,0 +1,151 @@ +import Foundation +import SwiftSignalKit +import SyncCore +import TelegramCore +import TelegramPresentationData +import TelegramStringFormatting +import MapKit + +extension TelegramMediaMap { + convenience init(coordinate: CLLocationCoordinate2D, liveBroadcastingTimeout: Int32? = nil) { + self.init(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: liveBroadcastingTimeout) + } + + var coordinate: CLLocationCoordinate2D { + return CLLocationCoordinate2D(latitude: self.latitude, longitude: self.longitude) + } +} + +extension MKMapRect { + init(region: MKCoordinateRegion) { + let point1 = MKMapPoint(CLLocationCoordinate2D(latitude: region.center.latitude + region.span.latitudeDelta / 2.0, longitude: region.center.longitude - region.span.longitudeDelta / 2.0)) + let point2 = MKMapPoint(CLLocationCoordinate2D(latitude: region.center.latitude - region.span.latitudeDelta / 2.0, longitude: region.center.longitude + region.span.longitudeDelta / 2.0)) + self = MKMapRect(x: min(point1.x, point2.x), y: min(point1.y, point2.y), width: abs(point1.x - point2.x), height: abs(point1.y - point2.y)) + } +} + +extension CLLocationCoordinate2D: Equatable { + +} + +public func ==(lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { + return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude +} + +public func nearbyVenues(account: Account, latitude: Double, longitude: Double, query: String? = nil) -> Signal<[TelegramMediaMap], NoError> { + return resolvePeerByName(account: account, name: "foursquare") + |> take(1) + |> mapToSignal { peerId -> Signal in + guard let peerId = peerId else { + return .single(nil) + } + return requestChatContextResults(account: account, botId: peerId, peerId: account.peerId, query: query ?? "", location: .single((latitude, longitude)), offset: "") + |> `catch` { error -> Signal in + return .single(nil) + } + } + |> map { contextResult -> [TelegramMediaMap] in + guard let contextResult = contextResult else { + return [] + } + var list: [TelegramMediaMap] = [] + for result in contextResult.results { + switch result.message { + case let .mapLocation(mapMedia, _): + if let _ = mapMedia.venue { + list.append(mapMedia) + } + default: + break + } + } + return list + } +} + +private var sharedDistanceFormatter: MKDistanceFormatter? +func stringForDistance(strings: PresentationStrings, distance: CLLocationDistance) -> String { + let distanceFormatter: MKDistanceFormatter + if let currentDistanceFormatter = sharedDistanceFormatter { + distanceFormatter = currentDistanceFormatter + } else { + distanceFormatter = MKDistanceFormatter() + distanceFormatter.unitStyle = .full + sharedDistanceFormatter = distanceFormatter + } + + let locale = localeWithStrings(strings) + if distanceFormatter.locale != locale { + distanceFormatter.locale = locale + } + return distanceFormatter.string(fromDistance: distance) +} + +func stringForEstimatedDuration(strings: PresentationStrings, eta: Double) -> String? { + if eta > 0.0 && eta < 60.0 * 60.0 * 10.0 { + var eta = max(eta, 60.0) + let minutes = Int32(eta / 60.0) % 60 + let hours = Int32(eta / 3600.0) + + let string: String + if hours > 1 { + if hours == 1 && minutes == 0 { + string = strings.Map_ETAHours(1) + } else { + string = strings.Map_ETAHours(9999).replacingOccurrences(of: "9999", with: String(format: "%d:%02d", arguments: [hours, minutes])) + } + } else { + string = strings.Map_ETAMinutes(minutes) + } + return strings.Map_DirectionsDriveEta(string).0 + } else { + return nil + } +} + +func throttledUserLocation(_ userLocation: Signal) -> Signal { + return userLocation + |> reduceLeft(value: nil) { current, updated, emit -> CLLocation? in + if let current = current { + if let updated = updated { + if updated.distance(from: current) > 250 || (updated.horizontalAccuracy < 50.0 && updated.horizontalAccuracy < current.horizontalAccuracy) { + emit(updated) + return updated + } else { + return current + } + } else { + return current + } + } else { + if let updated = updated, updated.horizontalAccuracy > 0.0 { + emit(updated) + return updated + } else { + return nil + } + } + } +} + +func driveEta(coordinate: CLLocationCoordinate2D) -> Signal { + return Signal { subscriber in + let destinationPlacemark = MKPlacemark(coordinate: coordinate, addressDictionary: nil) + let destination = MKMapItem(placemark: destinationPlacemark) + + let request = MKDirections.Request() + request.source = MKMapItem.forCurrentLocation() + request.destination = destination + request.transportType = .automobile + request.requestsAlternateRoutes = false + + let directions = MKDirections(request: request) + directions.calculateETA { response, error in + subscriber.putNext(response?.expectedTravelTime) + subscriber.putCompletion() + } + return ActionDisposable { + directions.cancel() + } + } +} diff --git a/submodules/LocationUI/Sources/LocationViewController.swift b/submodules/LocationUI/Sources/LocationViewController.swift new file mode 100644 index 0000000000..1e9f171847 --- /dev/null +++ b/submodules/LocationUI/Sources/LocationViewController.swift @@ -0,0 +1,188 @@ +import Foundation +import UIKit +import Display +import LegacyComponents +import TelegramCore +import SyncCore +import Postbox +import SwiftSignalKit +import TelegramPresentationData +import AccountContext +import AppBundle +import CoreLocation +import PresentationDataUtils +import OpenInExternalAppUI +import ShareController + +public class LocationViewParams { + let sendLiveLocation: (TelegramMediaMap) -> Void + let stopLiveLocation: () -> Void + let openUrl: (String) -> Void + let openPeer: (Peer) -> Void + + public init(sendLiveLocation: @escaping (TelegramMediaMap) -> Void, stopLiveLocation: @escaping () -> Void, openUrl: @escaping (String) -> Void, openPeer: @escaping (Peer) -> Void) { + self.sendLiveLocation = sendLiveLocation + self.stopLiveLocation = stopLiveLocation + self.openUrl = openUrl + self.openPeer = openPeer + } +} + +class LocationViewInteraction { + let toggleMapModeSelection: () -> Void + let updateMapMode: (LocationMapMode) -> Void + let goToUserLocation: () -> Void + let goToCoordinate: (CLLocationCoordinate2D) -> Void + let requestDirections: () -> Void + let share: () -> Void + + init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping () -> Void, share: @escaping () -> Void) { + self.toggleMapModeSelection = toggleMapModeSelection + self.updateMapMode = updateMapMode + self.goToUserLocation = goToUserLocation + self.goToCoordinate = goToCoordinate + self.requestDirections = requestDirections + self.share = share + } +} + +public final class LocationViewController: ViewController { + private var controllerNode: LocationViewControllerNode { + return self.displayNode as! LocationViewControllerNode + } + private let context: AccountContext + private var mapMedia: TelegramMediaMap + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private var interaction: LocationViewInteraction? + + public init(context: AccountContext, mapMedia: TelegramMediaMap, params: LocationViewParams) { + self.context = context + self.mapMedia = mapMedia + + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme).withUpdatedSeparatorColor(.clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) + + self.navigationPresentation = .modal + + self.title = self.presentationData.strings.Map_LocationTitle + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationShareIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.sharePressed)) + self.navigationItem.rightBarButtonItem?.accessibilityLabel = self.presentationData.strings.VoiceOver_MessageContextShare + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + guard let strongSelf = self, strongSelf.presentationData.theme !== presentationData.theme else { + return + } + strongSelf.presentationData = presentationData + + strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: strongSelf.presentationData.theme).withUpdatedSeparatorColor(.clear), strings: NavigationBarStrings(presentationStrings: strongSelf.presentationData.strings))) + + strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationShareIcon(strongSelf.presentationData.theme), style: .plain, target: strongSelf, action: #selector(strongSelf.sharePressed)) + + if strongSelf.isNodeLoaded { + strongSelf.controllerNode.updatePresentationData(presentationData) + } + }) + + self.interaction = LocationViewInteraction(toggleMapModeSelection: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.updateState { state in + var state = state + state.displayingMapModeOptions = !state.displayingMapModeOptions + return state + } + }, updateMapMode: { [weak self] mode in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.updateState { state in + var state = state + state.mapMode = mode + state.displayingMapModeOptions = false + return state + } + }, goToUserLocation: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.updateState { state in + var state = state + state.displayingMapModeOptions = false + state.selectedLocation = .user + return state + } + }, goToCoordinate: { [weak self] coordinate in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.updateState { state in + var state = state + state.displayingMapModeOptions = false + state.selectedLocation = .coordinate(coordinate) + return state + } + }, requestDirections: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.present(OpenInActionSheetController(context: context, item: .location(location: mapMedia, withDirections: true), additionalAction: nil, openUrl: params.openUrl), in: .window(.root), with: nil) + }, share: { [weak self] in + guard let strongSelf = self else { + return + } + let shareAction = OpenInControllerAction(title: strongSelf.presentationData.strings.Conversation_ContextMenuShare, action: { + strongSelf.present(ShareController(context: context, subject: .mapMedia(mapMedia), externalShare: true), in: .window(.root), with: nil) + }) + strongSelf.present(OpenInActionSheetController(context: context, item: .location(location: mapMedia, withDirections: false), additionalAction: shareAction, openUrl: params.openUrl), in: .window(.root), with: nil) + }) + + self.scrollToTop = { [weak self] in + if let strongSelf = self { + strongSelf.controllerNode.scrollToTop() + } + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + override public func loadDisplayNode() { + super.loadDisplayNode() + guard let interaction = self.interaction else { + return + } + + self.displayNode = LocationViewControllerNode(context: self.context, presentationData: self.presentationData, mapMedia: self.mapMedia, interaction: interaction) + self.displayNodeDidLoad() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition) + } + + @objc private func cancelPressed() { + self.dismiss() + } + + @objc private func sharePressed() { + self.interaction?.share() + } + + @objc private func showAllPressed() { + self.dismiss() + } +} + diff --git a/submodules/LocationUI/Sources/LocationViewControllerNode.swift b/submodules/LocationUI/Sources/LocationViewControllerNode.swift new file mode 100644 index 0000000000..2dac87a12e --- /dev/null +++ b/submodules/LocationUI/Sources/LocationViewControllerNode.swift @@ -0,0 +1,431 @@ +import Foundation +import UIKit +import Display +import LegacyComponents +import TelegramCore +import SyncCore +import Postbox +import SwiftSignalKit +import MergeLists +import ItemListUI +import ItemListVenueItem +import TelegramPresentationData +import AccountContext +import AppBundle +import CoreLocation +import Geocoding + +private func areMessagesEqual(_ lhsMessage: Message, _ rhsMessage: Message) -> Bool { + if lhsMessage.stableVersion != rhsMessage.stableVersion { + return false + } + if lhsMessage.id != rhsMessage.id || lhsMessage.flags != rhsMessage.flags { + return false + } + return true +} + +private struct LocationViewTransaction { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] +} + +private enum LocationViewEntryId: Hashable { + case info + case toggleLiveLocation + case liveLocation(PeerId) +} + +private enum LocationViewEntry: Comparable, Identifiable { + case info(PresentationTheme, TelegramMediaMap, String?, Double?, Double?) + case toggleLiveLocation(PresentationTheme, String, String) + case liveLocation(PresentationTheme, Message, Int) + + var stableId: LocationViewEntryId { + switch self { + case .info: + return .info + case .toggleLiveLocation: + return .toggleLiveLocation + case let .liveLocation(_, message, _): + return .liveLocation(message.id.peerId) + } + } + + static func ==(lhs: LocationViewEntry, rhs: LocationViewEntry) -> Bool { + switch lhs { + case let .info(lhsTheme, lhsLocation, lhsAddress, lhsDistance, lhsTime): + if case let .info(rhsTheme, rhsLocation, rhsAddress, rhsDistance, rhsTime) = rhs, lhsTheme === rhsTheme, lhsLocation.venue?.id == rhsLocation.venue?.id, lhsAddress == rhsAddress, lhsDistance == rhsDistance, lhsTime == rhsTime { + return true + } else { + return false + } + case let .toggleLiveLocation(lhsTheme, lhsTitle, lhsSubtitle): + if case let .toggleLiveLocation(rhsTheme, rhsTitle, rhsSubtitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle { + return true + } else { + return false + } + case let .liveLocation(lhsTheme, lhsMessage, lhsIndex): + if case let .liveLocation(rhsTheme, rhsMessage, rhsIndex) = rhs, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage), lhsIndex == rhsIndex { + return true + } else { + return false + } + } + } + + static func <(lhs: LocationViewEntry, rhs: LocationViewEntry) -> Bool { + switch lhs { + case .info: + switch rhs { + case .info: + return false + case .toggleLiveLocation, .liveLocation: + return true + } + case .toggleLiveLocation: + switch rhs { + case .info, .toggleLiveLocation: + return false + case .liveLocation: + return true + } + case let .liveLocation(_, _, lhsIndex): + switch rhs { + case .info, .toggleLiveLocation: + return false + case let .liveLocation(_, _, rhsIndex): + return lhsIndex < rhsIndex + } + } + } + + func item(account: Account, presentationData: PresentationData, interaction: LocationViewInteraction?) -> ListViewItem { + switch self { + case let .info(theme, location, address, distance, time): + let addressString: String? + if let address = address { + addressString = address + } else { + addressString = presentationData.strings.Map_Locating + } + let distanceString: String? + if let distance = distance { + distanceString = distance < 10 ? presentationData.strings.Map_YouAreHere : presentationData.strings.Map_DistanceAway(stringForDistance(strings: presentationData.strings, distance: distance)).0 + } else { + distanceString = nil + } + let eta = time.flatMap { stringForEstimatedDuration(strings: presentationData.strings, eta: $0) } + return LocationInfoListItem(presentationData: ItemListPresentationData(presentationData), account: account, location: location, address: addressString, distance: distanceString, eta: eta, action: { + interaction?.goToCoordinate(location.coordinate) + }, getDirections: { + interaction?.requestDirections() + }) + case let .toggleLiveLocation(theme, title, subtitle): + return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: account, title: title, subtitle: subtitle, icon: .liveLocation, action: { +// if let coordinate = coordinate { +// interaction?.sendLiveLocation(coordinate) +// } + }) + case let .liveLocation(theme, message, _): + return ItemListTextItem(presentationData: ItemListPresentationData(presentationData), text: .plain(""), sectionId: 0) + } + } +} + +private func preparedTransition(from fromEntries: [LocationViewEntry], to toEntries: [LocationViewEntry], account: Account, presentationData: PresentationData, interaction: LocationViewInteraction?) -> LocationViewTransaction { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, interaction: interaction), directionHint: nil) } + + return LocationViewTransaction(deletions: deletions, insertions: insertions, updates: updates) +} + +enum LocationViewLocation: Equatable { + case initial + case user + case coordinate(CLLocationCoordinate2D) + case custom +} + +struct LocationViewState { + var mapMode: LocationMapMode + var displayingMapModeOptions: Bool + var selectedLocation: LocationViewLocation + + init() { + self.mapMode = .map + self.displayingMapModeOptions = false + self.selectedLocation = .initial + } +} + +final class LocationViewControllerNode: ViewControllerTracingNode { + private let context: AccountContext + private var presentationData: PresentationData + private let presentationDataPromise: Promise + private var mapMedia: TelegramMediaMap + private let interaction: LocationViewInteraction + + private let listNode: ListView + private let headerNode: LocationMapHeaderNode + private let optionsNode: LocationOptionsNode + + private var enqueuedTransitions: [LocationViewTransaction] = [] + + private var disposable: Disposable? + private var state: LocationViewState + private let statePromise: Promise + private var geocodingDisposable = MetaDisposable() + + private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? + private var listOffset: CGFloat? + + init(context: AccountContext, presentationData: PresentationData, mapMedia: TelegramMediaMap, interaction: LocationViewInteraction) { + self.context = context + self.presentationData = presentationData + self.presentationDataPromise = Promise(presentationData) + self.mapMedia = mapMedia + self.interaction = interaction + + self.state = LocationViewState() + self.statePromise = Promise(self.state) + + self.listNode = ListView() + self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3) + self.listNode.verticalScrollIndicatorFollowsOverscroll = true + + self.headerNode = LocationMapHeaderNode(presentationData: presentationData, toggleMapModeSelection: interaction.toggleMapModeSelection, goToUserLocation: interaction.goToUserLocation) + self.headerNode.mapNode.isRotateEnabled = false + + self.optionsNode = LocationOptionsNode(presentationData: presentationData, updateMapMode: interaction.updateMapMode) + + super.init() + + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + self.addSubnode(self.listNode) + self.addSubnode(self.headerNode) + self.addSubnode(self.optionsNode) + + let distance: Signal = .single(nil) + |> then( + throttledUserLocation(self.headerNode.mapNode.userLocation) + |> map { userLocation -> Double? in + let location = CLLocation(latitude: mapMedia.latitude, longitude: mapMedia.longitude) + return userLocation.flatMap { location.distance(from: $0) } + } + ) + let address: Signal + var eta: Signal = .single(nil) + |> then( + driveEta(coordinate: mapMedia.coordinate) + ) + if let venue = mapMedia.venue, let venueAddress = venue.address, !venueAddress.isEmpty { + address = .single(venueAddress) + } else if mapMedia.liveBroadcastingTimeout == nil { + address = .single(nil) + |> then( + reverseGeocodeLocation(latitude: mapMedia.latitude, longitude: mapMedia.longitude) + |> map { placemark -> String? in + return placemark?.compactDisplayAddress ?? "" + } + ) + } else { + address = .single(nil) + eta = .single(nil) + } + + let previousState = Atomic(value: nil) + let previousAnnotations = Atomic<[LocationPinAnnotation]>(value: []) + let previousEntries = Atomic<[LocationViewEntry]?>(value: nil) + + self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), self.headerNode.mapNode.userLocation, distance, address, eta) + |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, userLocation, distance, address, eta in + if let strongSelf = self { + var entries: [LocationViewEntry] = [] + + entries.append(.info(presentationData.theme, mapMedia, address, distance, eta)) + + let previousEntries = previousEntries.swap(entries) + let previousState = previousState.swap(state) + + let transition = preparedTransition(from: previousEntries ?? [], to: entries, account: context.account, presentationData: presentationData, interaction: strongSelf.interaction) + strongSelf.enqueueTransition(transition) + + strongSelf.headerNode.updateState(mapMode: state.mapMode, displayingMapModeOptions: state.displayingMapModeOptions, displayingPlacesButton: false, animated: false) + + switch state.selectedLocation { + case .initial: + if previousState?.selectedLocation != .initial { + strongSelf.headerNode.mapNode.setMapCenter(coordinate: mapMedia.coordinate, span: viewMapSpan, animated: previousState != nil) + } + case let .coordinate(coordinate): + if let previousState = previousState, case let .coordinate(previousCoordinate) = previousState.selectedLocation, previousCoordinate == coordinate { + } else { + strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, span: viewMapSpan, animated: true) + } + case .user: + if previousState?.selectedLocation != .user, let userLocation = userLocation { + strongSelf.headerNode.mapNode.setMapCenter(coordinate: userLocation.coordinate, isUserLocation: true, animated: true) + } + case .custom: + break + } + + let annotations: [LocationPinAnnotation] = [LocationPinAnnotation(context: context, theme: presentationData.theme, location: mapMedia, forcedSelection: true)] + + let previousAnnotations = previousAnnotations.swap(annotations) + if annotations != previousAnnotations { + strongSelf.headerNode.mapNode.annotations = annotations + } + + if let (layout, navigationBarHeight) = strongSelf.validLayout { + var updateLayout = false + var transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring) + + if previousState?.displayingMapModeOptions != state.displayingMapModeOptions { + updateLayout = true + } + + if updateLayout { + strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationBarHeight, transition: transition) + } + } + } + }) + + self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in + guard let strongSelf = self, let (layout, navigationBarHeight) = strongSelf.validLayout, strongSelf.listNode.scrollEnabled else { + return + } + let overlap: CGFloat = 6.0 + strongSelf.listOffset = max(0.0, offset) + let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(0.0, offset + overlap))) + listTransition.updateFrame(node: strongSelf.headerNode, frame: headerFrame) + strongSelf.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, offset: 0.0, size: headerFrame.size, transition: listTransition) + } + + self.listNode.beganInteractiveDragging = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateState { state in + var state = state + state.displayingMapModeOptions = false + return state + } + } + + self.headerNode.mapNode.beganInteractiveDragging = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateState { state in + var state = state + state.displayingMapModeOptions = false + state.selectedLocation = .custom + return state + } + } + } + + deinit { + self.disposable?.dispose() + self.geocodingDisposable.dispose() + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + self.presentationDataPromise.set(.single(presentationData)) + + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.headerNode.updatePresentationData(self.presentationData) + self.optionsNode.updatePresentationData(self.presentationData) + } + + func updateState(_ f: (LocationViewState) -> LocationViewState) { + self.state = f(self.state) + self.statePromise.set(.single(self.state)) + } + + private func enqueueTransition(_ transition: LocationViewTransaction) { + self.enqueuedTransitions.append(transition) + + if let _ = self.validLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + guard let layout = self.validLayout, let transition = self.enqueuedTransitions.first else { + return + } + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + }) + } + + func scrollToTop() { + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let isFirstLayout = self.validLayout == nil + self.validLayout = (layout, navigationHeight) + + let optionsHeight: CGFloat = 38.0 + var actionHeight: CGFloat? + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? LocationActionListItemNode { + if actionHeight == nil { + actionHeight = itemNode.frame.height + } + } + } + + let overlap: CGFloat = 6.0 + let topInset: CGFloat = layout.size.height - layout.intrinsicInsets.bottom - 126.0 - overlap + let headerHeight: CGFloat + if let listOffset = self.listOffset { + headerHeight = max(0.0, listOffset + overlap) + } else { + headerHeight = topInset + overlap + } + let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: headerHeight)) + transition.updateFrame(node: self.headerNode, frame: headerFrame) + + self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationHeight, topPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, offset: 0.0, size: headerFrame.size, transition: transition) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + + let insets = UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), scrollIndicatorInsets: UIEdgeInsets(top: topInset + 3.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + let listFrame: CGRect = CGRect(origin: CGPoint(), size: layout.size) + transition.updateFrame(node: self.listNode, frame: listFrame) + + if isFirstLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + + let optionsOffset: CGFloat = self.state.displayingMapModeOptions ? navigationHeight : navigationHeight - optionsHeight + let optionsFrame = CGRect(x: 0.0, y: optionsOffset, width: layout.size.width, height: optionsHeight) + transition.updateFrame(node: self.optionsNode, frame: optionsFrame) + self.optionsNode.updateLayout(size: optionsFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) + } +} diff --git a/submodules/MapResourceToAvatarSizes/Sources/MapResourceToAvatarSizes.swift b/submodules/MapResourceToAvatarSizes/Sources/MapResourceToAvatarSizes.swift index 9c4963d8dd..c42f510e76 100644 --- a/submodules/MapResourceToAvatarSizes/Sources/MapResourceToAvatarSizes.swift +++ b/submodules/MapResourceToAvatarSizes/Sources/MapResourceToAvatarSizes.swift @@ -15,7 +15,13 @@ public func mapResourceToAvatarSizes(postbox: Postbox, resource: MediaResource, } var result: [Int: Data] = [:] for i in 0 ..< representations.count { - if let scaledImage = generateScaledImage(image: image, size: representations[i].dimensions.cgSize, scale: 1.0), let scaledData = scaledImage.jpegData(compressionQuality: 0.8) { + let size: CGSize + if representations[i].dimensions.width == 80 { + size = CGSize(width: 160.0, height: 160.0) + } else { + size = representations[i].dimensions.cgSize + } + if let scaledImage = generateScaledImage(image: image, size: size, scale: 1.0), let scaledData = scaledImage.jpegData(compressionQuality: 0.8) { result[i] = scaledData } } diff --git a/submodules/MediaPlayer/Sources/MediaPlayer.swift b/submodules/MediaPlayer/Sources/MediaPlayer.swift index d57c7d5366..9f3ab99e65 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayer.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayer.swift @@ -530,8 +530,7 @@ private final class MediaPlayerContext { } else { if case let .timecode(time) = seek { self.seek(timestamp: Double(time), action: .play) - } - else if case .playing = self.state { + } else if case .playing = self.state { } else { self.play() } diff --git a/submodules/MediaPlayer/Sources/MediaPlayerNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerNode.swift index 40be380263..220adfb6a1 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerNode.swift @@ -146,14 +146,117 @@ public final class MediaPlayerNode: ASDisplayNode { private func poll(completion: @escaping (PollStatus) -> Void) { if let (takeFrameQueue, takeFrame) = self.takeFrameAndQueue, let videoLayer = self.videoLayer, let (timebase, _, _, _) = self.state { - let layerRef = Unmanaged.passRetained(videoLayer) + let layerTime = CMTimeGetSeconds(CMTimebaseGetTime(timebase)) + let rate = CMTimebaseGetRate(timebase) + + struct PollState { + var numFrames: Int + var maxTakenTime: Double + } + + var loop: ((PollState) -> Void)? + let loopImpl: (PollState) -> Void = { [weak self] state in + assert(Queue.mainQueue().isCurrent()) + + guard let strongSelf = self, let videoLayer = strongSelf.videoLayer else { + return + } + if !videoLayer.isReadyForMoreMediaData { + completion(.delay(max(1.0 / 30.0, state.maxTakenTime - layerTime))) + return + } + + var state = state + + takeFrameQueue.async { + switch takeFrame() { + case let .restoreState(frames, atTime): + Queue.mainQueue().async { + guard let strongSelf = self, let videoLayer = strongSelf.videoLayer else { + return + } + videoLayer.flush() + } + for i in 0 ..< frames.count { + let frame = frames[i] + let frameTime = CMTimeGetSeconds(frame.position) + state.maxTakenTime = frameTime + let attachments = CMSampleBufferGetSampleAttachmentsArray(frame.sampleBuffer, createIfNecessary: true)! as NSArray + let dict = attachments[0] as! NSMutableDictionary + if i == 0 { + CMSetAttachment(frame.sampleBuffer, key: kCMSampleBufferAttachmentKey_ResetDecoderBeforeDecoding as NSString, value: kCFBooleanTrue as AnyObject, attachmentMode: kCMAttachmentMode_ShouldPropagate) + CMSetAttachment(frame.sampleBuffer, key: kCMSampleBufferAttachmentKey_EndsPreviousSampleDuration as NSString, value: kCFBooleanTrue as AnyObject, attachmentMode: kCMAttachmentMode_ShouldPropagate) + } + if CMTimeCompare(frame.position, atTime) < 0 { + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DoNotDisplay as NSString as String) + } else if CMTimeCompare(frame.position, atTime) == 0 { + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DisplayImmediately as NSString as String) + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleBufferAttachmentKey_EndsPreviousSampleDuration as NSString as String) + } + Queue.mainQueue().async { + guard let strongSelf = self, let videoLayer = strongSelf.videoLayer else { + return + } + videoLayer.enqueue(frame.sampleBuffer) + } + } + Queue.mainQueue().async { + loop?(state) + } + case let .frame(frame): + state.numFrames += 1 + let frameTime = CMTimeGetSeconds(frame.position) + if frame.resetDecoder { + Queue.mainQueue().async { + guard let strongSelf = self, let videoLayer = strongSelf.videoLayer else { + return + } + videoLayer.flush() + } + } + + if frame.decoded && frameTime < layerTime { + Queue.mainQueue().async { + loop?(state) + } + } else { + state.maxTakenTime = frameTime + Queue.mainQueue().async { + guard let strongSelf = self, let videoLayer = strongSelf.videoLayer else { + return + } + videoLayer.enqueue(frame.sampleBuffer) + } + + Queue.mainQueue().async { + loop?(state) + } + } + case .skipFrame: + Queue.mainQueue().async { + loop?(state) + } + case .noFrames: + DispatchQueue.main.async { + completion(.finished) + } + case .finished: + DispatchQueue.main.async { + completion(.finished) + } + } + } + } + loop = loopImpl + loop?(PollState(numFrames: 0, maxTakenTime: layerTime + 0.1)) + + /*let layerRef = Unmanaged.passRetained(videoLayer) takeFrameQueue.async { let status: PollStatus do { var numFrames = 0 let layer = layerRef.takeUnretainedValue() - let layerTime = CMTimeGetSeconds(CMTimebaseGetTime(timebase)) - let rate = CMTimebaseGetRate(timebase) + var maxTakenTime = layerTime + 0.1 var finised = false loop: while true { @@ -230,7 +333,7 @@ public final class MediaPlayerNode: ASDisplayNode { completion(status) } - } + }*/ } } @@ -286,10 +389,6 @@ public final class MediaPlayerNode: ASDisplayNode { strongSelf.layer.addSublayer(videoLayer) - /*let testLayer = RuntimeUtils.makeLayerHostCopy(videoLayer.sublayers![0].sublayers![0])*/ - //testLayer.frame = CGRect(origin: CGPoint(x: -500.0, y: -300.0), size: CGSize(width: 60.0, height: 60.0)) - //strongSelf.layer.addSublayer(testLayer) - strongSelf.updateState() } } @@ -302,13 +401,11 @@ public final class MediaPlayerNode: ASDisplayNode { if let (takeFrameQueue, _) = self.takeFrameAndQueue { if let videoLayer = self.videoLayer { - takeFrameQueue.async { + videoLayer.flushAndRemoveImage() + + Queue.mainQueue().after(1.0, { videoLayer.flushAndRemoveImage() - - takeFrameQueue.after(1.0, { - videoLayer.flushAndRemoveImage() - }) - } + }) } } } diff --git a/submodules/MediaPlayer/Sources/MediaPlayerTimeTextNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerTimeTextNode.swift index ef3381062f..e9d8038454 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerTimeTextNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerTimeTextNode.swift @@ -68,6 +68,9 @@ private final class MediaPlayerTimeTextNodeParameters: NSObject { public final class MediaPlayerTimeTextNode: ASDisplayNode { public var alignment: NSTextAlignment = .left public var mode: MediaPlayerTimeTextNodeMode = .normal + + public var keepPreviousValueOnEmptyState = false + public var textColor: UIColor { didSet { self.updateTimestamp() @@ -151,6 +154,10 @@ public final class MediaPlayerTimeTextNode: ASDisplayNode { } func updateTimestamp() { + if ((self.statusValue?.duration ?? 0.0) < 0.1) && self.state.seconds != nil && self.keepPreviousValueOnEmptyState { + return + } + if let statusValue = self.statusValue, Double(0.0).isLess(than: statusValue.duration) { let timestampSeconds: Double if !statusValue.generationTimestamp.isZero { diff --git a/submodules/MediaResources/BUCK b/submodules/MediaResources/BUCK index 5d75f42e63..681d3aabaf 100644 --- a/submodules/MediaResources/BUCK +++ b/submodules/MediaResources/BUCK @@ -14,6 +14,5 @@ static_library( frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", "$SDKROOT/System/Library/Frameworks/UIKit.framework", - "$SDKROOT/System/Library/Frameworks/MapKit.framework", ], ) diff --git a/submodules/MediaResources/Sources/CachedResourceRepresentations.swift b/submodules/MediaResources/Sources/CachedResourceRepresentations.swift index 4c4b6b8270..ea1cfbccc7 100644 --- a/submodules/MediaResources/Sources/CachedResourceRepresentations.swift +++ b/submodules/MediaResources/Sources/CachedResourceRepresentations.swift @@ -147,21 +147,34 @@ public final class CachedPatternWallpaperMaskRepresentation: CachedMediaResource public final class CachedPatternWallpaperRepresentation: CachedMediaResourceRepresentation { public let keepDuration: CachedMediaRepresentationKeepDuration = .general - public let color: Int32 + public let color: UInt32 + public let bottomColor: UInt32? public let intensity: Int32 + public let rotation: Int32? public var uniqueId: String { - return "pattern-wallpaper-\(self.color)-\(self.intensity)" + var id: String + if let bottomColor = self.bottomColor { + id = "pattern-wallpaper-\(self.color)-\(bottomColor)-\(self.intensity)" + } else { + id = "pattern-wallpaper-\(self.color)-\(self.intensity)" + } + if let rotation = self.rotation, rotation != 0 { + id += "-\(rotation)deg" + } + return id } - public init(color: Int32, intensity: Int32) { + public init(color: UInt32, bottomColor: UInt32?, intensity: Int32, rotation: Int32?) { self.color = color + self.bottomColor = bottomColor self.intensity = intensity + self.rotation = rotation } public func isEqual(to: CachedMediaResourceRepresentation) -> Bool { if let to = to as? CachedPatternWallpaperRepresentation { - return self.color == to.color && self.intensity == intensity + return self.color == to.color && self.bottomColor == to.bottomColor && self.intensity == intensity && self.rotation == to.rotation } else { return false } diff --git a/submodules/MergeLists/Sources/MergeLists.swift b/submodules/MergeLists/Sources/MergeLists.swift index 8b2780082e..5b366bba3a 100644 --- a/submodules/MergeLists/Sources/MergeLists.swift +++ b/submodules/MergeLists/Sources/MergeLists.swift @@ -181,7 +181,7 @@ public func mergeListsStableWithUpdates(leftList: [T], rightList: [T], allUpd return (removeIndices, insertItems, updatedIndices) } -@inlinable +//@inlinable public func mergeListsStableWithUpdates(leftList: [T], rightList: [T], isLess: (T, T) -> Bool, isEqual: (T, T) -> Bool, getId: (T) -> AnyHashable, allUpdated: Bool = false) -> ([Int], [(Int, T, Int?)], [(Int, T, Int)]) { var removeIndices: [Int] = [] var insertItems: [(Int, T, Int?)] = [] @@ -207,6 +207,25 @@ public func mergeListsStableWithUpdates(leftList: [T], rightList: [T], isLess } #endif + var leftStableIds: [AnyHashable] = [] + var rightStableIds: [AnyHashable] = [] + for item in leftList { + leftStableIds.append(getId(item)) + } + for item in rightList { + rightStableIds.append(getId(item)) + } + if Set(leftStableIds) == Set(rightStableIds) && leftStableIds != rightStableIds { + /*var i = 0 + var j = 0 + while true { + if getId(leftList[i]) != getId(rightList[i]) { + + } + }*/ + print("order changed") + } + var currentList = leftList var i = 0 diff --git a/submodules/MessageReactionListUI/Sources/MessageReactionListController.swift b/submodules/MessageReactionListUI/Sources/MessageReactionListController.swift index 5cdc3a252a..ff0b9951fe 100644 --- a/submodules/MessageReactionListUI/Sources/MessageReactionListController.swift +++ b/submodules/MessageReactionListUI/Sources/MessageReactionListController.swift @@ -8,6 +8,7 @@ import SyncCore import SwiftSignalKit import MergeLists import ItemListPeerItem +import ItemListUI public final class MessageReactionListController: ViewController { private let context: AccountContext @@ -93,7 +94,7 @@ private struct MessageReactionListEntry: Comparable, Identifiable { } func item(context: AccountContext, presentationData: PresentationData) -> ListViewItem { - return ItemListPeerItem(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, account: context.account, peer: self.item.peer, height: .peerList, nameStyle: .distinctBold, presence: nil, text: .none, label: .text(self.item.reaction, .custom(Font.regular(19.0))), editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: false, sectionId: 0, action: { + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: self.item.peer, height: .peerList, nameStyle: .distinctBold, presence: nil, text: .none, label: .text(self.item.reaction, .custom(Font.regular(19.0))), editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: false, sectionId: 0, action: { }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, noInsets: true, tag: nil) } @@ -262,29 +263,8 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode placeholderNode.updateLayout(size: CGSize(width: layout.size.width, height: placeholderHeight)) } - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) let sideInset: CGFloat = 12.0 let spacing: CGFloat = 6.0 diff --git a/submodules/MtProtoKit/MTBackupAddressSignals.m b/submodules/MtProtoKit/MTBackupAddressSignals.m index 515c1c90f6..6de20c310f 100644 --- a/submodules/MtProtoKit/MTBackupAddressSignals.m +++ b/submodules/MtProtoKit/MTBackupAddressSignals.m @@ -227,6 +227,8 @@ static NSString *makeRandomPadding() { + (MTSignal *)fetchConfigFromAddress:(MTBackupDatacenterAddress *)address currentContext:(MTContext *)currentContext { MTApiEnvironment *apiEnvironment = [currentContext.apiEnvironment copy]; + apiEnvironment = [apiEnvironment withUpdatedSocksProxySettings:nil]; + NSMutableDictionary *datacenterAddressOverrides = [[NSMutableDictionary alloc] init]; datacenterAddressOverrides[@(address.datacenterId)] = [[MTDatacenterAddress alloc] initWithIp:address.ip port:(uint16_t)address.port preferForMedia:false restrictToTcp:false cdn:false preferForProxy:false secret:address.secret]; diff --git a/submodules/MtProtoKit/MTDiscoverConnectionSignals.h b/submodules/MtProtoKit/MTDiscoverConnectionSignals.h index f29c2da4a3..a71313c198 100644 --- a/submodules/MtProtoKit/MTDiscoverConnectionSignals.h +++ b/submodules/MtProtoKit/MTDiscoverConnectionSignals.h @@ -3,6 +3,7 @@ @class MTContext; @class MTDatacenterAddress; @class MTSignal; +@class MTDatacenterAuthKey; typedef struct { uint8_t nonce[16]; @@ -14,4 +15,6 @@ typedef struct { + (MTSignal *)discoverSchemeWithContext:(MTContext *)context datacenterId:(NSInteger)datacenterId addressList:(NSArray *)addressList media:(bool)media isProxy:(bool)isProxy; ++ (MTSignal * _Nonnull)checkIfAuthKeyRemovedWithContext:(MTContext * _Nonnull)context datacenterId:(NSInteger)datacenterId authKey:(MTDatacenterAuthKey *)authKey; + @end diff --git a/submodules/MtProtoKit/MTDiscoverConnectionSignals.m b/submodules/MtProtoKit/MTDiscoverConnectionSignals.m index dd4b304e12..d3c0ae312d 100644 --- a/submodules/MtProtoKit/MTDiscoverConnectionSignals.m +++ b/submodules/MtProtoKit/MTDiscoverConnectionSignals.m @@ -13,6 +13,7 @@ #import "MTContext.h" #import "MTApiEnvironment.h" #import "MTLogging.h" +#import "MTDatacenterAuthAction.h" #import #import @@ -245,4 +246,25 @@ }]; } ++ (MTSignal * _Nonnull)checkIfAuthKeyRemovedWithContext:(MTContext * _Nonnull)context datacenterId:(NSInteger)datacenterId authKey:(MTDatacenterAuthKey *)authKey { + return [[MTSignal alloc] initWithGenerator:^id(MTSubscriber *subscriber) { + MTMetaDisposable *disposable = [[MTMetaDisposable alloc] init]; + + [[MTContext contextQueue] dispatchOnQueue:^{ + MTDatacenterAuthAction *action = [[MTDatacenterAuthAction alloc] initWithTempAuth:true tempAuthKeyType:MTDatacenterAuthTempKeyTypeMain bindKey:authKey]; + action.completedWithResult = ^(bool success) { + [subscriber putNext:@(!success)]; + [subscriber putCompletion]; + }; + [action execute:context datacenterId:datacenterId isCdn:false]; + + [disposable setDisposable:[[MTBlockDisposable alloc] initWithBlock:^{ + [action cancel]; + }]]; + }]; + + return disposable; + }]; +} + @end diff --git a/submodules/MtProtoKit/MTProtoKit/MTContext.h b/submodules/MtProtoKit/MTProtoKit/MTContext.h index 6f1dfc0f91..c4cff82ef3 100644 --- a/submodules/MtProtoKit/MTProtoKit/MTContext.h +++ b/submodules/MtProtoKit/MTProtoKit/MTContext.h @@ -13,6 +13,7 @@ @class MTSessionInfo; @class MTApiEnvironment; @class MTSignal; +@class MTQueue; @protocol MTContextChangeListener @@ -21,12 +22,13 @@ - (void)contextDatacenterAddressSetUpdated:(MTContext *)context datacenterId:(NSInteger)datacenterId addressSet:(MTDatacenterAddressSet *)addressSet; - (void)contextDatacenterAuthInfoUpdated:(MTContext *)context datacenterId:(NSInteger)datacenterId authInfo:(MTDatacenterAuthInfo *)authInfo; - (void)contextDatacenterAuthTokenUpdated:(MTContext *)context datacenterId:(NSInteger)datacenterId authToken:(id)authToken; -- (void)contextDatacenterTransportSchemesUpdated:(MTContext *)context datacenterId:(NSInteger)datacenterId; +- (void)contextDatacenterTransportSchemesUpdated:(MTContext *)context datacenterId:(NSInteger)datacenterId shouldReset:(bool)shouldReset; - (void)contextIsPasswordRequiredUpdated:(MTContext *)context datacenterId:(NSInteger)datacenterId; - (void)contextDatacenterPublicKeysUpdated:(MTContext *)context datacenterId:(NSInteger)datacenterId publicKeys:(NSArray *)publicKeys; - (MTSignal *)fetchContextDatacenterPublicKeys:(MTContext *)context datacenterId:(NSInteger)datacenterId; - (void)contextApiEnvironmentUpdated:(MTContext *)context apiEnvironment:(MTApiEnvironment *)apiEnvironment; - (MTSignal *)isContextNetworkAccessAllowed:(MTContext *)context; +- (void)contextLoggedOut:(MTContext *)context; @end @@ -51,6 +53,8 @@ + (int32_t)fixedTimeDifference; + (void)setFixedTimeDifference:(int32_t)fixedTimeDifference; ++ (MTQueue *)contextQueue; + - (instancetype)initWithSerialization:(id)serialization encryptionProvider:(id)encryptionProvider apiEnvironment:(MTApiEnvironment *)apiEnvironment isTestingEnvironment:(bool)isTestingEnvironment useTempAuthKeys:(bool)useTempAuthKeys; - (void)performBatchUpdates:(void (^)())block; @@ -113,4 +117,6 @@ - (void)beginExplicitBackupAddressDiscovery; +- (void)checkIfLoggedOut:(NSInteger)datacenterId; + @end diff --git a/submodules/MtProtoKit/MTProtoKit/MTContext.m b/submodules/MtProtoKit/MTProtoKit/MTContext.m index 4c5b096a30..6abb56c6a0 100644 --- a/submodules/MtProtoKit/MTProtoKit/MTContext.m +++ b/submodules/MtProtoKit/MTProtoKit/MTContext.m @@ -143,6 +143,9 @@ NSMutableDictionary *_datacenterTempAuthActions; NSMutableDictionary *_datacenterTransferAuthActions; + NSMutableDictionary *_datacenterCheckKeyRemovedActionTimestamps; + NSMutableDictionary > *_datacenterCheckKeyRemovedActions; + NSMutableDictionary *_cleanupSessionIdsByAuthKeyId; NSMutableArray *_currentSessionInfos; @@ -218,6 +221,8 @@ static int32_t fixedTimeDifferenceValue = 0; _datacenterAuthActions = [[NSMutableDictionary alloc] init]; _datacenterTempAuthActions = [[NSMutableDictionary alloc] init]; _datacenterTransferAuthActions = [[NSMutableDictionary alloc] init]; + _datacenterCheckKeyRemovedActionTimestamps = [[NSMutableDictionary alloc] init]; + _datacenterCheckKeyRemovedActions = [[NSMutableDictionary alloc] init]; _cleanupSessionIdsByAuthKeyId = [[NSMutableDictionary alloc] init]; _currentSessionInfos = [[NSMutableArray alloc] init]; @@ -263,6 +268,9 @@ static int32_t fixedTimeDifferenceValue = 0; NSDictionary *datacenterTransferAuthActions = _datacenterTransferAuthActions; _datacenterTransferAuthActions = nil; + NSDictionary *datacenterCheckKeyRemovedActions = _datacenterCheckKeyRemovedActions; + _datacenterCheckKeyRemovedActions = nil; + NSDictionary *fetchPublicKeysActions = _fetchPublicKeysActions; _fetchPublicKeysActions = nil; @@ -304,6 +312,10 @@ static int32_t fixedTimeDifferenceValue = 0; [disposable dispose]; } + for (NSNumber *nDatacenterId in datacenterCheckKeyRemovedActions) { + [datacenterCheckKeyRemovedActions[nDatacenterId] dispose]; + } + [cleanupSessionInfoDisposables dispose]; }]; } @@ -479,10 +491,11 @@ static int32_t fixedTimeDifferenceValue = 0; [listener contextDatacenterAddressSetUpdated:self datacenterId:datacenterId addressSet:addressSet]; } - if (previousAddressSetWasEmpty || updateSchemes || true) { + if (true) { + bool shouldReset = previousAddressSetWasEmpty || updateSchemes; for (id listener in currentListeners) { - if ([listener respondsToSelector:@selector(contextDatacenterTransportSchemesUpdated:datacenterId:)]) { - [listener contextDatacenterTransportSchemesUpdated:self datacenterId:datacenterId]; + if ([listener respondsToSelector:@selector(contextDatacenterTransportSchemesUpdated:datacenterId:shouldReset:)]) { + [listener contextDatacenterTransportSchemesUpdated:self datacenterId:datacenterId shouldReset:shouldReset]; } } } else { @@ -652,8 +665,8 @@ static int32_t fixedTimeDifferenceValue = 0; } for (id listener in currentListeners) { - if ([listener respondsToSelector:@selector(contextDatacenterTransportSchemesUpdated:datacenterId:)]) - [listener contextDatacenterTransportSchemesUpdated:self datacenterId:datacenterId]; + if ([listener respondsToSelector:@selector(contextDatacenterTransportSchemesUpdated:datacenterId:shouldReset:)]) + [listener contextDatacenterTransportSchemesUpdated:self datacenterId:datacenterId shouldReset:true]; } } }]; @@ -815,6 +828,9 @@ static int32_t fixedTimeDifferenceValue = 0; return current; }]; } + if (MTLogEnabled()) { + MTLog(@"[MTContext has chosen a scheme for DC%d: %@]", datacenterId, schemeWithEarliestFailure); + } result = schemeWithEarliestFailure; } synchronous:true]; @@ -1242,7 +1258,7 @@ static int32_t fixedTimeDifferenceValue = 0; { if (_datacenterAuthActions[@(datacenterId)] == nil) { - MTDatacenterAuthAction *authAction = [[MTDatacenterAuthAction alloc] initWithTempAuth:false tempAuthKeyType:MTDatacenterAuthTempKeyTypeMain]; + MTDatacenterAuthAction *authAction = [[MTDatacenterAuthAction alloc] initWithTempAuth:false tempAuthKeyType:MTDatacenterAuthTempKeyTypeMain bindKey:nil]; authAction.delegate = self; _datacenterAuthActions[@(datacenterId)] = authAction; [authAction execute:self datacenterId:datacenterId isCdn:isCdn]; @@ -1253,7 +1269,7 @@ static int32_t fixedTimeDifferenceValue = 0; - (void)tempAuthKeyForDatacenterWithIdRequired:(NSInteger)datacenterId keyType:(MTDatacenterAuthTempKeyType)keyType { [[MTContext contextQueue] dispatchOnQueue:^{ if (_datacenterTempAuthActions[@(datacenterId)] == nil) { - MTDatacenterAuthAction *authAction = [[MTDatacenterAuthAction alloc] initWithTempAuth:true tempAuthKeyType:keyType]; + MTDatacenterAuthAction *authAction = [[MTDatacenterAuthAction alloc] initWithTempAuth:true tempAuthKeyType:keyType bindKey:nil]; authAction.delegate = self; _datacenterTempAuthActions[@(datacenterId)] = authAction; [authAction execute:self datacenterId:datacenterId isCdn:false]; @@ -1354,4 +1370,37 @@ static int32_t fixedTimeDifferenceValue = 0; }]; } +- (void)checkIfLoggedOut:(NSInteger)datacenterId { + [[MTContext contextQueue] dispatchOnQueue:^{ + MTDatacenterAuthInfo *authInfo = [self authInfoForDatacenterWithId:datacenterId]; + if (authInfo == nil || authInfo.authKey == nil) { + return; + } + + int32_t timestamp = (int32_t)CFAbsoluteTimeGetCurrent(); + NSNumber *currentTimestamp = _datacenterCheckKeyRemovedActionTimestamps[@(datacenterId)]; + if (currentTimestamp == nil || [currentTimestamp intValue] + 60 < timestamp) { + _datacenterCheckKeyRemovedActionTimestamps[@(datacenterId)] = currentTimestamp; + [_datacenterCheckKeyRemovedActions[@(datacenterId)] dispose]; + __weak MTContext *weakSelf = self; + _datacenterCheckKeyRemovedActions[@(datacenterId)] = [[MTDiscoverConnectionSignals checkIfAuthKeyRemovedWithContext:self datacenterId:datacenterId authKey:authInfo.authKey] startWithNext:^(NSNumber *isRemoved) { + [[MTContext contextQueue] dispatchOnQueue:^{ + __strong MTContext *strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + + if ([isRemoved boolValue]) { + NSArray *currentListeners = [[NSArray alloc] initWithArray:strongSelf->_changeListeners]; + for (id listener in currentListeners) { + if ([listener respondsToSelector:@selector(contextLoggedOut:)]) + [listener contextLoggedOut:strongSelf]; + } + } + }]; + }]; + } + }]; +} + @end diff --git a/submodules/MtProtoKit/MTProtoKit/MTDatacenterAuthAction.h b/submodules/MtProtoKit/MTProtoKit/MTDatacenterAuthAction.h index ee7cb48685..c9643cdaf3 100644 --- a/submodules/MtProtoKit/MTProtoKit/MTDatacenterAuthAction.h +++ b/submodules/MtProtoKit/MTProtoKit/MTDatacenterAuthAction.h @@ -16,8 +16,9 @@ @property (nonatomic, readonly) bool tempAuth; @property (nonatomic, weak) id delegate; +@property (nonatomic, copy) void (^completedWithResult)(bool); -- (instancetype)initWithTempAuth:(bool)tempAuth tempAuthKeyType:(MTDatacenterAuthTempKeyType)tempAuthKeyType; +- (instancetype)initWithTempAuth:(bool)tempAuth tempAuthKeyType:(MTDatacenterAuthTempKeyType)tempAuthKeyType bindKey:(MTDatacenterAuthKey *)bindKey; - (void)execute:(MTContext *)context datacenterId:(NSInteger)datacenterId isCdn:(bool)isCdn; - (void)cancel; diff --git a/submodules/MtProtoKit/MTProtoKit/MTDatacenterAuthAction.m b/submodules/MtProtoKit/MTProtoKit/MTDatacenterAuthAction.m index 39991daff9..b6f38a2a58 100644 --- a/submodules/MtProtoKit/MTProtoKit/MTDatacenterAuthAction.m +++ b/submodules/MtProtoKit/MTProtoKit/MTDatacenterAuthAction.m @@ -22,12 +22,14 @@ { bool _isCdn; MTDatacenterAuthTempKeyType _tempAuthKeyType; + MTDatacenterAuthKey *_bindKey; NSInteger _datacenterId; __weak MTContext *_context; bool _awaitingAddresSetUpdate; MTProto *_authMtProto; + MTProto *_bindMtProto; MTMetaDisposable *_verifyDisposable; } @@ -36,11 +38,12 @@ @implementation MTDatacenterAuthAction -- (instancetype)initWithTempAuth:(bool)tempAuth tempAuthKeyType:(MTDatacenterAuthTempKeyType)tempAuthKeyType { +- (instancetype)initWithTempAuth:(bool)tempAuth tempAuthKeyType:(MTDatacenterAuthTempKeyType)tempAuthKeyType bindKey:(MTDatacenterAuthKey *)bindKey { self = [super init]; if (self != nil) { _tempAuth = tempAuth; _tempAuthKeyType = tempAuthKeyType; + _bindKey = bindKey; _verifyDisposable = [[MTMetaDisposable alloc] init]; } return self; @@ -61,7 +64,7 @@ { bool alreadyCompleted = false; MTDatacenterAuthInfo *currentAuthInfo = [context authInfoForDatacenterWithId:_datacenterId]; - if (currentAuthInfo != nil) { + if (currentAuthInfo != nil && _bindKey == nil) { if (_tempAuth) { if ([currentAuthInfo tempAuthKeyWithType:_tempAuthKeyType] != nil) { alreadyCompleted = true; @@ -108,15 +111,35 @@ if (_tempAuth) { MTContext *mainContext = _context; if (mainContext != nil) { - MTContext *context = _context; - [context performBatchUpdates:^{ - MTDatacenterAuthInfo *authInfo = [context authInfoForDatacenterWithId:_datacenterId]; - if (authInfo != nil) { - authInfo = [authInfo withUpdatedTempAuthKeyWithType:_tempAuthKeyType key:authKey]; - [context updateAuthInfoForDatacenterWithId:_datacenterId authInfo:authInfo]; - } - }]; - [self complete]; + if (_bindKey != nil) { + _bindMtProto = [[MTProto alloc] initWithContext:mainContext datacenterId:_datacenterId usageCalculationInfo:nil requiredAuthToken:nil authTokenMasterDatacenterId:0]; + _bindMtProto.cdn = false; + _bindMtProto.useUnauthorizedMode = false; + _bindMtProto.useTempAuthKeys = true; + __weak MTDatacenterAuthAction *weakSelf = self; + _bindMtProto.tempAuthKeyBindingResultUpdated = ^(bool success) { + __strong MTDatacenterAuthAction *strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + [strongSelf->_bindMtProto stop]; + if (strongSelf->_completedWithResult) { + strongSelf->_completedWithResult(success); + } + }; + _bindMtProto.useExplicitAuthKey = authKey; + [_bindMtProto resume]; + } else { + MTContext *context = _context; + [context performBatchUpdates:^{ + MTDatacenterAuthInfo *authInfo = [context authInfoForDatacenterWithId:_datacenterId]; + if (authInfo != nil) { + authInfo = [authInfo withUpdatedTempAuthKeyWithType:_tempAuthKeyType key:authKey]; + [context updateAuthInfoForDatacenterWithId:_datacenterId authInfo:authInfo]; + } + }]; + [self complete]; + } } } else { MTDatacenterAuthInfo *authInfo = [[MTDatacenterAuthInfo alloc] initWithAuthKey:authKey.authKey authKeyId:authKey.authKeyId saltSet:@[[[MTDatacenterSaltInfo alloc] initWithSalt:0 firstValidMessageId:timestamp lastValidMessageId:timestamp + (29.0 * 60.0) * 4294967296]] authKeyAttributes:nil mainTempAuthKey:nil mediaTempAuthKey:nil]; diff --git a/submodules/MtProtoKit/MTProtoKit/MTDatacenterAuthMessageService.m b/submodules/MtProtoKit/MTProtoKit/MTDatacenterAuthMessageService.m index a91d324576..bfd7d919b9 100644 --- a/submodules/MtProtoKit/MTProtoKit/MTDatacenterAuthMessageService.m +++ b/submodules/MtProtoKit/MTProtoKit/MTDatacenterAuthMessageService.m @@ -244,7 +244,7 @@ typedef enum { [reqPqBuffer appendBytes:_nonce.bytes length:_nonce.length]; NSString *messageDescription = [NSString stringWithFormat:@"reqPq nonce:%@", _nonce]; - MTOutgoingMessage *message = [[MTOutgoingMessage alloc] initWithData:reqPqBuffer.data metadata:messageDescription shortMetadata:messageDescription messageId:_currentStageMessageId messageSeqNo:_currentStageMessageSeqNo]; + MTOutgoingMessage *message = [[MTOutgoingMessage alloc] initWithData:reqPqBuffer.data metadata:messageDescription additionalDebugDescription:nil shortMetadata:messageDescription messageId:_currentStageMessageId messageSeqNo:_currentStageMessageSeqNo]; return [[MTMessageTransaction alloc] initWithMessagePayload:@[message] prepared:nil failed:nil completion:^(NSDictionary *messageInternalIdToTransactionId, NSDictionary *messageInternalIdToPreparedMessage, __unused NSDictionary *messageInternalIdToQuickAckId) { if (_stage == MTDatacenterAuthStagePQ && messageInternalIdToTransactionId[message.internalId] != nil && messageInternalIdToPreparedMessage[message.internalId] != nil) @@ -268,7 +268,7 @@ typedef enum { [reqDhBuffer appendTLBytes:_dhEncryptedData]; NSString *messageDescription = [NSString stringWithFormat:@"reqDh nonce:%@ serverNonce:%@ p:%@ q:%@ fingerprint:%llx", _nonce, _serverNonce, _dhP, _dhQ, _dhPublicKeyFingerprint]; - MTOutgoingMessage *message = [[MTOutgoingMessage alloc] initWithData:reqDhBuffer.data metadata:messageDescription shortMetadata:messageDescription messageId:_currentStageMessageId messageSeqNo:_currentStageMessageSeqNo]; + MTOutgoingMessage *message = [[MTOutgoingMessage alloc] initWithData:reqDhBuffer.data metadata:messageDescription additionalDebugDescription:nil shortMetadata:messageDescription messageId:_currentStageMessageId messageSeqNo:_currentStageMessageSeqNo]; return [[MTMessageTransaction alloc] initWithMessagePayload:@[message] prepared:nil failed:nil completion:^(NSDictionary *messageInternalIdToTransactionId, NSDictionary *messageInternalIdToPreparedMessage, __unused NSDictionary *messageInternalIdToQuickAckId) { if (_stage == MTDatacenterAuthStageReqDH && messageInternalIdToTransactionId[message.internalId] != nil && messageInternalIdToPreparedMessage[message.internalId] != nil) @@ -288,7 +288,7 @@ typedef enum { [setDhParamsBuffer appendBytes:_serverNonce.bytes length:_serverNonce.length]; [setDhParamsBuffer appendTLBytes:_encryptedClientData]; - MTOutgoingMessage *message = [[MTOutgoingMessage alloc] initWithData:setDhParamsBuffer.data metadata:@"setDhParams" shortMetadata:@"setDhParams" messageId:_currentStageMessageId messageSeqNo:_currentStageMessageSeqNo]; + MTOutgoingMessage *message = [[MTOutgoingMessage alloc] initWithData:setDhParamsBuffer.data metadata:@"setDhParams" additionalDebugDescription:nil shortMetadata:@"setDhParams" messageId:_currentStageMessageId messageSeqNo:_currentStageMessageSeqNo]; return [[MTMessageTransaction alloc] initWithMessagePayload:@[message] prepared:nil failed:nil completion:^(NSDictionary *messageInternalIdToTransactionId, NSDictionary *messageInternalIdToPreparedMessage, __unused NSDictionary *messageInternalIdToQuickAckId) { if (_stage == MTDatacenterAuthStageKeyVerification && messageInternalIdToTransactionId[message.internalId] != nil && messageInternalIdToPreparedMessage[message.internalId] != nil) diff --git a/submodules/MtProtoKit/MTProtoKit/MTOutgoingMessage.h b/submodules/MtProtoKit/MTProtoKit/MTOutgoingMessage.h index 159d08dc81..b84359a49e 100644 --- a/submodules/MtProtoKit/MTProtoKit/MTOutgoingMessage.h +++ b/submodules/MtProtoKit/MTProtoKit/MTOutgoingMessage.h @@ -7,6 +7,7 @@ @property (nonatomic, strong, readonly) id internalId; @property (nonatomic, strong, readonly) NSData *data; @property (nonatomic, strong, readonly) id metadata; +@property (nonatomic, strong, readonly) NSString *additionalDebugDescription; @property (nonatomic, strong, readonly) id shortMetadata; @property (nonatomic, readonly) int64_t messageId; @property (nonatomic, readonly) int32_t messageSeqNo; @@ -15,9 +16,9 @@ @property (nonatomic) bool hasHighPriority; @property (nonatomic) int64_t inResponseToMessageId; -@property (nonatomic, copy) id (^dynamicDecorator)(NSData *currentData, NSMutableDictionary *messageInternalIdToPreparedMessage); +@property (nonatomic, copy) id (^dynamicDecorator)(int64_t, NSData *currentData, NSMutableDictionary *messageInternalIdToPreparedMessage); -- (instancetype)initWithData:(NSData *)data metadata:(id)metadata shortMetadata:(id)shortMetadata; -- (instancetype)initWithData:(NSData *)data metadata:(id)metadata shortMetadata:(id)shortMetadata messageId:(int64_t)messageId messageSeqNo:(int32_t)messageSeqNo; +- (instancetype)initWithData:(NSData *)data metadata:(id)metadata additionalDebugDescription:(NSString *)additionalDebugDescription shortMetadata:(id)shortMetadata; +- (instancetype)initWithData:(NSData *)data metadata:(id)metadata additionalDebugDescription:(NSString *)additionalDebugDescription shortMetadata:(id)shortMetadata messageId:(int64_t)messageId messageSeqNo:(int32_t)messageSeqNo; @end diff --git a/submodules/MtProtoKit/MTProtoKit/MTOutgoingMessage.m b/submodules/MtProtoKit/MTProtoKit/MTOutgoingMessage.m index e1dd14fd3e..d0ef4631ac 100644 --- a/submodules/MtProtoKit/MTProtoKit/MTOutgoingMessage.m +++ b/submodules/MtProtoKit/MTProtoKit/MTOutgoingMessage.m @@ -46,12 +46,12 @@ @implementation MTOutgoingMessage -- (instancetype)initWithData:(NSData *)data metadata:(id)metadata shortMetadata:(id)shortMetadata +- (instancetype)initWithData:(NSData *)data metadata:(id)metadata additionalDebugDescription:(NSString *)additionalDebugDescription shortMetadata:(id)shortMetadata { - return [self initWithData:data metadata:metadata shortMetadata:shortMetadata messageId:0 messageSeqNo:0]; + return [self initWithData:data metadata:metadata additionalDebugDescription:additionalDebugDescription shortMetadata:shortMetadata messageId:0 messageSeqNo:0]; } -- (instancetype)initWithData:(NSData *)data metadata:(id)metadata shortMetadata:(id)shortMetadata messageId:(int64_t)messageId messageSeqNo:(int32_t)messageSeqNo +- (instancetype)initWithData:(NSData *)data metadata:(id)metadata additionalDebugDescription:(NSString *)additionalDebugDescription shortMetadata:(id)shortMetadata messageId:(int64_t)messageId messageSeqNo:(int32_t)messageSeqNo { self = [super init]; if (self != nil) @@ -59,6 +59,7 @@ _internalId = [[MTOutgoingMessageInternalId alloc] init]; _data = data; _metadata = metadata; + _additionalDebugDescription = additionalDebugDescription; _shortMetadata = shortMetadata; _messageId = messageId; _messageSeqNo = messageSeqNo; diff --git a/submodules/MtProtoKit/MTProtoKit/MTProto.h b/submodules/MtProtoKit/MTProtoKit/MTProto.h index 81bfc74065..231d1b4a26 100644 --- a/submodules/MtProtoKit/MTProtoKit/MTProto.h +++ b/submodules/MtProtoKit/MTProtoKit/MTProto.h @@ -37,6 +37,9 @@ @property (nonatomic, strong, readonly) MTContext *context; @property (nonatomic, strong, readonly) MTApiEnvironment *apiEnvironment; @property (nonatomic) NSInteger datacenterId; +@property (nonatomic, strong) MTDatacenterAuthKey *useExplicitAuthKey; + +@property (nonatomic, copy) void (^tempAuthKeyBindingResultUpdated)(bool); @property (nonatomic) bool shouldStayConnected; @property (nonatomic) bool useUnauthorizedMode; diff --git a/submodules/MtProtoKit/MTProtoKit/MTProto.m b/submodules/MtProtoKit/MTProtoKit/MTProto.m index f8372ac755..6b4f2ce316 100644 --- a/submodules/MtProtoKit/MTProtoKit/MTProto.m +++ b/submodules/MtProtoKit/MTProtoKit/MTProto.m @@ -174,6 +174,15 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; }]; } +- (void)setUseExplicitAuthKey:(MTDatacenterAuthKey *)useExplicitAuthKey { + _useExplicitAuthKey = useExplicitAuthKey; + if (_useExplicitAuthKey != nil) { + _authInfo = [_authInfo withUpdatedTempAuthKeyWithType:MTDatacenterAuthTempKeyTypeMain key:useExplicitAuthKey]; + [self setMtState:_mtState | MTProtoStateBindingTempAuthKey]; + [self requestTransportTransaction]; + } +} + - (void)setUsageCalculationInfo:(MTNetworkUsageCalculationInfo *)usageCalculationInfo { [[MTProto managerQueue] dispatchOnQueue:^{ _usageCalculationInfo = usageCalculationInfo; @@ -226,7 +235,7 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; if ((_mtState & MTProtoStateStopped) == 0) { [self setMtState:_mtState | MTProtoStateStopped]; - + [_context removeChangeListener:self]; if (_transport != nil) { _transport.delegate = nil; @@ -282,18 +291,17 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; [previousTransport stop]; if (_transport != nil && _useTempAuthKeys) { - assert(false); - /*MTDatacenterAuthTempKeyType tempAuthKeyType = MTDatacenterAuthTempKeyTypeMain; - if (_transport.scheme.address.preferForMedia) { + MTDatacenterAuthTempKeyType tempAuthKeyType = MTDatacenterAuthTempKeyTypeMain; + /*if (_transport.scheme.address.preferForMedia) { tempAuthKeyType = MTDatacenterAuthTempKeyTypeMedia; - } + }*/ MTDatacenterAuthKey *effectiveAuthKey = [_authInfo tempAuthKeyWithType:tempAuthKeyType]; if (effectiveAuthKey == nil) { if (MTLogEnabled()) { MTLog(@"[MTProto#%p setTransport temp auth key missing]", self); } - }*/ + } } if (_transport != nil) @@ -859,7 +867,7 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; - (NSString *)outgoingMessageDescription:(MTOutgoingMessage *)message messageId:(int64_t)messageId messageSeqNo:(int32_t)messageSeqNo { - return [[NSString alloc] initWithFormat:@"%@ (%" PRId64 "/%" PRId32 ")", message.metadata, message.messageId == 0 ? messageId : message.messageId, message.messageSeqNo == 0 ? message.messageSeqNo : messageSeqNo]; + return [[NSString alloc] initWithFormat:@"%@%@ (%" PRId64 "/%" PRId32 ")", message.metadata, message.additionalDebugDescription != nil ? message.additionalDebugDescription : @"", message.messageId == 0 ? messageId : message.messageId, message.messageSeqNo == 0 ? message.messageSeqNo : messageSeqNo]; } - (NSString *)outgoingShortMessageDescription:(MTOutgoingMessage *)message messageId:(int64_t)messageId messageSeqNo:(int32_t)messageSeqNo @@ -948,7 +956,7 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; [msgsAckBuffer appendInt64:(int64_t)[nMessageId longLongValue]]; } - MTOutgoingMessage *outgoingMessage = [[MTOutgoingMessage alloc] initWithData:msgsAckBuffer.data metadata:@"msgsAck" shortMetadata:@"msgsAck"]; + MTOutgoingMessage *outgoingMessage = [[MTOutgoingMessage alloc] initWithData:msgsAckBuffer.data metadata:@"msgsAck" additionalDebugDescription:nil shortMetadata:@"msgsAck"]; outgoingMessage.requiresConfirmation = false; [messageTransactions addObject:[[MTMessageTransaction alloc] initWithMessagePayload:@[outgoingMessage] prepared:nil failed:nil completion:^(__unused NSDictionary *messageInternalIdToTransactionId, NSDictionary *messageInternalIdToPreparedMessage, __unused NSDictionary *messageInternalIdToQuickAckId) @@ -985,17 +993,6 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; { for (MTOutgoingMessage *outgoingMessage in messageTransaction.messagePayload) { - NSData *messageData = outgoingMessage.data; - - if (outgoingMessage.dynamicDecorator != nil) - { - id decoratedData = outgoingMessage.dynamicDecorator(messageData, messageInternalIdToPreparedMessage); - if (decoratedData != nil) - messageData = decoratedData; - } - - NSData *data = messageData; - int64_t messageId = 0; int32_t messageSeqNo = 0; if (outgoingMessage.messageId == 0) @@ -1009,18 +1006,19 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; messageSeqNo = outgoingMessage.messageSeqNo; } + NSData *messageData = outgoingMessage.data; + + if (outgoingMessage.dynamicDecorator != nil) + { + id decoratedData = outgoingMessage.dynamicDecorator(messageId, messageData, messageInternalIdToPreparedMessage); + if (decoratedData != nil) + messageData = decoratedData; + } + + NSData *data = messageData; + if (MTLogEnabled()) { NSString *messageDescription = [self outgoingMessageDescription:outgoingMessage messageId:messageId messageSeqNo:messageSeqNo]; - /*if ([messageDescription hasPrefix:@"updates.getDifference"]) { - static dispatch_once_t onceToken; - __block bool flag = false; - dispatch_once(&onceToken, ^{ - flag = true; - }); - if (flag) { - debugResetTransport = true; - } - }*/ MTLog(@"[MTProto#%p@%p preparing %@]", self, _context, messageDescription); } NSString *shortMessageDescription = [self outgoingShortMessageDescription:outgoingMessage messageId:messageId messageSeqNo:messageSeqNo]; @@ -2033,7 +2031,17 @@ static NSString *dumpHexString(NSData *data, int maxLength) { } - (void)transportHasIncomingData:(MTTransport *)transport scheme:(MTTransportScheme *)scheme data:(NSData *)data transactionId:(id)transactionId requestTransactionAfterProcessing:(bool)requestTransactionAfterProcessing decodeResult:(void (^)(id transactionId, bool success))decodeResult -{ +{ + /*__block bool simulateError = false; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + simulateError = true; + }); + if (simulateError) { + int32_t protocolErrorCode = -404; + data = [NSData dataWithBytes:&protocolErrorCode length:4]; + }*/ + [[MTProto managerQueue] dispatchOnQueue:^ { if (_transport != transport || [self isStopped]) @@ -2090,6 +2098,9 @@ static NSString *dumpHexString(NSData *data, int maxLength) { int64_t dataMessageId = 0; bool parseError = false; NSArray *parsedMessages = [self _parseIncomingMessages:decryptedData dataMessageId:&dataMessageId parseError:&parseError]; + + + for (MTIncomingMessage *message in parsedMessages) { if ([message.body isKindOfClass:[MTRpcResultMessage class]]) { MTRpcResultMessage *rpcResultMessage = message.body; @@ -2103,7 +2114,7 @@ static NSString *dumpHexString(NSData *data, int maxLength) { MTShortLog(@"[MTProto#%p@%p received AUTH_KEY_PERM_EMPTY]", self, _context); [self handleMissingKey:scheme.address]; [self requestSecureTransportReset]; - + return; } } @@ -2151,7 +2162,8 @@ static NSString *dumpHexString(NSData *data, int maxLength) { - (void)handleMissingKey:(MTDatacenterAddress *)address { NSAssert([[MTProto managerQueue] isCurrentQueue], @"invalid queue"); - if (_cdn) { + if (_useExplicitAuthKey != nil) { + } else if (_cdn) { _authInfo = nil; [_context performBatchUpdates:^{ [_context updateAuthInfoForDatacenterWithId:_datacenterId authInfo:nil]; @@ -2193,6 +2205,8 @@ static NSString *dumpHexString(NSData *data, int maxLength) { [_context authInfoForDatacenterWithIdRequired:_datacenterId isCdn:false]; }]; _mtState |= MTProtoStateAwaitingDatacenterAuthorization; + } else { + [_context checkIfLoggedOut:_datacenterId]; } } } @@ -2666,7 +2680,12 @@ static NSString *dumpHexString(NSData *data, int maxLength) { NSMutableDictionary *authKeyAttributes = [[NSMutableDictionary alloc] initWithDictionary:_authInfo.authKeyAttributes]; [authKeyAttributes removeObjectForKey:@"apiInitializationHash"]; _authInfo = [_authInfo withUpdatedAuthKeyAttributes:authKeyAttributes]; - [_context updateAuthInfoForDatacenterWithId:_datacenterId authInfo:_authInfo]; + if (_useExplicitAuthKey == nil) { + [_context updateAuthInfoForDatacenterWithId:_datacenterId authInfo:_authInfo]; + } + if (_tempAuthKeyBindingResultUpdated) { + _tempAuthKeyBindingResultUpdated(true); + } } _bindingTempAuthKeyId = 0; if ((_mtState & MTProtoStateBindingTempAuthKey) != 0) { @@ -2680,20 +2699,29 @@ static NSString *dumpHexString(NSData *data, int maxLength) { MTShortLog(@"[MTProto#%p@%p bindTempAuthKey error %@]", self, _context, rpcError); [self requestTransportTransaction]; + + if (_tempAuthKeyBindingResultUpdated) { + _tempAuthKeyBindingResultUpdated(false); + } } } } } -- (void)contextDatacenterTransportSchemesUpdated:(MTContext *)context datacenterId:(NSInteger)datacenterId { +- (void)contextDatacenterTransportSchemesUpdated:(MTContext *)context datacenterId:(NSInteger)datacenterId shouldReset:(bool)shouldReset { [[MTProto managerQueue] dispatchOnQueue:^ { if (context == _context && datacenterId == _datacenterId && ![self isStopped]) { + bool resolvedShouldReset = shouldReset; + if (_mtState & MTProtoStateAwaitingDatacenterScheme) { [self setMtState:_mtState & (~MTProtoStateAwaitingDatacenterScheme)]; + resolvedShouldReset = true; } - [self resetTransport]; - [self requestTransportTransaction]; + if (resolvedShouldReset) { + [self resetTransport]; + [self requestTransportTransaction]; + } } }]; } @@ -2705,6 +2733,9 @@ static NSString *dumpHexString(NSData *data, int maxLength) { if (!_useUnauthorizedMode && context == _context && datacenterId == _datacenterId) { _authInfo = authInfo; + if (_useExplicitAuthKey != nil) { + _authInfo = [_authInfo withUpdatedTempAuthKeyWithType:MTDatacenterAuthTempKeyTypeMain key:_useExplicitAuthKey]; + } bool wasSuspended = _mtState & (MTProtoStateAwaitingDatacenterAuthorization | MTProtoStateAwaitingDatacenterTempAuthKey); diff --git a/submodules/MtProtoKit/MTProtoKit/MTRequestMessageService.m b/submodules/MtProtoKit/MTProtoKit/MTRequestMessageService.m index 17c09abe21..0720adc1cc 100644 --- a/submodules/MtProtoKit/MTProtoKit/MTRequestMessageService.m +++ b/submodules/MtProtoKit/MTProtoKit/MTRequestMessageService.m @@ -285,10 +285,12 @@ } } -- (NSData *)decorateRequestData:(MTRequest *)request initializeApi:(bool)initializeApi unresolvedDependencyOnRequestInternalId:(__autoreleasing id *)unresolvedDependencyOnRequestInternalId +- (NSData *)decorateRequestData:(MTRequest *)request initializeApi:(bool)initializeApi unresolvedDependencyOnRequestInternalId:(__autoreleasing id *)unresolvedDependencyOnRequestInternalId decoratedDebugDescription:(__autoreleasing NSString **)decoratedDebugDescription { NSData *currentData = request.payload; + NSString *debugDescription = @""; + if (initializeApi && _apiEnvironment != nil) { if (MTLogEnabled()) { @@ -345,6 +347,8 @@ [buffer appendBytes:currentData.bytes length:currentData.length]; currentData = buffer.data; + + debugDescription = [debugDescription stringByAppendingString:@", disableUpdates"]; } if (request.shouldDependOnRequest != nil) @@ -352,9 +356,12 @@ NSUInteger index = [_requests indexOfObject:request]; if (index != NSNotFound) { - for (NSInteger i = ((NSInteger)index) - 1; i >= 0; i--) + for (MTRequest *anotherRequest in _requests.reverseObjectEnumerator) { - MTRequest *anotherRequest = _requests[(NSUInteger)i]; + if (request == anotherRequest) { + continue; + } + if (request.shouldDependOnRequest(anotherRequest)) { if (anotherRequest.requestContext != nil) @@ -367,9 +374,13 @@ [buffer appendBytes:currentData.bytes length:currentData.length]; currentData = buffer.data; + + debugDescription = [debugDescription stringByAppendingFormat:@", invokeAfter(%lld)", anotherRequest.requestContext.messageId]; } - else if (unresolvedDependencyOnRequestInternalId != nil) + else if (unresolvedDependencyOnRequestInternalId != nil) { *unresolvedDependencyOnRequestInternalId = anotherRequest.internalId; + debugDescription = [debugDescription stringByAppendingString:@", unresolvedDependency"]; + } break; } @@ -377,6 +388,10 @@ } } + if (decoratedDebugDescription != nil) { + *decoratedDebugDescription = debugDescription; + } + return currentData; } @@ -410,6 +425,7 @@ requestInternalIdToMessageInternalId = [[NSMutableDictionary alloc] init]; __autoreleasing id autoreleasingUnresolvedDependencyOnRequestInternalId = nil; + __autoreleasing NSString *decoratedDebugDescription = nil; int64_t messageId = 0; int32_t messageSeqNo = 0; @@ -419,14 +435,16 @@ messageSeqNo = request.requestContext.messageSeqNo; } - MTOutgoingMessage *outgoingMessage = [[MTOutgoingMessage alloc] initWithData:[self decorateRequestData:request initializeApi:requestsWillInitializeApi unresolvedDependencyOnRequestInternalId:&autoreleasingUnresolvedDependencyOnRequestInternalId] metadata:request.metadata shortMetadata:request.shortMetadata messageId:messageId messageSeqNo:messageSeqNo]; + NSData *decoratedRequestData = [self decorateRequestData:request initializeApi:requestsWillInitializeApi unresolvedDependencyOnRequestInternalId:&autoreleasingUnresolvedDependencyOnRequestInternalId decoratedDebugDescription:&decoratedDebugDescription]; + + MTOutgoingMessage *outgoingMessage = [[MTOutgoingMessage alloc] initWithData:decoratedRequestData metadata:request.metadata additionalDebugDescription:decoratedDebugDescription shortMetadata:request.shortMetadata messageId:messageId messageSeqNo:messageSeqNo]; outgoingMessage.needsQuickAck = request.acknowledgementReceived != nil; outgoingMessage.hasHighPriority = request.hasHighPriority; id unresolvedDependencyOnRequestInternalId = autoreleasingUnresolvedDependencyOnRequestInternalId; if (unresolvedDependencyOnRequestInternalId != nil) { - outgoingMessage.dynamicDecorator = ^id (NSData *currentData, NSDictionary *messageInternalIdToPreparedMessage) + outgoingMessage.dynamicDecorator = ^id (int64_t currentMessageId, NSData *currentData, NSDictionary *messageInternalIdToPreparedMessage) { id messageInternalId = requestInternalIdToMessageInternalId[unresolvedDependencyOnRequestInternalId]; if (messageInternalId != nil) @@ -438,6 +456,9 @@ [invokeAfterBuffer appendInt32:(int32_t)0xcb9f372d]; [invokeAfterBuffer appendInt64:preparedMessage.messageId]; [invokeAfterBuffer appendBytes:currentData.bytes length:currentData.length]; + if (MTLogEnabled()) { + MTLog(@"[MTRequestMessageService] %lld dynamically added invokeAfter %lld", currentMessageId, preparedMessage.messageId); + } return invokeAfterBuffer.data; } } @@ -464,7 +485,7 @@ [dropAnswerBuffer appendInt64:dropContext.dropMessageId]; NSString *messageDecription = [NSString stringWithFormat:@"dropAnswer for %" PRId64, dropContext.dropMessageId]; - MTOutgoingMessage *outgoingMessage = [[MTOutgoingMessage alloc] initWithData:dropAnswerBuffer.data metadata:messageDecription shortMetadata:messageDecription messageId:dropContext.messageId messageSeqNo:dropContext.messageSeqNo]; + MTOutgoingMessage *outgoingMessage = [[MTOutgoingMessage alloc] initWithData:dropAnswerBuffer.data metadata:messageDecription additionalDebugDescription:nil shortMetadata:messageDecription messageId:dropContext.messageId messageSeqNo:dropContext.messageSeqNo]; outgoingMessage.requiresConfirmation = false; dropMessageIdToMessageInternalId[@(dropContext.dropMessageId)] = outgoingMessage.internalId; [messages addObject:outgoingMessage]; diff --git a/submodules/MtProtoKit/MTProtoKit/MTResendMessageService.m b/submodules/MtProtoKit/MTProtoKit/MTResendMessageService.m index ba3651d445..b876b8caa0 100644 --- a/submodules/MtProtoKit/MTProtoKit/MTResendMessageService.m +++ b/submodules/MtProtoKit/MTProtoKit/MTResendMessageService.m @@ -57,7 +57,7 @@ NSData *resentMessagesRequestData = resendRequestBuffer.data; - MTOutgoingMessage *outgoingMessage = [[MTOutgoingMessage alloc] initWithData:resentMessagesRequestData metadata:@"resendMessages" shortMetadata:@"resendMessages"]; + MTOutgoingMessage *outgoingMessage = [[MTOutgoingMessage alloc] initWithData:resentMessagesRequestData metadata:@"resendMessages" additionalDebugDescription:nil shortMetadata:@"resendMessages"]; outgoingMessage.requiresConfirmation = false; return [[MTMessageTransaction alloc] initWithMessagePayload:@[outgoingMessage] prepared:nil failed:nil completion:^(NSDictionary *messageInternalIdToTransactionId, NSDictionary *messageInternalIdToPreparedMessage, __unused NSDictionary *messageInternalIdToQuickAckId) diff --git a/submodules/MtProtoKit/MTProtoKit/MTTcpTransport.m b/submodules/MtProtoKit/MTProtoKit/MTTcpTransport.m index c5a3d10cb3..eed908931b 100644 --- a/submodules/MtProtoKit/MTProtoKit/MTTcpTransport.m +++ b/submodules/MtProtoKit/MTProtoKit/MTTcpTransport.m @@ -620,7 +620,7 @@ static const NSTimeInterval MTTcpTransportSleepWatchdogTimeout = 60.0; [pingBuffer appendInt32:(int32_t)0x7abe77ec]; [pingBuffer appendInt64:randomId]; - MTOutgoingMessage *outgoingMessage = [[MTOutgoingMessage alloc] initWithData:pingBuffer.data metadata:@"ping" shortMetadata:@"ping"]; + MTOutgoingMessage *outgoingMessage = [[MTOutgoingMessage alloc] initWithData:pingBuffer.data metadata:@"ping" additionalDebugDescription:nil shortMetadata:@"ping"]; outgoingMessage.requiresConfirmation = false; __weak MTTcpTransport *weakSelf = self; diff --git a/submodules/MtProtoKit/MTProtoKit/MTTimeSyncMessageService.m b/submodules/MtProtoKit/MTProtoKit/MTTimeSyncMessageService.m index a39efe07fb..f78e824f2f 100644 --- a/submodules/MtProtoKit/MTProtoKit/MTTimeSyncMessageService.m +++ b/submodules/MtProtoKit/MTProtoKit/MTTimeSyncMessageService.m @@ -59,7 +59,7 @@ [getFutureSaltsBuffer appendInt32:(int32_t)0xb921bd04]; [getFutureSaltsBuffer appendInt32:_futureSalts.count != 0 ? 1 : 32]; - MTOutgoingMessage *outgoingMessage = [[MTOutgoingMessage alloc] initWithData:getFutureSaltsBuffer.data metadata:@"getFutureSalts" shortMetadata:@"getFutureSalts"]; + MTOutgoingMessage *outgoingMessage = [[MTOutgoingMessage alloc] initWithData:getFutureSaltsBuffer.data metadata:@"getFutureSalts" additionalDebugDescription:nil shortMetadata:@"getFutureSalts"]; return [[MTMessageTransaction alloc] initWithMessagePayload:@[outgoingMessage] prepared:nil failed:nil completion:^(NSDictionary *messageInternalIdToTransactionId, NSDictionary *messageInternalIdToPreparedMessage, __unused NSDictionary *messageInternalIdToQuickAckId) { diff --git a/submodules/NotificationMuteSettingsUI/Sources/NotificationMuteSettingsController.swift b/submodules/NotificationMuteSettingsUI/Sources/NotificationMuteSettingsController.swift index 513c4319c9..953e997502 100644 --- a/submodules/NotificationMuteSettingsUI/Sources/NotificationMuteSettingsController.swift +++ b/submodules/NotificationMuteSettingsUI/Sources/NotificationMuteSettingsController.swift @@ -23,7 +23,7 @@ public struct NotificationSoundSettings { } public func notificationMuteSettingsController(presentationData: PresentationData, notificationSettings: MessageNotificationSettings, soundSettings: NotificationSoundSettings?, openSoundSettings: @escaping () -> Void, updateSettings: @escaping (Int32?) -> Void) -> ViewController { - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } diff --git a/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift index e63fab7609..5a6806fcf7 100644 --- a/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift +++ b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift @@ -125,23 +125,23 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! NotificationSoundSelectionArguments switch self { case let.modernHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .classicHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .none(_, theme, text, selected): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: selected, zeroSeparatorInsets: true, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: true, sectionId: self.section, action: { arguments.selectSound(.none) }) case let .default(_, theme, text, selected): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.selectSound(.default) }) case let .sound(_, _, theme, text, sound, selected): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.selectSound(sound) }) } @@ -294,8 +294,8 @@ public func notificationSoundSelectionController(context: AccountContext, isModa arguments.complete() }) - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Notifications_TextTone), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: notificationsAndSoundsEntries(presentationData: presentationData, defaultSound: defaultSound, state: state), style: .blocks) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Notifications_TextTone), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: notificationsAndSoundsEntries(presentationData: presentationData, defaultSound: defaultSound, state: state), style: .blocks) return (controllerState, (listState, arguments)) } @@ -304,6 +304,9 @@ public func notificationSoundSelectionController(context: AccountContext, isModa playSoundDisposable.dispose() }) controller.enableInteractiveDismiss = true + if isModal { + controller.navigationPresentation = .modal + } completeImpl = { [weak controller] in let sound = stateValue.with { state in diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInActionSheetController.swift b/submodules/OpenInExternalAppUI/Sources/OpenInActionSheetController.swift index cbb3711de7..3e84e47d75 100644 --- a/submodules/OpenInExternalAppUI/Sources/OpenInActionSheetController.swift +++ b/submodules/OpenInExternalAppUI/Sources/OpenInActionSheetController.swift @@ -35,11 +35,11 @@ public final class OpenInActionSheetController: ActionSheetController { let theme = presentationData.theme let strings = presentationData.strings - super.init(theme: ActionSheetControllerTheme(presentationTheme: theme)) + super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationTheme: presentationData.theme) + strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) } }) @@ -116,9 +116,6 @@ private final class OpenInActionSheetItem: ActionSheetItem { } } -private let titleFont = Font.medium(20.0) -private let textFont = Font.regular(11.0) - private final class OpenInActionSheetItemNode: ActionSheetItemNode { let theme: ActionSheetControllerTheme let strings: PresentationStrings @@ -132,6 +129,9 @@ private final class OpenInActionSheetItemNode: ActionSheetItemNode { self.theme = theme self.strings = strings + let titleFont = Font.medium(floor(theme.baseFontSize * 20.0 / 17.0)) + let textFont = Font.regular(floor(theme.baseFontSize * 11.0 / 17.0)) + self.titleNode = ASTextNode() self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = true @@ -216,6 +216,7 @@ private final class OpenInAppNode : ASDisplayNode { } func setup(postbox: Postbox, context: AccountContext, theme: ActionSheetControllerTheme, option: OpenInOption, invokeAction: @escaping (OpenInAction) -> Void) { + let textFont = Font.regular(floor(theme.baseFontSize * 11.0 / 17.0)) self.textNode.attributedText = NSAttributedString(string: option.title, font: textFont, textColor: theme.primaryTextColor, paragraphAlignment: .center) let iconSize = CGSize(width: 60.0, height: 60.0) diff --git a/submodules/PasscodeUI/Sources/PasscodeSetupController.swift b/submodules/PasscodeUI/Sources/PasscodeSetupController.swift index b3c400ae9c..33a9f3cb4a 100644 --- a/submodules/PasscodeUI/Sources/PasscodeSetupController.swift +++ b/submodules/PasscodeUI/Sources/PasscodeSetupController.swift @@ -59,7 +59,7 @@ public final class PasscodeSetupController: ViewController { return } - let controller = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let controller = ActionSheetController(presentationData: strongSelf.presentationData) let dismissAction: () -> Void = { [weak controller] in self?.controllerNode.activateInput() controller?.dismissAnimated() diff --git a/submodules/PassportUI/Sources/SecureIdAuthController.swift b/submodules/PassportUI/Sources/SecureIdAuthController.swift index a21449e859..1df3ddb5a0 100644 --- a/submodules/PassportUI/Sources/SecureIdAuthController.swift +++ b/submodules/PassportUI/Sources/SecureIdAuthController.swift @@ -330,7 +330,7 @@ public final class SecureIdAuthController: ViewController, StandalonePresentable guard let strongSelf = self else { return } - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) } }) diff --git a/submodules/PassportUI/Sources/SecureIdAuthControllerNode.swift b/submodules/PassportUI/Sources/SecureIdAuthControllerNode.swift index 0c1dc472dd..030978b94c 100644 --- a/submodules/PassportUI/Sources/SecureIdAuthControllerNode.swift +++ b/submodules/PassportUI/Sources/SecureIdAuthControllerNode.swift @@ -45,7 +45,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { self.activityIndicator.isHidden = true self.scrollNode = ASScrollNode() - self.headerNode = SecureIdAuthHeaderNode(account: context.account, theme: presentationData.theme, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder) + self.headerNode = SecureIdAuthHeaderNode(context: context, theme: presentationData.theme, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder) self.acceptNode = SecureIdAuthAcceptNode(title: presentationData.strings.Passport_Authorize, theme: presentationData.theme) super.init() @@ -727,7 +727,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { } if let currentValue = currentValue { - let controller = ActionSheetController(presentationTheme: self.presentationData.theme) + let controller = ActionSheetController(presentationData: self.presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -847,7 +847,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { return } - let controller = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let controller = ActionSheetController(presentationData: strongSelf.presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -916,7 +916,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { ] } - let controller = ActionSheetController(presentationTheme: self.presentationData.theme) + let controller = ActionSheetController(presentationData: self.presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -961,7 +961,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { } private func deleteAllValues() { - let controller = ActionSheetController(presentationTheme: self.presentationData.theme) + let controller = ActionSheetController(presentationData: self.presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } diff --git a/submodules/PassportUI/Sources/SecureIdAuthHeaderNode.swift b/submodules/PassportUI/Sources/SecureIdAuthHeaderNode.swift index 3e0fda862a..81eb358d81 100644 --- a/submodules/PassportUI/Sources/SecureIdAuthHeaderNode.swift +++ b/submodules/PassportUI/Sources/SecureIdAuthHeaderNode.swift @@ -10,13 +10,14 @@ import TelegramPresentationData import TelegramUIPreferences import AvatarNode import AppBundle +import AccountContext private let avatarFont = avatarPlaceholderFont(size: 26.0) private let titleFont = Font.semibold(14.0) private let textFont = Font.regular(14.0) final class SecureIdAuthHeaderNode: ASDisplayNode { - private let account: Account + private let context: AccountContext private let theme: PresentationTheme private let strings: PresentationStrings private let nameDisplayOrder: PresentationPersonNameOrder @@ -27,8 +28,8 @@ final class SecureIdAuthHeaderNode: ASDisplayNode { private var verificationState: SecureIdAuthControllerVerificationState? - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) { - self.account = account + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) { + self.context = context self.theme = theme self.strings = strings self.nameDisplayOrder = nameDisplayOrder @@ -53,7 +54,7 @@ final class SecureIdAuthHeaderNode: ASDisplayNode { func updateState(formData: SecureIdEncryptedFormData?, verificationState: SecureIdAuthControllerVerificationState) { if let formData = formData { - self.serviceAvatarNode.setPeer(account: self.account, theme: self.theme, peer: formData.servicePeer) + self.serviceAvatarNode.setPeer(context: self.context, theme: self.theme, peer: formData.servicePeer) let titleData = self.strings.Passport_RequestHeader(formData.servicePeer.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder)) let titleString = NSMutableAttributedString() diff --git a/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift b/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift index e2b30260c5..e0f44699b3 100644 --- a/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift +++ b/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift @@ -2143,6 +2143,7 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode Void = { [weak controller] in controller?.dismissAnimated() } @@ -2756,7 +2758,7 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode Void = { [weak controller] in controller?.dismissAnimated() } @@ -2816,7 +2818,7 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode Void = { [weak controller] in controller?.dismissAnimated() } @@ -3035,7 +3037,7 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode Void) -> GalleryItem { - return SecureIdDocumentGalleryItem(context: context, theme: theme, strings: strings, secureIdContext: secureIdContext, resource: self.resource, caption: self.error, location: self.location, delete: { + return SecureIdDocumentGalleryItem(context: context, theme: theme, strings: strings, secureIdContext: secureIdContext, itemId: self.index, resource: self.resource, caption: self.error, location: self.location, delete: { delete(self.resource) }) } @@ -68,7 +68,7 @@ class SecureIdDocumentGalleryController: ViewController, StandalonePresentableCo private let centralItemTitle = Promise() private let centralItemTitleView = Promise() private let centralItemNavigationStyle = Promise() - private let centralItemFooterContentNode = Promise() + private let centralItemFooterContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>() private let centralItemAttributesDisposable = DisposableSet(); private let _hiddenMedia = Promise(nil) @@ -123,7 +123,7 @@ class SecureIdDocumentGalleryController: ViewController, StandalonePresentableCo self?.navigationItem.titleView = titleView })) - self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode in + self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode, _ in self?.galleryNode.updatePresentationState({ $0.withUpdatedFooterContentNode(footerContentNode) }, transition: .immediate) diff --git a/submodules/PassportUI/Sources/SecureIdDocumentGalleryFooterContentNode.swift b/submodules/PassportUI/Sources/SecureIdDocumentGalleryFooterContentNode.swift index b6a15328d3..269c1bdbdc 100644 --- a/submodules/PassportUI/Sources/SecureIdDocumentGalleryFooterContentNode.swift +++ b/submodules/PassportUI/Sources/SecureIdDocumentGalleryFooterContentNode.swift @@ -143,7 +143,7 @@ final class SecureIdDocumentGalleryFooterContentNode: GalleryFooterContentNode { @objc func deleteButtonPressed() { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) let items: [ActionSheetItem] = [ ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() @@ -153,7 +153,7 @@ final class SecureIdDocumentGalleryFooterContentNode: GalleryFooterContentNode { actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) diff --git a/submodules/PassportUI/Sources/SecureIdDocumentImageGalleryItem.swift b/submodules/PassportUI/Sources/SecureIdDocumentImageGalleryItem.swift index 4ceaa45422..d1315f41a7 100644 --- a/submodules/PassportUI/Sources/SecureIdDocumentImageGalleryItem.swift +++ b/submodules/PassportUI/Sources/SecureIdDocumentImageGalleryItem.swift @@ -12,6 +12,12 @@ import PhotoResources import GalleryUI class SecureIdDocumentGalleryItem: GalleryItem { + var id: AnyHashable { + return self.itemId + } + + let itemId: AnyHashable + let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings @@ -21,7 +27,8 @@ class SecureIdDocumentGalleryItem: GalleryItem { let location: SecureIdDocumentGalleryEntryLocation let delete: () -> Void - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, secureIdContext: SecureIdAccessContext, resource: TelegramMediaResource, caption: String, location: SecureIdDocumentGalleryEntryLocation, delete: @escaping () -> Void) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, secureIdContext: SecureIdAccessContext, itemId: AnyHashable, resource: TelegramMediaResource, caption: String, location: SecureIdDocumentGalleryEntryLocation, delete: @escaping () -> Void) { + self.itemId = itemId self.context = context self.theme = theme self.strings = strings @@ -131,13 +138,13 @@ final class SecureIdDocumentGalleryItemNode: ZoomableContentGalleryItemNode { self.contextAndMedia = (context, secureIdContext, resource) } - override func animateIn(from node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { + override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view) let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view.superview) let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) let transformedCopyViewFinalFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view) - let copyView = node.1().0! + let copyView = node.2().0! self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame @@ -157,7 +164,7 @@ final class SecureIdDocumentGalleryItemNode: ZoomableContentGalleryItemNode { self.imageNode.layer.animateBounds(from: transformedFrame, to: self.imageNode.layer.bounds, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) } - override func animateOut(to node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view) let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view.superview) let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) @@ -167,7 +174,7 @@ final class SecureIdDocumentGalleryItemNode: ZoomableContentGalleryItemNode { var boundsCompleted = false var copyCompleted = false - let copyView = node.1().0! + let copyView = node.2().0! self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame @@ -210,8 +217,8 @@ final class SecureIdDocumentGalleryItemNode: ZoomableContentGalleryItemNode { return self._title.get() } - override func footerContent() -> Signal { - return .single(self.footerContentNode) + override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { + return .single((self.footerContentNode, nil)) } } diff --git a/submodules/PassportUI/Sources/SecureIdDocumentTypeSelectionController.swift b/submodules/PassportUI/Sources/SecureIdDocumentTypeSelectionController.swift index ba7ffea302..08b80c1d04 100644 --- a/submodules/PassportUI/Sources/SecureIdDocumentTypeSelectionController.swift +++ b/submodules/PassportUI/Sources/SecureIdDocumentTypeSelectionController.swift @@ -89,11 +89,11 @@ final class SecureIdDocumentTypeSelectionController: ActionSheetController { self.completion = completion - super.init(theme: ActionSheetControllerTheme(presentationTheme: theme)) + super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationTheme: presentationData.theme) + strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) } }) diff --git a/submodules/PasswordSetupUI/Resources/TwoFactorSetupMonkeyIdle1.tgs b/submodules/PasswordSetupUI/Resources/TwoFactorSetupMonkeyIdle1.tgs new file mode 100644 index 0000000000..974a1b0eb6 Binary files /dev/null and b/submodules/PasswordSetupUI/Resources/TwoFactorSetupMonkeyIdle1.tgs differ diff --git a/submodules/PasswordSetupUI/Resources/TwoFactorSetupMonkeyIdle2.tgs b/submodules/PasswordSetupUI/Resources/TwoFactorSetupMonkeyIdle2.tgs new file mode 100644 index 0000000000..52460f74d2 Binary files /dev/null and b/submodules/PasswordSetupUI/Resources/TwoFactorSetupMonkeyIdle2.tgs differ diff --git a/submodules/PasswordSetupUI/Sources/ManagedAnimationNode.swift b/submodules/PasswordSetupUI/Sources/ManagedAnimationNode.swift index d7eaace4bd..cbfeb1526c 100644 --- a/submodules/PasswordSetupUI/Sources/ManagedAnimationNode.swift +++ b/submodules/PasswordSetupUI/Sources/ManagedAnimationNode.swift @@ -5,6 +5,7 @@ import AsyncDisplayKit import RLottieBinding import AppBundle import GZip +import SwiftSignalKit private final class ManagedAnimationState { let item: ManagedAnimationItem @@ -14,7 +15,7 @@ private final class ManagedAnimationState { let frameCount: Int let fps: Double - var startTime: Double? + var relativeTime: Double = 0.0 var frameIndex: Int? private let renderContext: DrawingContext @@ -65,6 +66,7 @@ struct ManagedAnimationFrameRange: Equatable { struct ManagedAnimationItem: Equatable { let name: String var frames: ManagedAnimationFrameRange + var duration: Double } class ManagedAnimationNode: ASDisplayNode { @@ -73,7 +75,9 @@ class ManagedAnimationNode: ASDisplayNode { private let imageNode: ASImageNode private let displayLink: CADisplayLink - private var state: ManagedAnimationState? + fileprivate var state: ManagedAnimationState? + fileprivate var trackStack: [ManagedAnimationItem] = [] + fileprivate var didTryAdvancingState = false init(size: CGSize) { self.intrinsicSize = size @@ -110,30 +114,43 @@ class ManagedAnimationNode: ASDisplayNode { } } - private func updateAnimation() { + func advanceState() { + guard !self.trackStack.isEmpty else { + return + } + + let item = self.trackStack.removeFirst() + + if let state = self.state, state.item.name == item.name { + self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state) + } else { + self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: nil) + } + + self.didTryAdvancingState = false + } + + fileprivate func updateAnimation() { + if self.state == nil { + self.advanceState() + } + guard let state = self.state else { return } let timestamp = CACurrentMediaTime() - var startTime: Double - if let current = state.startTime { - startTime = current - } else { - startTime = timestamp - state.startTime = startTime - } - let fps = state.fps let frameRange = state.item.frames - let duration: Double = 0.3 - var t = (timestamp - startTime) / duration + let duration: Double = state.item.duration + var t = state.relativeTime / duration t = max(0.0, t) t = min(1.0, t) - let frameOffset = Int(Double(frameRange.startFrame) * (1.0 - t) + Double(frameRange.startFrame) * t) - let lowerBound = min(frameRange.startFrame, state.frameCount - 1) - let upperBound = min(frameRange.endFrame, state.frameCount - 1) + //print("\(t) \(state.item.name)") + let frameOffset = Int(Double(frameRange.startFrame) * (1.0 - t) + Double(frameRange.endFrame) * t) + let lowerBound: Int = 0 + let upperBound = state.frameCount - 1 let frameIndex = max(lowerBound, min(upperBound, frameOffset)) if state.frameIndex != frameIndex { @@ -142,142 +159,181 @@ class ManagedAnimationNode: ASDisplayNode { self.imageNode.image = image } } + + var animationAdvancement: Double = 1.0 / 60.0 + animationAdvancement *= Double(min(2, self.trackStack.count + 1)) + + state.relativeTime += animationAdvancement + + if state.relativeTime >= duration && !self.didTryAdvancingState { + self.didTryAdvancingState = true + self.advanceState() + } } - func trackTo(item: ManagedAnimationItem, frameIndex: Int) { - if let state = self.state, state.item.name == item.name { - self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state) - } else { - self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: nil) - } + func trackTo(item: ManagedAnimationItem) { + self.trackStack.append(item) + self.didTryAdvancingState = false self.updateAnimation() } } +enum ManagedMonkeyAnimationIdle: CaseIterable { + case blink + case ear + case still +} + enum ManagedMonkeyAnimationState: Equatable { - case idle + case idle(ManagedMonkeyAnimationIdle) case eyesClosed case peeking case tracking(CGFloat) } -/*private let animationIdle = ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", - intro: nil, - loop: ManagedAnimationTrack(frameRange: 0 ..< 1), - outro: nil -) - - private let animationIdle = ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", - intro: nil, - loop: ManagedAnimationTrack(frameRange: 0 ..< 1), - outro: nil - ) - - private let animationTracking = ManagedAnimationItem(name: "TwoFactorSetupMonkeyTracking", - intro: nil, - loop: ManagedAnimationTrack(frameRange: 0 ..< Int.max), - outro: nil - ) - - private let animationHide = ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", - intro: ManagedAnimationTrack(frameRange: 0 ..< 41), - loop: ManagedAnimationTrack(frameRange: 40 ..< 41), - outro: ManagedAnimationTrack(frameRange: 60 ..< 99) - ) - - private let animationHideNoOutro = ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", - intro: ManagedAnimationTrack(frameRange: 0 ..< 41), - loop: ManagedAnimationTrack(frameRange: 40 ..< 41), - outro: nil - ) - - private let animationHideNoIntro = ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", - intro: nil, - loop: ManagedAnimationTrack(frameRange: 40 ..< 41), - outro: ManagedAnimationTrack(frameRange: 60 ..< 99) - ) - - private let animationHideOutro = ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", - intro: nil, - loop: nil, - outro: ManagedAnimationTrack(frameRange: 60 ..< 99) - ) - - private let animationPeek = ManagedAnimationItem(name: "TwoFactorSetupMonkeyPeek", - intro: ManagedAnimationTrack(frameRange: 0 ..< 14), - loop: ManagedAnimationTrack(frameRange: 13 ..< 14), - outro: ManagedAnimationTrack(frameRange: 14 ..< 34) - ) - - private let animationMail = ManagedAnimationItem(name: "TwoFactorSetupMail", - intro: ManagedAnimationTrack(frameRange: 0 ..< Int.max), - loop: ManagedAnimationTrack(frameRange: Int.max - 1 ..< Int.max), - outro: nil - ) - - private let animationHint = ManagedAnimationItem(name: "TwoFactorSetupHint", - intro: ManagedAnimationTrack(frameRange: 0 ..< Int.max), - loop: ManagedAnimationTrack(frameRange: Int.max - 1 ..< Int.max), - outro: nil - )*/ - final class ManagedMonkeyAnimationNode: ManagedAnimationNode { - private var state: ManagedMonkeyAnimationState = .idle + private var monkeyState: ManagedMonkeyAnimationState = .idle(.blink) + private var timer: SwiftSignalKit.Timer? init() { super.init(size: CGSize(width: 136.0, height: 136.0)) - self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0)), frameIndex: 0) + self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3)) } - func setState(_ state: ManagedMonkeyAnimationState) { - let previousState = self.state - self.state = state + deinit { + self.timer?.invalidate() + } + + private func startIdleTimer() { + self.timer?.invalidate() + let timer = SwiftSignalKit.Timer(timeout: Double.random(in: 1.0 ..< 1.5), repeat: false, completion: { [weak self] in + guard let strongSelf = self else { + return + } + switch strongSelf.monkeyState { + case .idle: + if let idle = ManagedMonkeyAnimationIdle.allCases.randomElement() { + strongSelf.setState(.idle(idle)) + } + default: + break + } + }, queue: .mainQueue()) + self.timer = timer + timer.start() + } + + override func advanceState() { + super.advanceState() + + self.timer?.invalidate() + self.timer = nil + + if self.trackStack.isEmpty, case .idle = self.monkeyState { + self.startIdleTimer() + } + } + + private func enqueueIdle(_ idle: ManagedMonkeyAnimationIdle) { + switch idle { + case .still: + self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3)) + case .blink: + self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle1", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 30), duration: 0.3)) + case .ear: + self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle2", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 30), duration: 0.3)) + //self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 179), duration: 3.0)) + } + } + + func setState(_ monkeyState: ManagedMonkeyAnimationState) { + let previousState = self.monkeyState + self.monkeyState = monkeyState + + self.timer?.invalidate() + self.timer = nil + + func enqueueTracking(_ value: CGFloat) { + let lowerBound = 18 + let upperBound = 160 + let frameIndex = lowerBound + Int(value * CGFloat(upperBound - lowerBound)) + if let state = self.state, state.item.name == "TwoFactorSetupMonkeyTracking" { + let item = ManagedAnimationItem(name: "TwoFactorSetupMonkeyTracking", frames: ManagedAnimationFrameRange(startFrame: state.frameIndex ?? 0, endFrame: frameIndex), duration: 0.3) + self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state) + self.didTryAdvancingState = false + self.updateAnimation() + } else { + self.trackStack = self.trackStack.filter { + $0.name != "TwoFactorSetupMonkeyTracking" + } + self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyTracking", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: frameIndex), duration: 0.3)) + } + } + + func enqueueClearTracking() { + if let state = self.state, state.item.name == "TwoFactorSetupMonkeyTracking" { + let item = ManagedAnimationItem(name: "TwoFactorSetupMonkeyTracking", frames: ManagedAnimationFrameRange(startFrame: state.frameIndex ?? 0, endFrame: 0), duration: 0.3) + self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state) + self.didTryAdvancingState = false + self.updateAnimation() + } + } switch previousState { - case .idle: - switch state { - case .idle: - break + case let .idle(previousIdle): + switch monkeyState { + case let .idle(idle): + self.enqueueIdle(idle) case .eyesClosed: - break + self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3)) case .peeking: - break + self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3)) case let .tracking(value): - break + enqueueTracking(value) } case .eyesClosed: - switch state { - case .idle: - break + switch monkeyState { + case let .idle(idle): + self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3)) + self.enqueueIdle(idle) case .eyesClosed: break case .peeking: - break + self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyPeek", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 14), duration: 0.3)) case let .tracking(value): - break + self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3)) + enqueueTracking(value) } case .peeking: - switch state { - case .idle: - break + switch monkeyState { + case let .idle(idle): + self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3)) + self.enqueueIdle(idle) case .eyesClosed: - break + self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyPeek", frames: ManagedAnimationFrameRange(startFrame: 14, endFrame: 0), duration: 0.3)) case .peeking: break case let .tracking(value): - break + self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3)) + enqueueTracking(value) } - case let .tracking(previousValue): - switch state { - case .idle: - break + case let .tracking(currentValue): + switch monkeyState { + case let .idle(idle): + enqueueClearTracking() + self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3)) + self.enqueueIdle(idle) case .eyesClosed: - break + enqueueClearTracking() + self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3)) case .peeking: - break + enqueueClearTracking() + self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3)) case let .tracking(value): - break + if abs(currentValue - value) > CGFloat.ulpOfOne { + enqueueTracking(value) + } } } } diff --git a/submodules/PasswordSetupUI/Sources/ResetPasswordController.swift b/submodules/PasswordSetupUI/Sources/ResetPasswordController.swift index 0f4ef4efa5..db3835105c 100644 --- a/submodules/PasswordSetupUI/Sources/ResetPasswordController.swift +++ b/submodules/PasswordSetupUI/Sources/ResetPasswordController.swift @@ -68,18 +68,18 @@ private enum ResetPasswordEntry: ItemListNodeEntry, Equatable { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ResetPasswordControllerArguments switch self { case let .code(theme, strings, text, value): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: text, textColor: theme.list.itemPrimaryTextColor), text: value, placeholder: "", type: .number, spacing: 10.0, tag: ResetPasswordEntryTag.code, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: text, textColor: theme.list.itemPrimaryTextColor), text: value, placeholder: "", type: .number, spacing: 10.0, tag: ResetPasswordEntryTag.code, sectionId: self.section, textUpdated: { updatedText in arguments.updateCodeText(updatedText) }, action: { }) case let .codeInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .helpInfo(theme, text): - return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section, linkAction: { action in + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { action in if case .tap = action { arguments.openHelp() } @@ -193,8 +193,8 @@ public func resetPasswordController(context: AccountContext, emailPattern: Strin }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.TwoStepAuth_RecoveryTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: resetPasswordControllerEntries(presentationData: presentationData, state: state, pattern: emailPattern), style: .blocks, focusItemTag: ResetPasswordEntryTag.code, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.TwoStepAuth_RecoveryTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: resetPasswordControllerEntries(presentationData: presentationData, state: state, pattern: emailPattern), style: .blocks, focusItemTag: ResetPasswordEntryTag.code, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationContentNode.swift b/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationContentNode.swift index f2a564d5b7..80a45ac163 100644 --- a/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationContentNode.swift +++ b/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationContentNode.swift @@ -160,7 +160,7 @@ final class SetupTwoStepVerificationContentNode: ASDisplayNode, UITextFieldDeleg let minContentHeight = textHeight + inputHeight let contentHeight = min(215.0, max(size.height - insets.top - insets.bottom - 40.0, minContentHeight)) - let contentOrigin = insets.top + floor((size.height - insets.top - insets.bottom - contentHeight) / 2.0) + let contentOrigin = max(56.0, insets.top + floor((size.height - insets.top - insets.bottom - contentHeight) / 2.0)) let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: contentOrigin), size: titleSize) transition.updateFrame(node: self.titleNode, frame: titleFrame) transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: titleFrame.maxY + titleSubtitleSpacing), size: subtitleSize)) diff --git a/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationControllerNode.swift b/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationControllerNode.swift index b1eb0635ea..550b0f781c 100644 --- a/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationControllerNode.swift +++ b/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationControllerNode.swift @@ -698,7 +698,6 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { }, transition: .animated(duration: 0.5, curve: .spring)) } if case let .enterEmail(enterEmail)? = self.innerState.data.state, case .create = enterEmail.state, enterEmail.email.isEmpty { - self.present(textAlertController(context: self.context, title: nil, text: self.presentationData.strings.TwoStepAuth_EmailSkipAlert, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.TwoStepAuth_EmailSkip, action: { continueImpl() })]), nil) diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift index 98b8278fa4..eadc36ff4a 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift @@ -20,8 +20,6 @@ public enum TwoFactorDataInputMode { case passwordHint(password: String) } - - public final class TwoFactorDataInputScreen: ViewController { private let context: AccountContext private var presentationData: PresentationData @@ -38,14 +36,14 @@ public final class TwoFactorDataInputScreen: ViewController { let defaultTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme) let navigationBarTheme = NavigationBarTheme(buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor) - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Wallet_Navigation_Back, close: self.presentationData.strings.Wallet_Navigation_Close))) + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close))) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.navigationPresentation = .modalInLargeLayout self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.navigationBar?.intrinsicCanTransitionInline = false - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Wallet_Navigation_Back, style: .plain, target: nil, action: nil) + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) } required init(coder aDecoder: NSCoder) { @@ -68,7 +66,7 @@ public final class TwoFactorDataInputScreen: ViewController { return } if values[0] != values[1] { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [ + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) ]), in: .window(.root)) return @@ -291,7 +289,7 @@ public final class TwoFactorDataInputScreen: ViewController { } switch strongSelf.mode { case let .emailAddress(password, hint): - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationTitle, text: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationText, actions: [ + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationTitle, text: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationText, actions: [ TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationSkip, action: { guard let strongSelf = self else { return @@ -471,7 +469,7 @@ private func generateTextHiddenImage(color: UIColor, on: Bool) -> UIImage? { private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelegate { private let theme: PresentationTheme let mode: TwoFactorDataInputTextNodeType - private let focused: (TwoFactorDataInputTextNode) -> Void + private let focusUpdated: (TwoFactorDataInputTextNode, Bool) -> Void private let next: (TwoFactorDataInputTextNode) -> Void private let updated: (TwoFactorDataInputTextNode) -> Void private let toggleTextHidden: (TwoFactorDataInputTextNode) -> Void @@ -481,6 +479,12 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega private let hideButtonNode: HighlightableButtonNode private let clearButtonNode: HighlightableButtonNode + fileprivate var ignoreTextChanged: Bool = false + + var isFocused: Bool { + return self.inputNode.textField.isFirstResponder + } + var text: String { get { return self.inputNode.textField.text ?? "" @@ -490,10 +494,10 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega } } - init(theme: PresentationTheme, mode: TwoFactorDataInputTextNodeType, placeholder: String, focused: @escaping (TwoFactorDataInputTextNode) -> Void, next: @escaping (TwoFactorDataInputTextNode) -> Void, updated: @escaping (TwoFactorDataInputTextNode) -> Void, toggleTextHidden: @escaping (TwoFactorDataInputTextNode) -> Void) { + init(theme: PresentationTheme, mode: TwoFactorDataInputTextNodeType, placeholder: String, focusUpdated: @escaping (TwoFactorDataInputTextNode, Bool) -> Void, next: @escaping (TwoFactorDataInputTextNode) -> Void, updated: @escaping (TwoFactorDataInputTextNode) -> Void, toggleTextHidden: @escaping (TwoFactorDataInputTextNode) -> Void) { self.theme = theme self.mode = mode - self.focused = focused + self.focusUpdated = focusUpdated self.next = next self.updated = updated self.toggleTextHidden = toggleTextHidden @@ -501,12 +505,12 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega self.backgroundNode = ASImageNode() self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: theme.actionSheet.inputBackgroundColor) + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: theme.list.freePlainInputField.backgroundColor) self.inputNode = TextFieldNode() self.inputNode.textField.font = Font.regular(17.0) - self.inputNode.textField.textColor = theme.actionSheet.inputTextColor - self.inputNode.textField.attributedPlaceholder = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.actionSheet.inputPlaceholderColor) + self.inputNode.textField.textColor = theme.list.freePlainInputField.primaryColor + self.inputNode.textField.attributedPlaceholder = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.list.freePlainInputField.placeholderColor) self.hideButtonNode = HighlightableButtonNode() @@ -544,10 +548,10 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega } self.inputNode.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance - self.hideButtonNode.setImage(generateTextHiddenImage(color: theme.actionSheet.inputClearButtonColor, on: false), for: []) + self.hideButtonNode.setImage(generateTextHiddenImage(color: theme.list.freePlainInputField.controlColor, on: false), for: []) self.clearButtonNode = HighlightableButtonNode() - self.clearButtonNode.setImage(generateClearImage(color: theme.actionSheet.inputClearButtonColor), for: []) + self.clearButtonNode.setImage(generateClearImage(color: theme.list.freePlainInputField.controlColor), for: []) self.clearButtonNode.isHidden = true super.init() @@ -564,11 +568,21 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega func textFieldDidBeginEditing(_ textField: UITextField) { let text = self.text - let isEmpty = text.isEmpty - self.focused(self) + + if self.inputNode.textField.isSecureTextEntry { + let previousIgnoreTextChanged = self.ignoreTextChanged + self.ignoreTextChanged = true + self.inputNode.textField.text = "" + self.inputNode.textField.insertText(text + " ") + self.inputNode.textField.deleteBackward() + self.ignoreTextChanged = previousIgnoreTextChanged + } + + self.focusUpdated(self, true) } func textFieldDidEndEditing(_ textField: UITextField) { + self.focusUpdated(self, false) } func textFieldShouldReturn(_ textField: UITextField) -> Bool { @@ -581,13 +595,15 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega } @objc private func textFieldChanged(_ textField: UITextField) { - switch self.mode { - case .password: - break - default: - self.clearButtonNode.isHidden = self.text.isEmpty + if !self.ignoreTextChanged { + switch self.mode { + case .password: + break + default: + self.clearButtonNode.isHidden = self.text.isEmpty + } + self.updated(self) } - self.updated(self) } @objc private func hidePressed() { @@ -615,7 +631,19 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega func updateTextHidden(_ value: Bool) { self.hideButtonNode.setImage(generateTextHiddenImage(color: self.theme.actionSheet.inputClearButtonColor, on: !value), for: []) + let text = self.inputNode.textField.text ?? "" self.inputNode.textField.isSecureTextEntry = value + if value { + if self.inputNode.textField.isFirstResponder { + let previousIgnoreTextChanged = self.ignoreTextChanged + self.ignoreTextChanged = true + self.inputNode.textField.text = "" + self.inputNode.textField.becomeFirstResponder() + self.inputNode.textField.insertText(text + " ") + self.inputNode.textField.deleteBackward() + self.ignoreTextChanged = previousIgnoreTextChanged + } + } } } @@ -694,7 +722,7 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS var inputNodes: [TwoFactorDataInputTextNode] = [] var next: ((TwoFactorDataInputTextNode) -> Void)? - var focused: ((TwoFactorDataInputTextNode) -> Void)? + var focusUpdated: ((TwoFactorDataInputTextNode, Bool) -> Void)? var updated: ((TwoFactorDataInputTextNode) -> Void)? var toggleTextHidden: ((TwoFactorDataInputTextNode) -> Void)? @@ -707,8 +735,8 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS changeEmailActionText = "" resendCodeActionText = "" inputNodes = [ - TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(confirmation: false), placeholder: presentationData.strings.TwoFactorSetup_Password_PlaceholderPassword, focused: { node in - focused?(node) + TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(confirmation: false), placeholder: presentationData.strings.TwoFactorSetup_Password_PlaceholderPassword, focusUpdated: { node, focused in + focusUpdated?(node, focused) }, next: { node in next?(node) }, updated: { node in @@ -716,8 +744,8 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS }, toggleTextHidden: { node in toggleTextHidden?(node) }), - TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(confirmation: true), placeholder: presentationData.strings.TwoFactorSetup_Password_PlaceholderConfirmPassword, focused: { node in - focused?(node) + TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(confirmation: true), placeholder: presentationData.strings.TwoFactorSetup_Password_PlaceholderConfirmPassword, focusUpdated: { node, focused in + focusUpdated?(node, focused) }, next: { node in next?(node) }, updated: { node in @@ -734,8 +762,8 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS changeEmailActionText = "" resendCodeActionText = "" inputNodes = [ - TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .email, placeholder: presentationData.strings.TwoFactorSetup_Email_Placeholder, focused: { node in - focused?(node) + TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .email, placeholder: presentationData.strings.TwoFactorSetup_Email_Placeholder, focusUpdated: { node, focused in + focusUpdated?(node, focused) }, next: { node in next?(node) }, updated: { node in @@ -761,8 +789,8 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS changeEmailActionText = presentationData.strings.TwoFactorSetup_EmailVerification_ChangeAction resendCodeActionText = presentationData.strings.TwoFactorSetup_EmailVerification_ResendAction inputNodes = [ - TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .code, placeholder: presentationData.strings.TwoFactorSetup_EmailVerification_Placeholder, focused: { node in - focused?(node) + TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .code, placeholder: presentationData.strings.TwoFactorSetup_EmailVerification_Placeholder, focusUpdated: { node, focused in + focusUpdated?(node, focused) }, next: { node in next?(node) }, updated: { node in @@ -781,8 +809,8 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS changeEmailActionText = "" resendCodeActionText = "" inputNodes = [ - TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .hint, placeholder: presentationData.strings.TwoFactorSetup_Hint_Placeholder, focused: { node in - focused?(node) + TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .hint, placeholder: presentationData.strings.TwoFactorSetup_Hint_Placeholder, focusUpdated: { node, focused in + focusUpdated?(node, focused) }, next: { node in next?(node) }, updated: { node in @@ -917,67 +945,64 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS } } } - focused = { [weak self] node in - DispatchQueue.main.async { - guard let strongSelf = self else { - return - } - } - } var textHidden = true let updateAnimations: () -> Void = { [weak self] in guard let strongSelf = self else { return } - let hasText = strongSelf.inputNodes.contains(where: { !$0.text.isEmpty }) - /*switch strongSelf.mode { + switch strongSelf.mode { case .password: - if !hasText { - if strongSelf.animationNode.currentItemName == animationPeek.name { - strongSelf.animationNode.switchTo(animationHideOutro) - strongSelf.animationNode.switchTo(animationIdle) + if strongSelf.inputNodes[1].isFocused { + let textLength = strongSelf.inputNodes[1].text.count + let maxWidth = strongSelf.inputNodes[1].bounds.width + + let textNode = ImmediateTextNode() + textNode.attributedText = NSAttributedString(string: strongSelf.inputNodes[1].text, font: Font.regular(17.0), textColor: .black) + let textSize = textNode.updateLayout(CGSize(width: 1000.0, height: 100.0)) + + let maxTextLength = 20 + var trackingOffset = textSize.width / maxWidth + trackingOffset = max(0.0, min(1.0, trackingOffset)) + strongSelf.monkeyNode?.setState(.tracking(trackingOffset)) + } else if strongSelf.inputNodes[0].isFocused { + let hasText = !strongSelf.inputNodes[0].text.isEmpty + if !hasText { + strongSelf.monkeyNode?.setState(.idle(.still)) + } else if textHidden { + strongSelf.monkeyNode?.setState(.eyesClosed) } else { - strongSelf.animationNode.switchTo(animationIdle) - } - } else if textHidden { - if strongSelf.animationNode.currentItemName == animationPeek.name { - strongSelf.animationNode.switchTo(animationHideNoIntro) - } else { - strongSelf.animationNode.switchTo(animationHide) + strongSelf.monkeyNode?.setState(.peeking) } } else { - if strongSelf.animationNode.currentItemName != animationPeek.name { - if strongSelf.animationNode.currentItemName == animationHide.name { - strongSelf.animationNode.switchTo(animationPeek, noOutro: true) - } else if strongSelf.animationNode.currentItemName == animationIdle.name { - strongSelf.animationNode.switchTo(animationHideNoOutro) - strongSelf.animationNode.switchTo(animationPeek) - } else { - strongSelf.animationNode.switchTo(animationPeek, noOutro: strongSelf.animationNode.currentItemName == animationHide.name) - } - } + strongSelf.monkeyNode?.setState(.idle(.still)) } case .emailAddress: - let textLength = strongSelf.inputNodes[0].text.count - let maxWidth = strongSelf.inputNodes[0].bounds.width - if textLength == 0 || maxWidth.isZero { - strongSelf.animationNode.trackTo(frameIndex: 0) - } else { + if strongSelf.inputNodes[0].isFocused { + let textLength = strongSelf.inputNodes[0].text.count + let maxWidth = strongSelf.inputNodes[0].bounds.width + let textNode = ImmediateTextNode() textNode.attributedText = NSAttributedString(string: strongSelf.inputNodes[0].text, font: Font.regular(17.0), textColor: .black) let textSize = textNode.updateLayout(CGSize(width: 1000.0, height: 100.0)) let maxTextLength = 20 - let lowerBound = 14 - let upperBound = 160 var trackingOffset = textSize.width / maxWidth trackingOffset = max(0.0, min(1.0, trackingOffset)) - let frameIndex = lowerBound + Int(trackingOffset * CGFloat(upperBound - lowerBound)) - strongSelf.animationNode.trackTo(frameIndex: frameIndex) + strongSelf.monkeyNode?.setState(.tracking(trackingOffset)) + } else { + strongSelf.monkeyNode?.setState(.idle(.still)) } default: break - }*/ + } + } + focusUpdated = { [weak self] node, _ in + DispatchQueue.main.async { + guard let strongSelf = self else { + return + } + updateAnimations() + } } updated = { [weak self] _ in guard let strongSelf = self else { @@ -1068,7 +1093,13 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS let sideInset: CGFloat = 32.0 let buttonSideInset: CGFloat = 48.0 - let iconSpacing: CGFloat = 2.0 + let iconSpacing: CGFloat + switch self.mode { + case .passwordHint, .emailConfirmation: + iconSpacing = 6.0 + default: + iconSpacing = 2.0 + } let titleSpacing: CGFloat = 19.0 let titleInputSpacing: CGFloat = 26.0 let textSpacing: CGFloat = 30.0 @@ -1118,6 +1149,7 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS let iconFrame = CGRect(origin: CGPoint(x: floor((contentAreaSize.width - iconSize.width) / 2.0), y: contentVerticalOrigin), size: iconSize) if let animatedStickerNode = self.animatedStickerNode { + animatedStickerNode.updateLayout(size: iconFrame.size) transition.updateFrame(node: animatedStickerNode, frame: iconFrame) } else if let monkeyNode = self.monkeyNode { transition.updateFrame(node: monkeyNode, frame: iconFrame) @@ -1166,11 +1198,27 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS transition.updateFrame(node: self.skipActionButtonNode, frame: buttonFrame) transition.updateFrame(node: self.skipActionTitleNode, frame: CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - skipActionSize.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - skipActionSize.height) / 2.0)), size: skipActionSize)) - transition.updateFrame(node: self.changeEmailActionButtonNode, frame: CGRect(origin: CGPoint(x: buttonFrame.minX, y: buttonFrame.minY), size: CGSize(width: floor(buttonFrame.width / 2.0), height: buttonFrame.height))) - transition.updateFrame(node: self.resendCodeActionButtonNode, frame: CGRect(origin: CGPoint(x: buttonFrame.maxX - floor(buttonFrame.width / 2.0), y: buttonFrame.minY), size: CGSize(width: floor(buttonFrame.width / 2.0), height: buttonFrame.height))) + let changeEmailActionFrame: CGRect + let changeEmailActionButtonFrame: CGRect + let resendCodeActionFrame: CGRect + let resendCodeActionButtonFrame: CGRect + if changeEmailActionSize.width + resendCodeActionSize.width > layout.size.width - 24.0 { + changeEmailActionButtonFrame = CGRect(origin: CGPoint(x: buttonFrame.minX, y: buttonFrame.minY), size: CGSize(width: buttonFrame.width, height: buttonFrame.height)) + changeEmailActionFrame = CGRect(origin: CGPoint(x: changeEmailActionButtonFrame.minX + floor((changeEmailActionButtonFrame.width - changeEmailActionSize.width) / 2.0), y: changeEmailActionButtonFrame.minY + floor((changeEmailActionButtonFrame.height - changeEmailActionSize.height) / 2.0)), size: changeEmailActionSize) + resendCodeActionButtonFrame = CGRect(origin: CGPoint(x: buttonFrame.minX, y: buttonFrame.maxY), size: CGSize(width: buttonFrame.width, height: buttonFrame.height)) + resendCodeActionFrame = CGRect(origin: CGPoint(x: resendCodeActionButtonFrame.minX + floor((resendCodeActionButtonFrame.width - resendCodeActionSize.width) / 2.0), y: resendCodeActionButtonFrame.minY + floor((resendCodeActionButtonFrame.height - resendCodeActionSize.height) / 2.0)), size: resendCodeActionSize) + } else { + changeEmailActionButtonFrame = CGRect(origin: CGPoint(x: buttonFrame.minX, y: buttonFrame.minY), size: CGSize(width: floor(buttonFrame.width / 2.0), height: buttonFrame.height)) + changeEmailActionFrame = CGRect(origin: CGPoint(x: changeEmailActionButtonFrame.minX, y: changeEmailActionButtonFrame.minY + floor((changeEmailActionButtonFrame.height - changeEmailActionSize.height) / 2.0)), size: changeEmailActionSize) + resendCodeActionButtonFrame = CGRect(origin: CGPoint(x: buttonFrame.maxX - floor(buttonFrame.width / 2.0), y: buttonFrame.minY), size: CGSize(width: floor(buttonFrame.width / 2.0), height: buttonFrame.height)) + resendCodeActionFrame = CGRect(origin: CGPoint(x: resendCodeActionButtonFrame.maxX - resendCodeActionSize.width, y: resendCodeActionButtonFrame.minY + floor((resendCodeActionButtonFrame.height - resendCodeActionSize.height) / 2.0)), size: resendCodeActionSize) + } - transition.updateFrame(node: self.changeEmailActionTitleNode, frame: CGRect(origin: CGPoint(x: buttonFrame.minX, y: buttonFrame.minY + floor((buttonFrame.height - changeEmailActionSize.height) / 2.0)), size: changeEmailActionSize)) - transition.updateFrame(node: self.resendCodeActionTitleNode, frame: CGRect(origin: CGPoint(x: buttonFrame.maxX - resendCodeActionSize.width, y: buttonFrame.minY + floor((buttonFrame.height - resendCodeActionSize.height) / 2.0)), size: resendCodeActionSize)) + transition.updateFrame(node: self.changeEmailActionButtonNode, frame: changeEmailActionButtonFrame) + transition.updateFrame(node: self.resendCodeActionButtonNode, frame: resendCodeActionButtonFrame) + + transition.updateFrame(node: self.changeEmailActionTitleNode, frame: changeEmailActionFrame) + transition.updateFrame(node: self.resendCodeActionTitleNode, frame: resendCodeActionFrame) transition.animateView { self.scrollNode.view.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.insets(options: [.input]).bottom, right: 0.0) diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift index bb5ad1f340..48428f078e 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift @@ -30,14 +30,14 @@ public final class TwoFactorAuthSplashScreen: ViewController { let defaultTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme) let navigationBarTheme = NavigationBarTheme(buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor) - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Wallet_Intro_NotNow, close: self.presentationData.strings.Wallet_Navigation_Close))) + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close))) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.navigationPresentation = .modalInLargeLayout self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.navigationBar?.intrinsicCanTransitionInline = false - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Wallet_Navigation_Back, style: .plain, target: nil, action: nil) + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) } required init(coder aDecoder: NSCoder) { diff --git a/submodules/PeerAvatarGalleryUI/BUCK b/submodules/PeerAvatarGalleryUI/BUCK index bde6746427..f61fd2951b 100644 --- a/submodules/PeerAvatarGalleryUI/BUCK +++ b/submodules/PeerAvatarGalleryUI/BUCK @@ -26,6 +26,8 @@ static_library( "$SDKROOT/System/Library/Frameworks/Foundation.framework", "$SDKROOT/System/Library/Frameworks/UIKit.framework", "$SDKROOT/System/Library/Frameworks/QuickLook.framework", - "$SDKROOT/System/Library/Frameworks/Photos.framework", + ], + weak_frameworks = [ + "Photos", ], ) diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift index f86fb99565..1035a78876 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift @@ -11,15 +11,29 @@ import TelegramPresentationData import AccountContext import GalleryUI +public enum AvatarGalleryEntryId: Hashable { + case topImage + case image(MediaId) +} + public enum AvatarGalleryEntry: Equatable { case topImage([ImageRepresentationWithReference], GalleryItemIndexData?) - case image(TelegramMediaImageReference?, [ImageRepresentationWithReference], Peer, Int32, GalleryItemIndexData?, MessageId?) + case image(MediaId, TelegramMediaImageReference?, [ImageRepresentationWithReference], Peer?, Int32, GalleryItemIndexData?, MessageId?) + + public var id: AvatarGalleryEntryId { + switch self { + case .topImage: + return .topImage + case let .image(image): + return .image(image.0) + } + } public var representations: [ImageRepresentationWithReference] { switch self { case let .topImage(representations, _): return representations - case let .image(_, representations, _, _, _, _): + case let .image(_, _, representations, _, _, _, _): return representations } } @@ -28,7 +42,7 @@ public enum AvatarGalleryEntry: Equatable { switch self { case let .topImage(_, indexData): return indexData - case let .image(_, _, _, _, indexData, _): + case let .image(_, _, _, _, _, indexData, _): return indexData } } @@ -41,8 +55,8 @@ public enum AvatarGalleryEntry: Equatable { } else { return false } - case let .image(lhsImageReference, lhsRepresentations, lhsPeer, lhsDate, lhsIndexData, lhsMessageId): - if case let .image(rhsImageReference, rhsRepresentations, rhsPeer, rhsDate, rhsIndexData, rhsMessageId) = rhs, lhsImageReference == rhsImageReference, lhsRepresentations == rhsRepresentations, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate, lhsIndexData == rhsIndexData, lhsMessageId == rhsMessageId { + case let .image(lhsId, lhsImageReference, lhsRepresentations, lhsPeer, lhsDate, lhsIndexData, lhsMessageId): + if case let .image(rhsId, rhsImageReference, rhsRepresentations, rhsPeer, rhsDate, rhsIndexData, rhsMessageId) = rhs, lhsId == rhsId, lhsImageReference == rhsImageReference, lhsRepresentations == rhsRepresentations, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate, lhsIndexData == rhsIndexData, lhsMessageId == rhsMessageId { return true } else { return false @@ -61,7 +75,7 @@ public final class AvatarGalleryControllerPresentationArguments { } } -private func initialAvatarGalleryEntries(peer: Peer) -> [AvatarGalleryEntry]{ +public func initialAvatarGalleryEntries(peer: Peer) -> [AvatarGalleryEntry] { var initialEntries: [AvatarGalleryEntry] = [] if !peer.profileImageRepresentations.isEmpty, let peerReference = PeerReference(peer) { initialEntries.append(.topImage(peer.profileImageRepresentations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) }), nil)) @@ -70,26 +84,57 @@ private func initialAvatarGalleryEntries(peer: Peer) -> [AvatarGalleryEntry]{ } public func fetchedAvatarGalleryEntries(account: Account, peer: Peer) -> Signal<[AvatarGalleryEntry], NoError> { - return requestPeerPhotos(account: account, peerId: peer.id) - |> map { photos -> [AvatarGalleryEntry] in - var result: [AvatarGalleryEntry] = [] - let initialEntries = initialAvatarGalleryEntries(peer: peer) - if photos.isEmpty { - result = initialEntries - } else { - var index: Int32 = 0 - for photo in photos { - let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count)) - if result.isEmpty, let first = initialEntries.first { - result.append(.image(photo.image.reference, first.representations, peer, photo.date, indexData, photo.messageId)) - } else { - result.append(.image(photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.standalone(resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId)) + let initialEntries = initialAvatarGalleryEntries(peer: peer) + return Signal<[AvatarGalleryEntry], NoError>.single(initialEntries) + |> then( + requestPeerPhotos(account: account, peerId: peer.id) + |> map { photos -> [AvatarGalleryEntry] in + var result: [AvatarGalleryEntry] = [] + let initialEntries = initialAvatarGalleryEntries(peer: peer) + if photos.isEmpty { + result = initialEntries + } else { + var index: Int32 = 0 + for photo in photos { + let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count)) + if result.isEmpty, let first = initialEntries.first { + result.append(.image(photo.image.imageId, photo.image.reference, first.representations, peer, photo.date, indexData, photo.messageId)) + } else { + result.append(.image(photo.image.imageId, photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.standalone(resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId)) + } + index += 1 } - index += 1 } + return result } - return result - } + ) +} + +public func fetchedAvatarGalleryEntries(account: Account, peer: Peer, firstEntry: AvatarGalleryEntry) -> Signal<[AvatarGalleryEntry], NoError> { + let initialEntries = [firstEntry] + return Signal<[AvatarGalleryEntry], NoError>.single(initialEntries) + |> then( + requestPeerPhotos(account: account, peerId: peer.id) + |> map { photos -> [AvatarGalleryEntry] in + var result: [AvatarGalleryEntry] = [] + let initialEntries = [firstEntry] + if photos.isEmpty { + result = initialEntries + } else { + var index: Int32 = 0 + for photo in photos { + let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count)) + if result.isEmpty, let first = initialEntries.first { + result.append(.image(photo.image.imageId, photo.image.reference, first.representations, peer, photo.date, indexData, photo.messageId)) + } else { + result.append(.image(photo.image.imageId, photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.standalone(resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId)) + } + index += 1 + } + } + return result + } + ) } public class AvatarGalleryController: ViewController, StandalonePresentableController { @@ -99,6 +144,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr private let context: AccountContext private let peer: Peer + private let sourceHasRoundCorners: Bool private var presentationData: PresentationData @@ -118,7 +164,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr private let centralItemTitle = Promise() private let centralItemTitleView = Promise() private let centralItemNavigationStyle = Promise() - private let centralItemFooterContentNode = Promise() + private let centralItemFooterContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>() private let centralItemAttributesDisposable = DisposableSet(); private let _hiddenMedia = Promise(nil) @@ -128,12 +174,15 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr private let replaceRootController: (ViewController, ValuePromise?) -> Void - public init(context: AccountContext, peer: Peer, remoteEntries: Promise<[AvatarGalleryEntry]>? = nil, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, synchronousLoad: Bool = false) { + public init(context: AccountContext, peer: Peer, sourceHasRoundCorners: Bool = true, remoteEntries: Promise<[AvatarGalleryEntry]>? = nil, centralEntryIndex: Int? = nil, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, synchronousLoad: Bool = false) { self.context = context self.peer = peer + self.sourceHasRoundCorners = sourceHasRoundCorners self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.replaceRootController = replaceRootController + self.centralEntryIndex = centralEntryIndex + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: GalleryController.darkNavigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Back, target: self, action: #selector(self.donePressed)) @@ -165,7 +214,9 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr let f: () -> Void = { if let strongSelf = self { strongSelf.entries = entries - strongSelf.centralEntryIndex = 0 + if strongSelf.centralEntryIndex == nil { + strongSelf.centralEntryIndex = 0 + } if strongSelf.isViewLoaded { let canDelete: Bool if strongSelf.peer.id == strongSelf.context.account.peerId { @@ -182,7 +233,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr } else { canDelete = false } - strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ entry in PeerAvatarImageGalleryItem(context: context, peer: peer, presentationData: presentationData, entry: entry, delete: canDelete ? { + strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ entry in PeerAvatarImageGalleryItem(context: context, peer: peer, presentationData: presentationData, entry: entry, sourceHasRoundCorners: sourceHasRoundCorners, delete: canDelete ? { self?.deleteEntry(entry) } : nil) }), centralItemIndex: 0, keepFirst: true) @@ -232,7 +283,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr self?.navigationItem.titleView = titleView })) - self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode in + self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode, _ in self?.galleryNode.updatePresentationState({ $0.withUpdatedFooterContentNode(footerContentNode) }, transition: .immediate) @@ -265,7 +316,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? AvatarGalleryControllerPresentationArguments { if !self.entries.isEmpty { - if centralItemNode.index == 0, let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]), !forceAway { + if (centralItemNode.index == 0 || !self.sourceHasRoundCorners), let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]), !forceAway { animatedOutNode = false centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: { animatedOutNode = true @@ -302,7 +353,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr self.galleryNode.transitionDataForCentralItem = { [weak self] in if let strongSelf = self { if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? AvatarGalleryControllerPresentationArguments { - if centralItemNode.index != 0 { + if centralItemNode.index != 0 && strongSelf.sourceHasRoundCorners { return nil } if let transitionArguments = presentationArguments.transitionArguments(strongSelf.entries[centralItemNode.index]) { @@ -334,7 +385,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr } let presentationData = self.presentationData - self.galleryNode.pager.replaceItems(self.entries.map({ entry in PeerAvatarImageGalleryItem(context: self.context, peer: peer, presentationData: presentationData, entry: entry, delete: canDelete ? { [weak self] in + self.galleryNode.pager.replaceItems(self.entries.map({ entry in PeerAvatarImageGalleryItem(context: self.context, peer: peer, presentationData: presentationData, entry: entry, sourceHasRoundCorners: self.sourceHasRoundCorners, delete: canDelete ? { [weak self] in self?.deleteEntry(entry) } : nil) }), centralItemIndex: self.centralEntryIndex) @@ -438,7 +489,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr } } } - case let .image(reference, _, _, _, _, messageId): + case let .image(_, reference, _, _, _, _, messageId): if self.peer.id == self.context.account.peerId { if let reference = reference { let _ = removeAccountPhoto(network: self.context.account.network, reference: reference).start() @@ -453,7 +504,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr } } else { if let messageId = messageId { - let _ = deleteMessagesInteractively(postbox: self.context.account.postbox, messageIds: [messageId], type: .forEveryone).start() + let _ = deleteMessagesInteractively(account: self.context.account, messageIds: [messageId], type: .forEveryone).start() } if entry == self.entries.first { diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift index 38d9658a65..5bfda46d5a 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift @@ -84,8 +84,8 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode { var nameText: String? var dateText: String? switch entry { - case let .image(_, _, peer, date, _, _): - nameText = peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) + case let .image(_, _, _, peer, date, _, _): + nameText = peer?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "" dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: date) default: break @@ -148,7 +148,7 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode { @objc private func deleteButtonPressed() { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) let items: [ActionSheetItem] = [ ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() @@ -158,7 +158,7 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode { actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) diff --git a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift index d8510f4029..45415eef19 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift @@ -42,22 +42,28 @@ private struct PeerAvatarImageGalleryThumbnailItem: GalleryThumbnailItem { } class PeerAvatarImageGalleryItem: GalleryItem { + var id: AnyHashable { + return self.entry.id + } + let context: AccountContext let peer: Peer let presentationData: PresentationData let entry: AvatarGalleryEntry + let sourceHasRoundCorners: Bool let delete: (() -> Void)? - init(context: AccountContext, peer: Peer, presentationData: PresentationData, entry: AvatarGalleryEntry, delete: (() -> Void)?) { + init(context: AccountContext, peer: Peer, presentationData: PresentationData, entry: AvatarGalleryEntry, sourceHasRoundCorners: Bool, delete: (() -> Void)?) { self.context = context self.peer = peer self.presentationData = presentationData self.entry = entry + self.sourceHasRoundCorners = sourceHasRoundCorners self.delete = delete } func node() -> GalleryItemNode { - let node = PeerAvatarImageGalleryItemNode(context: self.context, presentationData: self.presentationData, peer: self.peer) + let node = PeerAvatarImageGalleryItemNode(context: self.context, presentationData: self.presentationData, peer: self.peer, sourceHasRoundCorners: self.sourceHasRoundCorners) if let indexData = self.entry.indexData { node._title.set(.single(self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").0)) @@ -85,7 +91,7 @@ class PeerAvatarImageGalleryItem: GalleryItem { switch self.entry { case let .topImage(representations, _): content = representations - case let .image(_, representations, _, _, _, _): + case let .image(_, _, representations, _, _, _, _): content = representations } @@ -96,6 +102,7 @@ class PeerAvatarImageGalleryItem: GalleryItem { final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { private let context: AccountContext private let peer: Peer + private let sourceHasRoundCorners: Bool private var entry: AvatarGalleryEntry? @@ -110,9 +117,10 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { private let statusDisposable = MetaDisposable() private var status: MediaResourceStatus? - init(context: AccountContext, presentationData: PresentationData, peer: Peer) { + init(context: AccountContext, presentationData: PresentationData, peer: Peer, sourceHasRoundCorners: Bool) { self.context = context self.peer = peer + self.sourceHasRoundCorners = sourceHasRoundCorners self.imageNode = TransformImageNode() self.footerContentNode = AvatarGalleryItemFooterContentNode(context: context, presentationData: presentationData) @@ -127,6 +135,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { self?._ready.set(.single(Void())) } + self.imageNode.contentAnimations = .subsequentUpdates self.imageNode.view.contentMode = .scaleAspectFill self.imageNode.clipsToBounds = true @@ -174,7 +183,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { switch entry { case let .topImage(topRepresentations, _): representations = topRepresentations - case let .image(_, imageRepresentations, _, _, _, _): + case let .image(_, _, imageRepresentations, _, _, _, _): representations = imageRepresentations } self.imageNode.setSignal(chatAvatarGalleryPhoto(account: self.context.account, representations: representations), dispatchOnDisplayLink: false) @@ -229,15 +238,49 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { } } - override func animateIn(from node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { + override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view) let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view.superview) let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) let transformedCopyViewFinalFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view) + let scaledLocalImageViewBounds = self.imageNode.view.bounds - let copyView = node.1().0! + let copyViewContents = node.2().0! + let copyView = UIView() + copyView.addSubview(copyViewContents) + copyViewContents.frame = CGRect(origin: CGPoint(x: (transformedSelfFrame.width - copyViewContents.frame.width) / 2.0, y: (transformedSelfFrame.height - copyViewContents.frame.height) / 2.0), size: copyViewContents.frame.size) + copyView.layer.sublayerTransform = CATransform3DMakeScale(transformedSelfFrame.width / copyViewContents.frame.width, transformedSelfFrame.height / copyViewContents.frame.height, 1.0) - self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) + let surfaceCopyViewContents = node.2().0! + let surfaceCopyView = UIView() + surfaceCopyView.addSubview(surfaceCopyViewContents) + + addToTransitionSurface(surfaceCopyView) + + var transformedSurfaceFrame: CGRect? + var transformedSurfaceFinalFrame: CGRect? + if let contentSurface = surfaceCopyView.superview { + transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface) + transformedSurfaceFinalFrame = self.imageNode.view.convert(scaledLocalImageViewBounds, to: contentSurface) + } + + if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedSurfaceFinalFrame = transformedSurfaceFinalFrame { + surfaceCopyViewContents.frame = CGRect(origin: CGPoint(x: (transformedSurfaceFrame.width - surfaceCopyViewContents.frame.width) / 2.0, y: (transformedSurfaceFrame.height - surfaceCopyViewContents.frame.height) / 2.0), size: surfaceCopyViewContents.frame.size) + surfaceCopyView.layer.sublayerTransform = CATransform3DMakeScale(transformedSurfaceFrame.width / surfaceCopyViewContents.frame.width, transformedSurfaceFrame.height / surfaceCopyViewContents.frame.height, 1.0) + surfaceCopyView.frame = transformedSurfaceFrame + + surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), to: CGPoint(x: transformedSurfaceFinalFrame.midX, y: transformedSurfaceFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + let scale = CGSize(width: transformedSurfaceFinalFrame.size.width / transformedSurfaceFrame.size.width, height: transformedSurfaceFrame.size.height / transformedSelfFrame.size.height) + surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false) + + surfaceCopyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak surfaceCopyView] _ in + surfaceCopyView?.removeFromSuperview() + }) + } + + if self.sourceHasRoundCorners { + self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) + } copyView.frame = transformedSelfFrame copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak copyView] _ in @@ -258,18 +301,20 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { self.imageNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: self.imageNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) self.imageNode.clipsToBounds = true - self.imageNode.layer.animate(from: (self.imageNode.frame.width / 2.0) as NSNumber, to: 0.0 as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.default.rawValue, duration: 0.18, removeOnCompletion: false, completion: { [weak self] value in - if value { - self?.imageNode.clipsToBounds = false - } - }) + if self.sourceHasRoundCorners { + self.imageNode.layer.animate(from: (self.imageNode.frame.width / 2.0) as NSNumber, to: 0.0 as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.default.rawValue, duration: 0.18, removeOnCompletion: false, completion: { [weak self] value in + if value { + self?.imageNode.clipsToBounds = false + } + }) + } self.statusNodeContainer.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusNodeContainer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) self.statusNodeContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) self.statusNodeContainer.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) } - override func animateOut(to node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view) let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.imageNode.view.superview) let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) @@ -278,20 +323,49 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { var positionCompleted = false var boundsCompleted = false var copyCompleted = false + var surfaceCopyCompleted = false - let copyView = node.1().0! + let copyView = node.2().0! - self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) + if self.sourceHasRoundCorners { + self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) + } copyView.frame = transformedSelfFrame - let intermediateCompletion = { [weak copyView] in + let surfaceCopyView = node.2().0! + if !self.sourceHasRoundCorners { + addToTransitionSurface(surfaceCopyView) + } + + var transformedSurfaceFrame: CGRect? + var transformedSurfaceCopyViewInitialFrame: CGRect? + if let contentSurface = surfaceCopyView.superview { + transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface) + transformedSurfaceCopyViewInitialFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: contentSurface) + } + + let durationFactor = 1.0 + + let intermediateCompletion = { [weak copyView, weak surfaceCopyView] in if positionCompleted && boundsCompleted && copyCompleted { copyView?.removeFromSuperview() + surfaceCopyView?.removeFromSuperview() completion() } } - let durationFactor = 1.0 + if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedSurfaceCopyViewInitialFrame = transformedSurfaceCopyViewInitialFrame { + surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1 * durationFactor, removeOnCompletion: false) + + surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedSurfaceCopyViewInitialFrame.midX, y: transformedSurfaceCopyViewInitialFrame.midY), to: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), duration: 0.25 * durationFactor, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + let scale = CGSize(width: transformedSurfaceCopyViewInitialFrame.size.width / transformedSurfaceFrame.size.width, height: transformedSurfaceCopyViewInitialFrame.size.height / transformedSurfaceFrame.size.height) + surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25 * durationFactor, removeOnCompletion: false, completion: { _ in + surfaceCopyCompleted = true + intermediateCompletion() + }) + } else { + surfaceCopyCompleted = true + } copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1 * durationFactor, removeOnCompletion: false) @@ -318,7 +392,9 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { }) self.imageNode.clipsToBounds = true - self.imageNode.layer.animate(from: 0.0 as NSNumber, to: (self.imageNode.frame.width / 2.0) as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.default.rawValue, duration: 0.18 * durationFactor, removeOnCompletion: false) + if self.sourceHasRoundCorners { + self.imageNode.layer.animate(from: 0.0 as NSNumber, to: (self.imageNode.frame.width / 2.0) as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.default.rawValue, duration: 0.18 * durationFactor, removeOnCompletion: false) + } self.statusNodeContainer.layer.animatePosition(from: self.statusNodeContainer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) self.statusNodeContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false) @@ -342,7 +418,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { switch entry { case let .topImage(topRepresentations, _): representations = topRepresentations - case let .image(_, imageRepresentations, _, _, _, _): + case let .image(_, _, imageRepresentations, _, _, _, _): representations = imageRepresentations } @@ -355,7 +431,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { } } - override func footerContent() -> Signal { - return .single(self.footerContentNode) + override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { + return .single((self.footerContentNode, nil)) } } diff --git a/submodules/PeerInfoUI/BUCK b/submodules/PeerInfoUI/BUCK index 8445b1bd3c..16d723277d 100644 --- a/submodules/PeerInfoUI/BUCK +++ b/submodules/PeerInfoUI/BUCK @@ -41,6 +41,7 @@ static_library( "//submodules/TelegramNotices:TelegramNotices", "//submodules/PhotoResources:PhotoResources", "//submodules/MediaResources:MediaResources", + "//submodules/LocationResources:LocationResources", "//submodules/Geocoding:Geocoding", "//submodules/LegacyComponents:LegacyComponents", "//submodules/LocationUI:LocationUI", @@ -60,6 +61,10 @@ static_library( "//submodules/AppBundle:AppBundle", "//submodules/Markdown:Markdown", "//submodules/PhoneNumberFormat:PhoneNumberFormat", + "//submodules/TelegramIntents:TelegramIntents", + "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", + "//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader", + "//submodules/StatisticsUI:StatisticsUI", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index 5affbc7b42..f26291b8a4 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -18,7 +18,7 @@ import LocalizedPeerData private let rankMaxLength: Int32 = 16 private final class ChannelAdminControllerArguments { - let account: Account + let context: AccountContext let toggleRight: (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void let toggleRightWhileDisabled: (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void let transferOwnership: () -> Void @@ -28,8 +28,8 @@ private final class ChannelAdminControllerArguments { let dismissInput: () -> Void let animateError: () -> Void - init(account: Account, toggleRight: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, transferOwnership: @escaping () -> Void, updateRank: @escaping (String, String) -> Void, updateFocusedOnRank: @escaping (Bool) -> Void, dismissAdmin: @escaping () -> Void, dismissInput: @escaping () -> Void, animateError: @escaping () -> Void) { - self.account = account + init(context: AccountContext, toggleRight: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, transferOwnership: @escaping () -> Void, updateRank: @escaping (String, String) -> Void, updateFocusedOnRank: @escaping (Bool) -> Void, dismissAdmin: @escaping () -> Void, dismissInput: @escaping () -> Void, animateError: @escaping () -> Void) { + self.context = context self.toggleRight = toggleRight self.toggleRightWhileDisabled = toggleRightWhileDisabled self.transferOwnership = transferOwnership @@ -365,11 +365,11 @@ private enum ChannelAdminEntry: ItemListNodeEntry { } } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ChannelAdminControllerArguments switch self { case let .info(theme, strings, dateTimeFormat, peer, presence): - return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: presence, cachedData: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true, withExtendedBottomInset: false), editingNameUpdated: { _ in + return ItemListAvatarAndNameInfoItem(accountContext: arguments.context, presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: presence, cachedData: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true, withExtendedBottomInset: false), editingNameUpdated: { _ in }, avatarTapped: { }) case let .rankTitle(theme, text, count, limit): @@ -377,9 +377,9 @@ private enum ChannelAdminEntry: ItemListNodeEntry { if let count = count { accessoryText = ItemListSectionHeaderAccessoryText(value: "\(limit - count)", color: count > limit ? .destructive : .generic) } - return ItemListSectionHeaderItem(theme: theme, text: text, accessoryText: accessoryText, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: accessoryText, sectionId: self.section) case let .rank(theme, strings, placeholder, text, enabled): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: true), spacing: 0.0, clearType: enabled ? .always : .none, enabled: enabled, tag: ChannelAdminEntryTag.rank, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: true), spacing: 0.0, clearType: enabled ? .always : .none, enabled: enabled, tag: ChannelAdminEntryTag.rank, sectionId: self.section, textUpdated: { updatedText in arguments.updateRank(text, updatedText) }, shouldUpdateText: { text in if text.containsEmoji { @@ -393,23 +393,23 @@ private enum ChannelAdminEntry: ItemListNodeEntry { arguments.dismissInput() }) case let .rankInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .rightsTitle(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .rightItem(theme, _, text, right, flags, value, enabled): - return ItemListSwitchItem(theme: theme, title: text, value: value, type: .icon, enabled: enabled, sectionId: self.section, style: .blocks, updated: { _ in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, type: .icon, enabled: enabled, sectionId: self.section, style: .blocks, updated: { _ in arguments.toggleRight(right, flags) }, activatedWhileDisabled: { arguments.toggleRightWhileDisabled(right, flags) }) case let .addAdminsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .transfer(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.transferOwnership() }, tag: nil) case let .dismiss(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.dismissAdmin() }, tag: nil) } @@ -791,6 +791,7 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi var dismissImpl: (() -> Void)? var dismissInputImpl: (() -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? var errorImpl: (() -> Void)? var scrollToRankImpl: (() -> Void)? @@ -800,7 +801,7 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi upgradedToSupergroup(peerId, completion) } - let arguments = ChannelAdminControllerArguments(account: context.account, toggleRight: { right, flags in + let arguments = ChannelAdminControllerArguments(context: context, toggleRight: { right, flags in updateState { current in var updated = flags if flags.contains(right) { @@ -829,7 +830,7 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi text = presentationData.strings.Channel_EditAdmin_CannotEdit } - presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) }) }, transferOwnership: { let _ = (context.account.postbox.transaction { transaction -> (peer: Peer?, member: Peer?) in @@ -868,7 +869,7 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi } }, dismissAdmin: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: presentationData.strings.Channel_Moderator_AccessLevelRevoke, color: .destructive, font: .default, enabled: true, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -893,7 +894,7 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi } })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -1062,12 +1063,17 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi return current.withUpdatedUpdating(true) } updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: context.account, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(flags: updateFlags), rank: updateRank) |> deliverOnMainQueue).start(error: { error in - if case let .addMemberError(error) = error, case .restricted = error, let admin = adminView.peers[adminView.peerId] { - var text = presentationData.strings.Privacy_GroupsAndChannels_InviteToChannelError(admin.compactDisplayTitle, admin.compactDisplayTitle).0 - if case .group = channel.info { - text = presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(admin.compactDisplayTitle, admin.compactDisplayTitle).0 + if case let .addMemberError(error) = error, let admin = adminView.peers[adminView.peerId] { + if case .restricted = error { + var text = presentationData.strings.Privacy_GroupsAndChannels_InviteToChannelError(admin.compactDisplayTitle, admin.compactDisplayTitle).0 + if case .group = channel.info { + text = presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(admin.compactDisplayTitle, admin.compactDisplayTitle).0 + } + presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + } else if case .tooMuchJoined = error { + let text = presentationData.strings.Invite_ChannelsTooMuch + presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } - presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } dismissImpl?() }, completed: { @@ -1115,22 +1121,31 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi dismissImpl?() })) } else if updateFlags != defaultFlags || updateRank != nil { + enum WrappedUpdateChannelAdminRightsError { + case direct(UpdateChannelAdminRightsError) + case conversionTooManyChannels + case conversionFailed + } + let signal = convertGroupToSupergroup(account: context.account, peerId: peerId) |> map(Optional.init) - |> `catch` { error -> Signal in + |> `catch` { error -> Signal in switch error { case .tooManyChannels: - return .fail(.addMemberError(.tooMuchJoined)) + return .fail(.conversionTooManyChannels) default: - return .fail(.generic) + return .fail(.conversionFailed) } } - |> mapToSignal { upgradedPeerId -> Signal in + |> mapToSignal { upgradedPeerId -> Signal in guard let upgradedPeerId = upgradedPeerId else { - return .fail(.generic) + return .fail(.conversionFailed) } return context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: context.account, peerId: upgradedPeerId, memberId: adminId, adminRights: TelegramChatAdminRights(flags: updateFlags), rank: updateRank) - |> mapToSignal { _ -> Signal in + |> mapError { error -> WrappedUpdateChannelAdminRightsError in + return .direct(error) + } + |> mapToSignal { _ -> Signal in return .complete() } |> then(.single(upgradedPeerId)) @@ -1147,14 +1162,23 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi }) } }, error: { error in - if case let .addMemberError(error) = error { - var text = presentationData.strings.Login_UnknownError - if case .restricted = error, let admin = adminView.peers[adminView.peerId] { - text = presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(admin.compactDisplayTitle, admin.compactDisplayTitle).0 - } else if case .tooMuchJoined = error { - text = presentationData.strings.Group_ErrorSupergroupConversionNotPossible + updateState { current in + return current.withUpdatedUpdating(false) + } + + switch error { + case let .direct(error): + if case let .addMemberError(error) = error { + var text = presentationData.strings.Login_UnknownError + if case .restricted = error, let admin = adminView.peers[adminView.peerId] { + text = presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(admin.compactDisplayTitle, admin.compactDisplayTitle).0 + } else if case .tooMuchJoined = error { + text = presentationData.strings.Invite_ChannelsTooMuch + } + presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } - presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + case .conversionFailed, .conversionTooManyChannels: + pushControllerImpl?(oldChannelsController(context: context, intent: .upgrade)) } dismissImpl?() @@ -1169,9 +1193,9 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Channel_Management_LabelEditor), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(initialParticipant?.adminInfo == nil ? presentationData.strings.Channel_Management_AddModerator : presentationData.strings.Channel_Moderator_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: channelAdminControllerEntries(presentationData: presentationData, state: state, accountPeerId: context.account.peerId, channelView: channelView, adminView: adminView, initialParticipant: initialParticipant, canEdit: canEdit), style: .blocks, focusItemTag: focusItemTag, ensureVisibleItemTag: nil, emptyStateItem: nil, animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelAdminControllerEntries(presentationData: presentationData, state: state, accountPeerId: context.account.peerId, channelView: channelView, adminView: adminView, initialParticipant: initialParticipant, canEdit: canEdit), style: .blocks, focusItemTag: focusItemTag, ensureVisibleItemTag: nil, emptyStateItem: nil, animateChanges: true) return (controllerState, (listState, arguments)) } @@ -1192,6 +1216,9 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi presentControllerImpl = { [weak controller] value, presentationArguments in controller?.present(value, in: .window(.root), with: presentationArguments) } + pushControllerImpl = { [weak controller] c in + controller?.push(c) + } let hapticFeedback = HapticFeedback() errorImpl = { [weak controller] in diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift index 121b8bb980..98bd7c24ad 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift @@ -18,7 +18,7 @@ import ItemListPeerItem import ItemListPeerActionItem private final class ChannelAdminsControllerArguments { - let account: Account + let context: AccountContext let openRecentActions: () -> Void let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void @@ -26,8 +26,8 @@ private final class ChannelAdminsControllerArguments { let addAdmin: () -> Void let openAdmin: (ChannelParticipant) -> Void - init(account: Account, openRecentActions: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removeAdmin: @escaping (PeerId) -> Void, addAdmin: @escaping () -> Void, openAdmin: @escaping (ChannelParticipant) -> Void) { - self.account = account + init(context: AccountContext, openRecentActions: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removeAdmin: @escaping (PeerId) -> Void, addAdmin: @escaping () -> Void, openAdmin: @escaping (ChannelParticipant) -> Void) { + self.context = context self.openRecentActions = openRecentActions self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.removeAdmin = removeAdmin @@ -201,15 +201,15 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { } } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ChannelAdminsControllerArguments switch self { case let .recentActions(theme, text): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openRecentActions() }) case let .adminsHeader(theme, title): - return ItemListSectionHeaderItem(theme: theme, text: title, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .adminPeerItem(theme, strings, dateTimeFormat, nameDisplayOrder, _, _, participant, editing, enabled, hasAction): let peerText: String var action: (() -> Void)? @@ -236,17 +236,17 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { arguments.openAdmin(participant.participant) } } - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: action, setPeerIdWithRevealedOptions: { previousId, id in + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: participant.peer, presence: nil, text: .text(peerText), label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: action, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.removeAdmin(peerId) }) case let .addAdmin(theme, text, editing): - return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.addPersonIcon(theme), title: text, sectionId: self.section, editing: editing, action: { + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addPersonIcon(theme), title: text, sectionId: self.section, editing: editing, action: { arguments.addAdmin() }) case let .adminsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -489,7 +489,7 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, return entries } -public func channelAdminsController(context: AccountContext, peerId: PeerId, loadCompleted: @escaping () -> Void = {}) -> ViewController { +public func channelAdminsController(context: AccountContext, peerId initialPeerId: PeerId, loadCompleted: @escaping () -> Void = {}) -> ViewController { let statePromise = ValuePromise(ChannelAdminsControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: ChannelAdminsControllerState()) let updateState: ((ChannelAdminsControllerState) -> ChannelAdminsControllerState) -> Void = { f in @@ -517,32 +517,47 @@ public func channelAdminsController(context: AccountContext, peerId: PeerId, loa var upgradedToSupergroupImpl: ((PeerId, @escaping () -> Void) -> Void)? + let currentPeerId = ValuePromise(initialPeerId) + let upgradedToSupergroup: (PeerId, @escaping () -> Void) -> Void = { upgradedPeerId, f in + currentPeerId.set(upgradedPeerId) upgradedToSupergroupImpl?(upgradedPeerId, f) } let transferedOwnership: (PeerId) -> Void = { memberId in let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let _ = (context.account.postbox.transaction { transaction -> (channel: Peer?, user: Peer?) in - return (channel: transaction.getPeer(peerId), user: transaction.getPeer(memberId)) - } |> deliverOnMainQueue).start(next: { peer, user in + let _ = (currentPeerId.get() + |> take(1) + |> mapToSignal { peerId in + context.account.postbox.transaction { transaction -> (channel: Peer?, user: Peer?) in + return (channel: transaction.getPeer(peerId), user: transaction.getPeer(memberId)) + } + } + |> deliverOnMainQueue).start(next: { peer, user in guard let peer = peer, let user = user else { return } - presentControllerImpl?(UndoOverlayController(presentationData: context.sharedContext.currentPresentationData.with { $0 }, content: .succeed(text: presentationData.strings.Channel_OwnershipTransfer_TransferCompleted(user.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).0), elevatedLayout: false, action: { _ in }), nil) + presentControllerImpl?(UndoOverlayController(presentationData: context.sharedContext.currentPresentationData.with { $0 }, content: .succeed(text: presentationData.strings.Channel_OwnershipTransfer_TransferCompleted(user.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).0), elevatedLayout: false, action: { _ in return false }), nil) }) } let peerView = Promise() - peerView.set(context.account.viewTracker.peerView(peerId)) + peerView.set(currentPeerId.get() + |> mapToSignal { peerId in + return context.account.viewTracker.peerView(peerId) + }) - let arguments = ChannelAdminsControllerArguments(account: context.account, openRecentActions: { - let _ = (context.account.postbox.loadedPeerWithId(peerId) - |> deliverOnMainQueue).start(next: { peer in - if peer is TelegramGroup { - } else { - pushControllerImpl?(context.sharedContext.makeChatRecentActionsController(context: context, peer: peer)) - } + let arguments = ChannelAdminsControllerArguments(context: context, openRecentActions: { + let _ = (currentPeerId.get() + |> take(1) + |> deliverOnMainQueue).start(next: { peerId in + let _ = (context.account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue).start(next: { peer in + if peer is TelegramGroup { + } else { + pushControllerImpl?(context.sharedContext.makeChatRecentActionsController(context: context, peer: peer)) + } + }) }) }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in updateState { state in @@ -553,140 +568,162 @@ public func channelAdminsController(context: AccountContext, peerId: PeerId, loa } } }, removeAdmin: { adminId in - updateState { - return $0.withUpdatedRemovingPeerId(adminId) - } - if peerId.namespace == Namespaces.Peer.CloudGroup { - removeAdminDisposable.set((removeGroupAdmin(account: context.account, peerId: peerId, adminId: adminId) - |> deliverOnMainQueue).start(completed: { - updateState { - return $0.withUpdatedRemovingPeerId(nil) - } - })) - } else { - removeAdminDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: context.account, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(flags: []), rank: nil) - |> deliverOnMainQueue).start(completed: { - updateState { - return $0.withUpdatedRemovingPeerId(nil) - } - })) - } - }, addAdmin: { - let _ = (peerView.get() + let _ = (currentPeerId.get() |> take(1) - |> deliverOnMainQueue).start(next: { peerView in - updateState { current in - var dismissController: (() -> Void)? - let controller = ChannelMembersSearchController(context: context, peerId: peerId, mode: .promote, filters: [], openPeer: { peer, participant in - dismissController?() - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - if peer.id == context.account.peerId { - return + |> deliverOnMainQueue).start(next: { peerId in + updateState { + return $0.withUpdatedRemovingPeerId(adminId) + } + if peerId.namespace == Namespaces.Peer.CloudGroup { + removeAdminDisposable.set((removeGroupAdmin(account: context.account, peerId: peerId, adminId: adminId) + |> deliverOnMainQueue).start(completed: { + updateState { + return $0.withUpdatedRemovingPeerId(nil) } - if let participant = participant { - switch participant.participant { - case .creator: + })) + } else { + removeAdminDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: context.account, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(flags: []), rank: nil) + |> deliverOnMainQueue).start(completed: { + updateState { + return $0.withUpdatedRemovingPeerId(nil) + } + })) + } + }) + }, addAdmin: { + let _ = (currentPeerId.get() + |> take(1) + |> deliverOnMainQueue).start(next: { peerId in + let _ = (peerView.get() + |> take(1) + |> deliverOnMainQueue).start(next: { peerView in + updateState { current in + var dismissController: (() -> Void)? + let controller = ChannelMembersSearchController(context: context, peerId: peerId, mode: .promote, filters: [], openPeer: { peer, participant in + dismissController?() + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + if peer.id == context.account.peerId { return - case let .member(_, _, _, banInfo, _): - if let banInfo = banInfo { - var canUnban = false - if banInfo.restrictedBy != context.account.peerId { - canUnban = true - } - if let channel = peerView.peers[peerId] as? TelegramChannel { - if channel.hasPermission(.banMembers) { + } + if let participant = participant { + switch participant.participant { + case .creator: + return + case let .member(_, _, _, banInfo, _): + if let banInfo = banInfo { + var canUnban = false + if banInfo.restrictedBy != context.account.peerId { canUnban = true } - } - if !canUnban { - presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Channel_Members_AddAdminErrorBlacklisted, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) - return + if let channel = peerView.peers[peerId] as? TelegramChannel { + if channel.hasPermission(.banMembers) { + canUnban = true + } + } + if !canUnban { + presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Channel_Members_AddAdminErrorBlacklisted, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + return + } } } } + pushControllerImpl?(channelAdminController(context: context, peerId: peerId, adminId: peer.id, initialParticipant: participant?.participant, updated: { _ in + }, upgradedToSupergroup: upgradedToSupergroup, transferedOwnership: transferedOwnership)) + }) + dismissController = { [weak controller] in + controller?.dismiss() } - pushControllerImpl?(channelAdminController(context: context, peerId: peerId, adminId: peer.id, initialParticipant: participant?.participant, updated: { _ in - }, upgradedToSupergroup: upgradedToSupergroup, transferedOwnership: transferedOwnership)) - }) - dismissController = { [weak controller] in - controller?.dismiss() + pushControllerImpl?(controller) + + return current } - pushControllerImpl?(controller) - - return current - } + }) }) }, openAdmin: { participant in - pushControllerImpl?(channelAdminController(context: context, peerId: peerId, adminId: participant.peerId, initialParticipant: participant, updated: { _ in - }, upgradedToSupergroup: upgradedToSupergroup, transferedOwnership: transferedOwnership)) + let _ = (currentPeerId.get() + |> take(1) + |> deliverOnMainQueue).start(next: { peerId in + pushControllerImpl?(channelAdminController(context: context, peerId: peerId, adminId: participant.peerId, initialParticipant: participant, updated: { _ in + }, upgradedToSupergroup: upgradedToSupergroup, transferedOwnership: transferedOwnership)) + }) }) - let membersAndLoadMoreControl: (Disposable, PeerChannelMemberCategoryControl?) - if peerId.namespace == Namespaces.Peer.CloudChannel { - var didReportLoadCompleted = false - membersAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.admins(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) { membersState in - if case .loading = membersState.loadingState, membersState.list.isEmpty { - adminsPromise.set(.single(nil)) - } else { - adminsPromise.set(.single(membersState.list)) - if !didReportLoadCompleted { - didReportLoadCompleted = true - loadCompleted() - } - } - } - } else { - loadCompleted() - let membersDisposable = (peerView.get() - |> map { peerView -> [RenderedChannelParticipant]? in - guard let cachedData = peerView.cachedData as? CachedGroupData, let participants = cachedData.participants else { - return nil - } - var result: [RenderedChannelParticipant] = [] - var creatorPeer: Peer? - for participant in participants.participants { - if let peer = peerView.peers[participant.peerId] { - switch participant { - case .creator: - creatorPeer = peer - default: - break - } - } - } - guard let creator = creatorPeer else { - return nil - } - for participant in participants.participants { - if let peer = peerView.peers[participant.peerId] { - switch participant { - case .creator: - result.append(RenderedChannelParticipant(participant: .creator(id: peer.id, rank: nil), peer: peer)) - case .admin: - var peers: [PeerId: Peer] = [:] - peers[creator.id] = creator - peers[peer.id] = peer - result.append(RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(flags: .groupSpecific), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil), peer: peer, peers: peers)) - case .member: - break - } - } - } - return result - }).start(next: { members in - adminsPromise.set(.single(members)) - }) - membersAndLoadMoreControl = (membersDisposable, nil) - } + let membersAndLoadMoreControlValue = Atomic<(Disposable, PeerChannelMemberCategoryControl?)?>(value: nil) - let (membersDisposable, loadMoreControl) = membersAndLoadMoreControl - actionsDisposable.add(membersDisposable) + let membersDisposableValue = MetaDisposable() + actionsDisposable.add(membersDisposableValue) + + actionsDisposable.add((currentPeerId.get() + |> deliverOnMainQueue).start(next: { peerId in + if peerId.namespace == Namespaces.Peer.CloudChannel { + var didReportLoadCompleted = false + let membersAndLoadMoreControl: (Disposable, PeerChannelMemberCategoryControl?) = context.peerChannelMemberCategoriesContextsManager.admins(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) { membersState in + if case .loading = membersState.loadingState, membersState.list.isEmpty { + adminsPromise.set(.single(nil)) + } else { + adminsPromise.set(.single(membersState.list)) + if !didReportLoadCompleted { + didReportLoadCompleted = true + loadCompleted() + } + } + } + let _ = membersAndLoadMoreControlValue.swap(membersAndLoadMoreControl) + membersDisposableValue.set(membersAndLoadMoreControl.0) + } else { + loadCompleted() + let membersDisposable = (peerView.get() + |> map { peerView -> [RenderedChannelParticipant]? in + guard let cachedData = peerView.cachedData as? CachedGroupData, let participants = cachedData.participants else { + return nil + } + var result: [RenderedChannelParticipant] = [] + var creatorPeer: Peer? + for participant in participants.participants { + if let peer = peerView.peers[participant.peerId] { + switch participant { + case .creator: + creatorPeer = peer + default: + break + } + } + } + guard let creator = creatorPeer else { + return nil + } + for participant in participants.participants { + if let peer = peerView.peers[participant.peerId] { + switch participant { + case .creator: + result.append(RenderedChannelParticipant(participant: .creator(id: peer.id, rank: nil), peer: peer)) + case .admin: + var peers: [PeerId: Peer] = [:] + peers[creator.id] = creator + peers[peer.id] = peer + result.append(RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(flags: .groupSpecific), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil), peer: peer, peers: peers)) + case .member: + break + } + } + } + return result + }).start(next: { members in + adminsPromise.set(.single(members)) + }) + let membersAndLoadMoreControl: (Disposable, PeerChannelMemberCategoryControl?) = (membersDisposable, nil) + let _ = membersAndLoadMoreControlValue.swap(membersAndLoadMoreControl) + membersDisposableValue.set(membersAndLoadMoreControl.0) + } + })) var previousPeers: [RenderedChannelParticipant]? let signal = combineLatest(queue: .mainQueue(), presentationDataSignal, statePromise.get(), peerView.get(), adminsPromise.get() |> deliverOnMainQueue) |> deliverOnMainQueue |> map { presentationData, state, view, admins -> (ItemListControllerState, (ItemListNodeState, Any)) in + let peerId = view.peerId + var rightNavigationButton: ItemListNavigationButton? var secondaryRightNavigationButton: ItemListNavigationButton? if let admins = admins, admins.count > 1 { @@ -757,8 +794,8 @@ public func channelAdminsController(context: AccountContext, peerId: PeerId, loa emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(isGroup ? presentationData.strings.ChatAdmins_Title : presentationData.strings.Channel_Management_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(entries: channelAdminsControllerEntries(presentationData: presentationData, accountPeerId: context.account.peerId, view: view, state: state, participants: admins), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: previous != nil && admins != nil && previous!.count >= admins!.count) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(isGroup ? presentationData.strings.ChatAdmins_Title : presentationData.strings.Channel_Management_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelAdminsControllerEntries(presentationData: presentationData, accountPeerId: context.account.peerId, view: view, state: state, participants: admins), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: previous != nil && admins != nil && previous!.count >= admins!.count) return (controllerState, (listState, arguments)) } |> afterDisposed { @@ -782,16 +819,50 @@ public func channelAdminsController(context: AccountContext, peerId: PeerId, loa guard let controller = controller, let navigationController = controller.navigationController as? NavigationController else { return } - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(upgradedPeerId), keepStack: .never, animated: false, completion: { - navigationController.pushViewController(channelAdminsController(context: context, peerId: upgradedPeerId, loadCompleted: { - f() - }), animated: false) - })) + + var replacedSelf = false + rebuildControllerStackAfterSupergroupUpgrade(controller: controller, navigationController: navigationController, replace: { c in + if c === controller { + replacedSelf = true + return channelAdminsController(context: context, peerId: upgradedPeerId, loadCompleted: { + }) + } else { + return c + } + }) + f() } controller.visibleBottomContentOffsetChanged = { offset in if case let .known(value) = offset, value < 40.0 { - context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl) + let _ = (currentPeerId.get() + |> take(1) + |> deliverOnMainQueue).start(next: { peerId in + if let loadMoreControl = membersAndLoadMoreControlValue.with({ $0?.1 }) { + context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl) + } + }) } } return controller } + +func rebuildControllerStackAfterSupergroupUpgrade(controller: ViewController, navigationController: NavigationController, replace: ((UIViewController) -> UIViewController)? = nil) { + var controllers = navigationController.viewControllers + for i in 0 ..< controllers.count { + if controllers[i] === controller { + for j in 0 ..< i { + if controllers[j] is ChatController { + controllers.removeSubrange(j + 1 ... i - 1) + break + } + } + break + } + } + for i in 0 ..< controllers.count { + if let replace = replace { + controllers[i] = replace(controllers[i]) + } + } + navigationController.setViewControllers(controllers, animated: false) +} diff --git a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift index 8dd8d0e148..6e32a759d5 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift @@ -16,14 +16,14 @@ import PresentationDataUtils import ItemListAvatarAndNameInfoItem private final class ChannelBannedMemberControllerArguments { - let account: Account + let context: AccountContext let toggleRight: (TelegramChatBannedRightsFlags, Bool) -> Void let toggleRightWhileDisabled: (TelegramChatBannedRightsFlags) -> Void let openTimeout: () -> Void let delete: () -> Void - init(account: Account, toggleRight: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatBannedRightsFlags) -> Void, openTimeout: @escaping () -> Void, delete: @escaping () -> Void) { - self.account = account + init(context: AccountContext, toggleRight: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatBannedRightsFlags) -> Void, openTimeout: @escaping () -> Void, delete: @escaping () -> Void) { + self.context = context self.toggleRight = toggleRight self.toggleRightWhileDisabled = toggleRightWhileDisabled self.openTimeout = openTimeout @@ -221,29 +221,29 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { } } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ChannelBannedMemberControllerArguments switch self { case let .info(theme, strings, dateTimeFormat, peer, presence): - return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: presence, cachedData: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true, withExtendedBottomInset: false), editingNameUpdated: { _ in + return ItemListAvatarAndNameInfoItem(accountContext: arguments.context, presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: presence, cachedData: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true, withExtendedBottomInset: false), editingNameUpdated: { _ in }, avatarTapped: { }) case let .rightsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .rightItem(theme, _, text, right, value, enabled): - return ItemListSwitchItem(theme: theme, title: text, value: value, type: .icon, enableInteractiveChanges: enabled, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, type: .icon, enableInteractiveChanges: enabled, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleRight(right, value) }, activatedWhileDisabled: { arguments.toggleRightWhileDisabled(right) }) case let .timeout(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openTimeout() }) case let .exceptionInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .delete(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.delete() }) } @@ -390,11 +390,12 @@ public func channelBannedMemberController(context: AccountContext, peerId: PeerI var dismissImpl: (() -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? let peerView = Promise() peerView.set(context.account.viewTracker.peerView(peerId)) - let arguments = ChannelBannedMemberControllerArguments(account: context.account, toggleRight: { rights, value in + let arguments = ChannelBannedMemberControllerArguments(context: context, toggleRight: { rights, value in let _ = (peerView.get() |> take(1) |> deliverOnMainQueue).start(next: { view in @@ -461,7 +462,7 @@ public func channelBannedMemberController(context: AccountContext, peerId: PeerI }) }, openTimeout: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) let intervals: [Int32] = [ 1 * 60 * 60 * 24, 7 * 60 * 60 * 24, @@ -492,14 +493,14 @@ public func channelBannedMemberController(context: AccountContext, peerId: PeerI }), nil) })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) presentControllerImpl?(actionSheet, nil) }, delete: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: presentationData.strings.GroupPermission_Delete, color: .destructive, font: .default, enabled: true, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -517,7 +518,7 @@ public func channelBannedMemberController(context: AccountContext, peerId: PeerI })) })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -654,7 +655,15 @@ public func channelBannedMemberController(context: AccountContext, peerId: PeerI if peerId.namespace == Namespaces.Peer.CloudGroup { let signal = convertGroupToSupergroup(account: context.account, peerId: peerId) |> map(Optional.init) - |> `catch` { _ -> Signal in + |> `catch` { error -> Signal in + switch error { + case .tooManyChannels: + Queue.mainQueue().async { + pushControllerImpl?(oldChannelsController(context: context, intent: .upgrade)) + } + default: + break + } return .single(nil) } |> mapToSignal { upgradedPeerId -> Signal in @@ -679,6 +688,12 @@ public func channelBannedMemberController(context: AccountContext, peerId: PeerI upgradedToSupergroup(upgradedPeerId, { dismissImpl?() }) + } else { + updateState { current in + var current = current + current.updating = false + return current + } } }, error: { _ in updateState { current in @@ -703,7 +718,7 @@ public func channelBannedMemberController(context: AccountContext, peerId: PeerI } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: presentationData.strings.GroupPermission_ApplyAlertText(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).0)) items.append(ActionSheetButtonItem(title: presentationData.strings.GroupPermission_ApplyAlertAction, color: .accent, font: .default, enabled: true, action: { [weak actionSheet] in @@ -711,7 +726,7 @@ public func channelBannedMemberController(context: AccountContext, peerId: PeerI applyRights() })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -731,9 +746,9 @@ public func channelBannedMemberController(context: AccountContext, peerId: PeerI title = presentationData.strings.GroupPermission_NewTitle } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: channelBannedMemberControllerEntries(presentationData: presentationData, state: state, accountPeerId: context.account.peerId, channelView: channelView, memberView: memberView, initialParticipant: initialParticipant, initialBannedBy: initialBannedByPeer), style: .blocks, emptyStateItem: nil, animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelBannedMemberControllerEntries(presentationData: presentationData, state: state, accountPeerId: context.account.peerId, channelView: channelView, memberView: memberView, initialParticipant: initialParticipant, initialBannedBy: initialBannedByPeer), style: .blocks, emptyStateItem: nil, animateChanges: true) return (controllerState, (listState, arguments)) } @@ -742,11 +757,15 @@ public func channelBannedMemberController(context: AccountContext, peerId: PeerI } let controller = ItemListController(context: context, state: signal) + controller.navigationPresentation = .modal dismissImpl = { [weak controller] in controller?.dismiss() } presentControllerImpl = { [weak controller] value, presentationArguments in controller?.present(value, in: .window(.root), with: presentationArguments) } + pushControllerImpl = { [weak controller] c in + controller?.push(c) + } return controller } diff --git a/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift b/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift index 400d178646..f2751edd1b 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift @@ -16,15 +16,15 @@ import PresentationDataUtils import ItemListPeerItem private final class ChannelBlacklistControllerArguments { - let account: Account + let context: AccountContext let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let addPeer: () -> Void let removePeer: (PeerId) -> Void let openPeer: (RenderedChannelParticipant) -> Void - init(account: Account, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (RenderedChannelParticipant) -> Void) { - self.account = account + init(context: AccountContext, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (RenderedChannelParticipant) -> Void) { + self.context = context self.addPeer = addPeer self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.removePeer = removePeer @@ -148,17 +148,17 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry { } } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ChannelBlacklistControllerArguments switch self { case let .add(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.addPeer() }) case let .addInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .bannedHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .peerItem(theme, strings, dateTimeFormat, nameDisplayOrder, _, participant, editing, enabled): var text: ItemListPeerItemText = .none switch participant.participant { @@ -169,7 +169,7 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry { default: break } - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: participant.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: { + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: participant.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: { arguments.openPeer(participant) }, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) @@ -291,7 +291,7 @@ public func channelBlacklistController(context: AccountContext, peerId: PeerId) peerView.set(context.account.viewTracker.peerView(peerId)) let blacklistPromise = Promise<[RenderedChannelParticipant]?>(nil) - let arguments = ChannelBlacklistControllerArguments(account: context.account, setPeerIdWithRevealedOptions: { peerId, fromPeerId in + let arguments = ChannelBlacklistControllerArguments(context: context, setPeerIdWithRevealedOptions: { peerId, fromPeerId in updateState { state in if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { return state.withUpdatedPeerIdWithRevealedOptions(peerId) @@ -359,14 +359,14 @@ public func channelBlacklistController(context: AccountContext, peerId: PeerId) return } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetItem] = [] if !participant.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder).isEmpty { items.append(ActionSheetTextItem(title: participant.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))) } items.append(ActionSheetButtonItem(title: presentationData.strings.GroupRemoved_ViewUserInfo, action: { [weak actionSheet] in actionSheet?.dismissAnimated() - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: participant.peer, mode: .generic) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: participant.peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { pushControllerImpl?(infoController) } })) @@ -418,7 +418,7 @@ public func channelBlacklistController(context: AccountContext, peerId: PeerId) })) })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -498,8 +498,8 @@ public func channelBlacklistController(context: AccountContext, peerId: PeerId) }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.GroupRemoved_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(entries: channelBlacklistControllerEntries(presentationData: presentationData, view: view, state: state, participants: participants), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: previous != nil && participants != nil && previous!.count >= participants!.count) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.GroupRemoved_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelBlacklistControllerEntries(presentationData: presentationData, view: view, state: state, participants: participants), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: previous != nil && participants != nil && previous!.count >= participants!.count) return (controllerState, (listState, arguments)) } diff --git a/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupActionSheetItem.swift b/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupActionSheetItem.swift index 282532c2cd..7655343750 100644 --- a/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupActionSheetItem.swift +++ b/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupActionSheetItem.swift @@ -66,8 +66,8 @@ private final class ChannelDiscussionGroupActionSheetItemNode: ActionSheetItemNo self.addSubnode(self.channelAvatarNode) self.addSubnode(self.textNode) - self.channelAvatarNode.setPeer(account: context.account, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: channelPeer) - self.groupAvatarNode.setPeer(account: context.account, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: groupPeer) + self.channelAvatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: channelPeer) + self.groupAvatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: groupPeer) let text: (String, [(Int, NSRange)]) if let channelPeer = channelPeer as? TelegramChannel, let addressName = channelPeer.addressName, !addressName.isEmpty { @@ -75,9 +75,13 @@ private final class ChannelDiscussionGroupActionSheetItemNode: ActionSheetItemNo } else { text = strings.Channel_DiscussionGroup_PrivateChannelLink(groupPeer.displayTitle(strings: strings, displayOrder: nameDisplayOrder), channelPeer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)) } - let attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: text.0, font: Font.regular(14.0), textColor: theme.primaryTextColor)) + + let textFont = Font.regular(floor(theme.baseFontSize * 14.0 / 17.0)) + let boldFont = Font.semibold(floor(theme.baseFontSize * 14.0 / 17.0)) + + let attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: text.0, font: textFont, textColor: theme.primaryTextColor)) for (_, range) in text.1 { - attributedText.addAttribute(.font, value: Font.semibold(14.0), range: range) + attributedText.addAttribute(.font, value: boldFont, range: range) } self.textNode.attributedText = attributedText diff --git a/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSearchContainerNode.swift b/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSearchContainerNode.swift index bff381d31e..a09237a99c 100644 --- a/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSearchContainerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSearchContainerNode.swift @@ -12,6 +12,7 @@ import MergeLists import AccountContext import SearchUI import ContactsPeerItem +import ItemListUI private enum ChannelDiscussionGroupSearchContent: Equatable { case peer(Peer) @@ -68,10 +69,10 @@ private final class ChannelDiscussionGroupSearchEntry: Comparable, Identifiable return lhs.index < rhs.index } - func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: ChannelDiscussionGroupSearchInteraction) -> ListViewItem { + func item(context: AccountContext, presentationData: PresentationData, interaction: ChannelDiscussionGroupSearchInteraction) -> ListViewItem { switch self.content { case let .peer(peer): - return ContactsPeerItem(theme: theme, strings: strings, sortOrder: .firstLast, displayOrder: .firstLast, account: account, peerMode: .peer, peer: .peer(peer: peer, chatPeer: peer), status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .peer, peer: .peer(peer: peer, chatPeer: peer), status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in interaction.peerSelected(peer) }) } @@ -85,12 +86,12 @@ struct ChannelDiscussionGroupSearchContainerTransition { let isSearching: Bool } -private func channelDiscussionGroupSearchContainerPreparedRecentTransition(from fromEntries: [ChannelDiscussionGroupSearchEntry], to toEntries: [ChannelDiscussionGroupSearchEntry], isSearching: Bool, account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: ChannelDiscussionGroupSearchInteraction) -> ChannelDiscussionGroupSearchContainerTransition { +private func channelDiscussionGroupSearchContainerPreparedRecentTransition(from fromEntries: [ChannelDiscussionGroupSearchEntry], to toEntries: [ChannelDiscussionGroupSearchEntry], isSearching: Bool, context: AccountContext, presentationData: PresentationData, interaction: ChannelDiscussionGroupSearchInteraction) -> ChannelDiscussionGroupSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } return ChannelDiscussionGroupSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) } @@ -114,14 +115,14 @@ final class ChannelDiscussionGroupSearchContainerNode: SearchDisplayControllerCo private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, PresentationDateTimeFormat)> + private let presentationDataPromise: Promise init(context: AccountContext, peers: [Peer], openPeer: @escaping (Peer) -> Void) { self.context = context self.openPeer = openPeer self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings, self.presentationData.nameSortOrder, self.presentationData.nameDisplayOrder, self.presentationData.dateTimeFormat)) + self.presentationDataPromise = Promise(self.presentationData) self.dimNode = ASDisplayNode() self.listNode = ListView() @@ -186,12 +187,12 @@ final class ChannelDiscussionGroupSearchContainerNode: SearchDisplayControllerCo let previousSearchItems = Atomic<[ChannelDiscussionGroupSearchEntry]?>(value: nil) - self.searchDisposable.set((combineLatest(foundItems, self.themeAndStringsPromise.get()) - |> deliverOnMainQueue).start(next: { [weak self] entries, themeAndStrings in + self.searchDisposable.set((combineLatest(foundItems, self.presentationDataPromise.get()) + |> deliverOnMainQueue).start(next: { [weak self] entries, presentationData in if let strongSelf = self { let previousEntries = previousSearchItems.swap(entries) let firstTime = previousEntries == nil - let transition = channelDiscussionGroupSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, account: context.account, theme: themeAndStrings.0, strings: themeAndStrings.1, interaction: interaction) + let transition = channelDiscussionGroupSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, context: context, presentationData: presentationData, interaction: interaction) strongSelf.enqueueTransition(transition, firstTime: firstTime) } })) @@ -267,27 +268,7 @@ final class ChannelDiscussionGroupSearchContainerNode: SearchDisplayControllerCo override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: nil) - } + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight @@ -296,7 +277,7 @@ final class ChannelDiscussionGroupSearchContainerNode: SearchDisplayControllerCo self.dimNode.frame = CGRect(origin: CGPoint(), size: layout.size) self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if !hasValidLayout { hasValidLayout = true diff --git a/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift b/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift index 495180435f..eea39f5c4c 100644 --- a/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift @@ -17,13 +17,13 @@ import ItemListPeerItem import ItemListPeerActionItem private final class ChannelDiscussionGroupSetupControllerArguments { - let account: Account + let context: AccountContext let createGroup: () -> Void let selectGroup: (PeerId) -> Void let unlinkGroup: () -> Void - init(account: Account, createGroup: @escaping () -> Void, selectGroup: @escaping (PeerId) -> Void, unlinkGroup: @escaping () -> Void) { - self.account = account + init(context: AccountContext, createGroup: @escaping () -> Void, selectGroup: @escaping (PeerId) -> Void, unlinkGroup: @escaping () -> Void) { + self.context = context self.createGroup = createGroup self.selectGroup = selectGroup self.unlinkGroup = unlinkGroup @@ -128,13 +128,13 @@ private enum ChannelDiscussionGroupSetupControllerEntry: ItemListNodeEntry { return lhs.sortIndex < rhs.sortIndex } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ChannelDiscussionGroupSetupControllerArguments switch self { case let .header(theme, strings, title, isGroup, label): return ChannelDiscussionGroupSetupHeaderItem(theme: theme, strings: strings, title: title, isGroup: isGroup, label: label, sectionId: self.section) case let .create(theme, text): - return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, editing: false, action: { + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, editing: false, action: { arguments.createGroup() }) case let .group(_, theme, strings, peer, nameOrder): @@ -144,13 +144,13 @@ private enum ChannelDiscussionGroupSetupControllerEntry: ItemListNodeEntry { } else { text = strings.Channel_DiscussionGroup_PrivateGroup } - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: "."), nameDisplayOrder: nameOrder, account: arguments.account, peer: peer, aliasHandling: .standard, nameStyle: .plain, presence: nil, text: .text(text), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: "."), nameDisplayOrder: nameOrder, context: arguments.context, peer: peer, aliasHandling: .standard, nameStyle: .plain, presence: nil, text: .text(text), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { arguments.selectGroup(peer.id) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) case let .groupsInfo(theme, title): - return ItemListTextItem(theme: theme, text: .plain(title), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(title), sectionId: self.section) case let .unlink(theme, title): - return ItemListActionItem(theme: theme, title: title, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: title, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.unlinkGroup() }) } @@ -237,7 +237,7 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI let applyGroupDisposable = MetaDisposable() actionsDisposable.add(applyGroupDisposable) - let arguments = ChannelDiscussionGroupSetupControllerArguments(account: context.account, createGroup: { + let arguments = ChannelDiscussionGroupSetupControllerArguments(context: context, createGroup: { let _ = (context.account.postbox.transaction { transaction -> Peer? in transaction.getPeer(peerId) } @@ -305,7 +305,7 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ChannelDiscussionGroupActionSheetItem(context: context, channelPeer: channelPeer, groupPeer: groupPeer, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder), ActionSheetButtonItem(title: presentationData.strings.Channel_DiscussionGroup_LinkGroup, color: .accent, action: { [weak actionSheet] in @@ -315,8 +315,13 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI var updatedPeerId: PeerId? = nil if let legacyGroup = groupPeer as? TelegramGroup { applySignal = convertGroupToSupergroup(account: context.account, peerId: legacyGroup.id) - |> mapError { _ -> ChannelDiscussionGroupError in - return .generic + |> mapError { error -> ChannelDiscussionGroupError in + switch error { + case .tooManyChannels: + return .tooManyChannels + default: + return .generic + } } |> deliverOnMainQueue |> mapToSignal { resultPeerId -> Signal in @@ -378,6 +383,8 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI applyGroupDisposable.set((applySignal |> deliverOnMainQueue).start(error: { error in switch error { + case .tooManyChannels: + pushControllerImpl?(oldChannelsController(context: context, intent: .upgrade)) case .generic, .hasNotPermissions: let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) @@ -458,7 +465,7 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI })) }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -591,8 +598,8 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI } } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: channelDiscussionGroupSetupControllerEntries(presentationData: presentationData, view: view, groups: groups), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelDiscussionGroupSetupControllerEntries(presentationData: presentationData, view: view, groups: groups), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/PeerInfoUI/Sources/ChannelInfoController.swift b/submodules/PeerInfoUI/Sources/ChannelInfoController.swift index 1767eafb52..a227e4aefc 100644 --- a/submodules/PeerInfoUI/Sources/ChannelInfoController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelInfoController.swift @@ -28,7 +28,7 @@ import NotificationSoundSelectionUI import Markdown private final class ChannelInfoControllerArguments { - let account: Account + let context: AccountContext let avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext let tapAvatarAction: () -> Void let changeProfilePhoto: () -> Void @@ -48,9 +48,10 @@ private final class ChannelInfoControllerArguments { let displayAddressNameContextMenu: (String) -> Void let displayContextMenu: (ChannelInfoEntryTag, String) -> Void let aboutLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void - let toggleSignatures:(Bool) -> Void - init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, openChannelTypeSetup: @escaping () -> Void, openDiscussionGroupSetup: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openStats: @escaping () -> Void, openAdmins: @escaping () -> Void, openMembers: @escaping () -> Void, openBanned: @escaping () -> Void, reportChannel: @escaping () -> Void, leaveChannel: @escaping () -> Void, deleteChannel: @escaping () -> Void, displayAddressNameContextMenu: @escaping (String) -> Void, displayContextMenu: @escaping (ChannelInfoEntryTag, String) -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void, toggleSignatures: @escaping(Bool)->Void) { - self.account = account + let toggleSignatures: (Bool) -> Void + + init(context: AccountContext, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, openChannelTypeSetup: @escaping () -> Void, openDiscussionGroupSetup: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openStats: @escaping () -> Void, openAdmins: @escaping () -> Void, openMembers: @escaping () -> Void, openBanned: @escaping () -> Void, reportChannel: @escaping () -> Void, leaveChannel: @escaping () -> Void, deleteChannel: @escaping () -> Void, displayAddressNameContextMenu: @escaping (String) -> Void, displayContextMenu: @escaping (ChannelInfoEntryTag, String) -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void, toggleSignatures: @escaping(Bool)->Void) { + self.context = context self.avatarAndNameInfoContext = avatarAndNameInfoContext self.tapAvatarAction = tapAvatarAction self.changeProfilePhoto = changeProfilePhoto @@ -322,85 +323,85 @@ private enum ChannelInfoEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ChannelInfoControllerArguments switch self { case let .info(theme, strings, dateTimeFormat, peer, cachedData, state, updatingAvatar): - return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in + return ItemListAvatarAndNameInfoItem(accountContext: arguments.context, presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { arguments.tapAvatarAction() }, context: arguments.avatarAndNameInfoContext, updatingImage: updatingAvatar) case let .about(theme, text, value): - return ItemListTextWithLabelItem(theme: theme, label: text, text: foldMultipleLineBreaks(value), enabledEntityTypes: [.url, .mention, .hashtag], multiline: true, sectionId: self.section, action: nil, longTapAction: { + return ItemListTextWithLabelItem(presentationData: presentationData, label: text, text: foldMultipleLineBreaks(value), enabledEntityTypes: [.url, .mention, .hashtag], multiline: true, sectionId: self.section, action: nil, longTapAction: { arguments.displayContextMenu(ChannelInfoEntryTag.about, value) }, linkItemAction: { action, itemLink in arguments.aboutLinkAction(action, itemLink) }, tag: ChannelInfoEntryTag.about) case let .addressName(theme, text, value): - return ItemListTextWithLabelItem(theme: theme, label: text, text: "https://t.me/\(value)", textColor: .accent, enabledEntityTypes: [], multiline: false, sectionId: self.section, action: { + return ItemListTextWithLabelItem(presentationData: presentationData, label: text, text: "https://t.me/\(value)", textColor: .accent, enabledEntityTypes: [], multiline: false, sectionId: self.section, action: { arguments.displayAddressNameContextMenu("https://t.me/\(value)") }, longTapAction: { arguments.displayContextMenu(ChannelInfoEntryTag.link, "https://t.me/\(value)") }, tag: ChannelInfoEntryTag.link) case let .channelPhotoSetup(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.changeProfilePhoto() }) case let .channelTypeSetup(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .plain, action: { arguments.openChannelTypeSetup() }) case let .discussionGroupSetup(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .plain, action: { arguments.openDiscussionGroupSetup() }) case let .discussionGroupSetupInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .channelDescriptionSetup(theme, placeholder, value): - return ItemListMultilineInputItem(theme: theme, text: value, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: 255, display: true), sectionId: self.section, style: .plain, textUpdated: { updatedText in + return ItemListMultilineInputItem(presentationData: presentationData, text: value, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: 255, display: true), sectionId: self.section, style: .plain, textUpdated: { updatedText in arguments.updateEditingDescriptionText(updatedText) }) case let .admins(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .plain, action: { arguments.openAdmins() }) case let .members(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .plain, action: { arguments.openMembers() }) case let .banned(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .plain, action: { arguments.openBanned() }) case let .signMessages(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .plain, updated: { updated in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .plain, updated: { updated in arguments.toggleSignatures(updated) }) case let .signInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section, style: .plain) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section, style: .plain) case let .sharedMedia(theme, text): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .plain, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .plain, action: { arguments.openSharedMedia() }) case let .stats(theme, text): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .plain, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .plain, action: { arguments.openStats() }) case let .notifications(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .plain, action: { arguments.changeNotificationMuteSettings() }) case let .report(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.reportChannel() }) case let .leave(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.leaveChannel() }) case let .deleteChannel(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.deleteChannel() }) } @@ -686,7 +687,7 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi var displayContextMenuImpl: ((ChannelInfoEntryTag, String) -> Void)? var aboutLinkActionImpl: ((TextLinkItemActionType, TextLinkItem) -> Void)? - let arguments = ChannelInfoControllerArguments(account: context.account, avatarAndNameInfoContext: avatarAndNameInfoContext, tapAvatarAction: { + let arguments = ChannelInfoControllerArguments(context: context, avatarAndNameInfoContext: avatarAndNameInfoContext, tapAvatarAction: { let _ = (context.account.postbox.loadedPeerWithId(peerId) |> take(1) |> deliverOnMainQueue).start(next: { peer in if peer.profileImageRepresentations.isEmpty { return @@ -905,7 +906,7 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi return } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -929,7 +930,7 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi return } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -1086,8 +1087,8 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi crossfadeState = true } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.UserInfo_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: channelInfoEntries(account: context.account, presentationData: presentationData, view: view, globalNotificationSettings: globalNotificationSettings, state: state), style: .plain, crossfadeState: crossfadeState, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.UserInfo_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelInfoEntries(account: context.account, presentationData: presentationData, view: view, globalNotificationSettings: globalNotificationSettings, state: state), style: .plain, crossfadeState: crossfadeState, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { @@ -1123,7 +1124,7 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi } avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { - var result: ((ASDisplayNode, () -> (UIView?, UIView?)), CGRect)? + var result: ((ASDisplayNode, CGRect, () -> (UIView?, UIView?)), CGRect)? controller.forEachItemNode { itemNode in if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { result = itemNode.avatarTransitionNode() diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift index 05e0c8f130..cb1c05e765 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift @@ -15,15 +15,15 @@ import PresentationDataUtils import ItemListPeerItem private final class ChannelMembersControllerArguments { - let account: Account + let context: AccountContext let addMember: () -> Void let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let removePeer: (PeerId) -> Void let openPeer: (Peer) -> Void let inviteViaLink: ()->Void - init(account: Account, addMember: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (Peer) -> Void, inviteViaLink: @escaping()->Void) { - self.account = account + init(context: AccountContext, addMember: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (Peer) -> Void, inviteViaLink: @escaping()->Void) { + self.context = context self.addMember = addMember self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.removePeer = removePeer @@ -178,19 +178,19 @@ private enum ChannelMembersEntry: ItemListNodeEntry { } } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ChannelMembersControllerArguments switch self { case let .addMember(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.addMember() }) case let .inviteLink(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.inviteViaLink() }) case let .addMemberInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .peerItem(_, theme, strings, dateTimeFormat, nameDisplayOrder, participant, editing, enabled): let text: ItemListPeerItemText if let user = participant.peer as? TelegramUser, let _ = user.botInfo { @@ -198,7 +198,7 @@ private enum ChannelMembersEntry: ItemListNodeEntry { } else { text = .presence } - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: participant.peer, presence: participant.presences[participant.peer.id], text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: { + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: participant.peer, presence: participant.presences[participant.peer.id], text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: { arguments.openPeer(participant.peer) }, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) @@ -351,7 +351,7 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) -> let peersPromise = Promise<[RenderedChannelParticipant]?>(nil) - let arguments = ChannelMembersControllerArguments(account: context.account, addMember: { + let arguments = ChannelMembersControllerArguments(context: context, addMember: { actionsDisposable.add((peersPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { members in @@ -450,7 +450,7 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) -> } })) }, openPeer: { peer in - if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { pushControllerImpl?(controller) } }, inviteViaLink: { @@ -502,7 +502,7 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) -> return state.withUpdatedSearchingMembers(false) } }, openPeer: { peer, _ in - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { pushControllerImpl?(infoController) } }, pushController: { c in @@ -520,8 +520,8 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) -> let previous = previousPeers previousPeers = peers - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Channel_Subscribers_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(entries: ChannelMembersControllerEntries(context: context, presentationData: presentationData, view: view, state: state, participants: peers), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Channel_Subscribers_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: ChannelMembersControllerEntries(context: context, presentationData: presentationData, view: view, state: state, participants: peers), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) return (controllerState, (listState, arguments)) } diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift index 6bca722680..e6ba424e59 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift @@ -15,8 +15,9 @@ import SearchUI import ItemListPeerItem import ContactsPeerItem import ChatListSearchItemHeader +import ItemListUI -enum ChannelMembersSearchMode { +public enum ChannelMembersSearchMode { case searchMembers case searchAdmins case searchBanned @@ -138,10 +139,10 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable { return lhs.index < rhs.index } - func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: ChannelMembersSearchContainerInteraction) -> ListViewItem { + func item(context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: ChannelMembersSearchContainerInteraction) -> ListViewItem { switch self.content { case let .peer(peer): - return ContactsPeerItem(theme: theme, strings: strings, sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, account: account, peerMode: .peer, peer: .peer(peer: peer, chatPeer: peer), status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: theme, strings: strings, actionTitle: nil, action: nil) }), action: { _ in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: peer, chatPeer: peer), status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) }), action: { _ in interaction.peerSelected(peer, nil) }) case let .participant(participant, label, revealActions, revealed, enabled): @@ -175,7 +176,7 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable { actionIcon = .add } - return ContactsPeerItem(theme: theme, strings: strings, sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, account: account, peerMode: .peer, peer: .peer(peer: participant.peer, chatPeer: participant.peer), status: status, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: revealed), options: options, actionIcon: actionIcon, index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: theme, strings: strings, actionTitle: nil, action: nil) }), action: { _ in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: participant.peer, chatPeer: participant.peer), status: status, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: revealed), options: options, actionIcon: actionIcon, index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) }), action: { _ in interaction.peerSelected(participant.peer, participant) }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in interaction.setPeerIdWithRevealedOptions(RevealedPeerId(peerId: participant.peer.id, section: self.section), fromPeerId.flatMap({ RevealedPeerId(peerId: $0, section: self.section) })) @@ -238,10 +239,10 @@ private struct GroupMembersSearchContextState { var members: [RenderedChannelParticipant] = [] } -final class GroupMembersSearchContext { +public final class GroupMembersSearchContext { fileprivate let state = Promise() - init(context: AccountContext, peerId: PeerId) { + public init(context: AccountContext, peerId: PeerId) { assert(Queue.mainQueue().isCurrent()) let combinedSignal = combineLatest(queue: .mainQueue(), categorySignal(context: context, peerId: peerId, category: .contacts), categorySignal(context: context, peerId: peerId, category: .bots), categorySignal(context: context, peerId: peerId, category: .admins), categorySignal(context: context, peerId: peerId, category: .members)) @@ -259,12 +260,12 @@ final class GroupMembersSearchContext { } } -private func channelMembersSearchContainerPreparedRecentTransition(from fromEntries: [ChannelMembersSearchEntry], to toEntries: [ChannelMembersSearchEntry], isSearching: Bool, account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: ChannelMembersSearchContainerInteraction) -> ChannelMembersSearchContainerTransition { +private func channelMembersSearchContainerPreparedRecentTransition(from fromEntries: [ChannelMembersSearchEntry], to toEntries: [ChannelMembersSearchEntry], isSearching: Bool, context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: ChannelMembersSearchContainerInteraction) -> ChannelMembersSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) } return ChannelMembersSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) } @@ -274,7 +275,7 @@ private struct ChannelMembersSearchContainerState: Equatable { var removingParticipantIds = Set() } -final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNode { +public final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNode { private let context: AccountContext private let openPeer: (Peer, RenderedChannelParticipant?) -> Void private let mode: ChannelMembersSearchMode @@ -295,15 +296,15 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod private let removeMemberDisposable = MetaDisposable() - private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, PresentationDateTimeFormat)> + private let presentationDataPromise: Promise - init(context: AccountContext, peerId: PeerId, mode: ChannelMembersSearchMode, filters: [ChannelMembersSearchFilter], searchContext: GroupMembersSearchContext?, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void, updateActivity: @escaping (Bool) -> Void, pushController: @escaping (ViewController) -> Void) { + public init(context: AccountContext, peerId: PeerId, mode: ChannelMembersSearchMode, filters: [ChannelMembersSearchFilter], searchContext: GroupMembersSearchContext?, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void, updateActivity: @escaping (Bool) -> Void, pushController: @escaping (ViewController) -> Void) { self.context = context self.openPeer = openPeer self.mode = mode self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings, self.presentationData.nameSortOrder, self.presentationData.nameDisplayOrder, self.presentationData.dateTimeFormat)) + self.presentationDataPromise = Promise(self.presentationData) self.emptyQueryListNode = ListView() self.listNode = ListView() @@ -433,12 +434,12 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod removeMemberDisposable.set(signal.start()) }) - let themeAndStringsPromise = self.themeAndStringsPromise + let presentationDataPromise = self.presentationDataPromise let emptyQueryItems: Signal<[ChannelMembersSearchEntry]?, NoError> if let searchContext = searchContext { - emptyQueryItems = combineLatest(queue: .mainQueue(), statePromise.get(), searchContext.state.get(), context.account.postbox.peerView(id: peerId) |> take(1), themeAndStringsPromise.get()) - |> map { state, searchState, peerView, themeAndStrings -> [ChannelMembersSearchEntry]? in + emptyQueryItems = combineLatest(queue: .mainQueue(), statePromise.get(), searchContext.state.get(), context.account.postbox.peerView(id: peerId) |> take(1), presentationDataPromise.get()) + |> map { state, searchState, peerView, presentationData -> [ChannelMembersSearchEntry]? in if let channel = peerView.peers[peerId] as? TelegramChannel { var entries: [ChannelMembersSearchEntry] = [] @@ -483,7 +484,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod if case .searchMembers = mode { switch participant.participant { case .creator: - label = themeAndStrings.1.Channel_Management_LabelOwner + label = presentationData.strings.Channel_Management_LabelOwner default: break } @@ -496,15 +497,15 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod var peerActions: [ParticipantRevealAction] = [] if case .searchMembers = mode { if canPromote { - peerActions.append(ParticipantRevealAction(type: .neutral, title: themeAndStrings.1.GroupInfo_ActionPromote, action: .promote)) + peerActions.append(ParticipantRevealAction(type: .neutral, title: presentationData.strings.GroupInfo_ActionPromote, action: .promote)) } if canRestrict { - peerActions.append(ParticipantRevealAction(type: .warning, title: themeAndStrings.1.GroupInfo_ActionRestrict, action: .restrict)) - peerActions.append(ParticipantRevealAction(type: .destructive, title: themeAndStrings.1.Common_Delete, action: .remove)) + peerActions.append(ParticipantRevealAction(type: .warning, title: presentationData.strings.GroupInfo_ActionRestrict, action: .restrict)) + peerActions.append(ParticipantRevealAction(type: .destructive, title: presentationData.strings.Common_Delete, action: .remove)) } } - entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: peerActions, revealed: state.revealedPeerId == RevealedPeerId(peerId: participant.peer.id, section: section), enabled: enabled), section: section, dateTimeFormat: themeAndStrings.4)) + entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: peerActions, revealed: state.revealedPeerId == RevealedPeerId(peerId: participant.peer.id, section: section), enabled: enabled), section: section, dateTimeFormat: presentationData.dateTimeFormat)) index += 1 } @@ -617,8 +618,8 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod foundRemotePeers = .single(([], [])) } - return combineLatest(foundGroupMembers, foundMembers, foundContacts, foundRemotePeers, themeAndStringsPromise.get(), statePromise.get()) - |> map { foundGroupMembers, foundMembers, foundContacts, foundRemotePeers, themeAndStrings, state -> [ChannelMembersSearchEntry]? in + return combineLatest(foundGroupMembers, foundMembers, foundContacts, foundRemotePeers, presentationDataPromise.get(), statePromise.get()) + |> map { foundGroupMembers, foundMembers, foundContacts, foundRemotePeers, presentationData, state -> [ChannelMembersSearchEntry]? in var entries: [ChannelMembersSearchEntry] = [] var existingPeerIds = Set() @@ -640,6 +641,10 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod var index = 0 for participant in foundGroupMembers { + if participant.peer.isDeleted { + continue + } + if !existingPeerIds.contains(participant.peer.id) { existingPeerIds.insert(participant.peer.id) let section: ChannelMembersSearchSection @@ -689,16 +694,16 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod var enabled = true if case .banAndPromoteActions = mode { if case .creator = participant.participant { - label = themeAndStrings.1.Channel_Management_LabelOwner + label = presentationData.strings.Channel_Management_LabelOwner enabled = false } } else if case .searchMembers = mode { switch participant.participant { case .creator: - label = themeAndStrings.1.Channel_Management_LabelOwner + label = presentationData.strings.Channel_Management_LabelOwner case let .member(member): if member.adminInfo != nil { - label = themeAndStrings.1.Channel_Management_LabelEditor + label = presentationData.strings.Channel_Management_LabelEditor } } } @@ -710,15 +715,15 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod var peerActions: [ParticipantRevealAction] = [] if case .searchMembers = mode { if canPromote { - peerActions.append(ParticipantRevealAction(type: .neutral, title: themeAndStrings.1.GroupInfo_ActionPromote, action: .promote)) + peerActions.append(ParticipantRevealAction(type: .neutral, title: presentationData.strings.GroupInfo_ActionPromote, action: .promote)) } if canRestrict { - peerActions.append(ParticipantRevealAction(type: .warning, title: themeAndStrings.1.GroupInfo_ActionRestrict, action: .restrict)) - peerActions.append(ParticipantRevealAction(type: .destructive, title: themeAndStrings.1.Common_Delete, action: .remove)) + peerActions.append(ParticipantRevealAction(type: .warning, title: presentationData.strings.GroupInfo_ActionRestrict, action: .restrict)) + peerActions.append(ParticipantRevealAction(type: .destructive, title: presentationData.strings.Common_Delete, action: .remove)) } } else if case .searchAdmins = mode { if canRestrict { - peerActions.append(ParticipantRevealAction(type: .destructive, title: themeAndStrings.1.Common_Delete, action: .remove)) + peerActions.append(ParticipantRevealAction(type: .destructive, title: presentationData.strings.Common_Delete, action: .remove)) } } @@ -726,14 +731,14 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod case .searchAdmins: switch participant.participant { case .creator: - label = themeAndStrings.1.Channel_Management_LabelOwner + label = presentationData.strings.Channel_Management_LabelOwner case let .member(_, _, adminInfo, _, _): if let adminInfo = adminInfo { if let peer = participant.peers[adminInfo.promotedBy] { if peer.id == participant.peer.id { - label = themeAndStrings.1.Channel_Management_LabelAdministrator + label = presentationData.strings.Channel_Management_LabelAdministrator } else { - label = themeAndStrings.1.Channel_Management_PromotedBy(peer.displayTitle(strings: themeAndStrings.1, displayOrder: themeAndStrings.3)).0 + label = presentationData.strings.Channel_Management_PromotedBy(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).0 } } } @@ -748,7 +753,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod if !exceptionsString.isEmpty { exceptionsString.append(", ") } - exceptionsString.append(compactStringForGroupPermission(strings: themeAndStrings.1, right: rights)) + exceptionsString.append(compactStringForGroupPermission(strings: presentationData.strings, right: rights)) } } label = exceptionsString @@ -760,7 +765,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod switch participant.participant { case let .member(_, _, _, banInfo, _): if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] { - label = themeAndStrings.1.Channel_Management_RemovedBy(peer.displayTitle(strings: themeAndStrings.1, displayOrder: themeAndStrings.3)).0 + label = presentationData.strings.Channel_Management_RemovedBy(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).0 } default: break @@ -768,7 +773,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod default: break } - entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: peerActions, revealed: state.revealedPeerId == RevealedPeerId(peerId: participant.peer.id, section: section), enabled: enabled), section: section, dateTimeFormat: themeAndStrings.4)) + entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: peerActions, revealed: state.revealedPeerId == RevealedPeerId(peerId: participant.peer.id, section: section), enabled: enabled), section: section, dateTimeFormat: presentationData.dateTimeFormat)) index += 1 } } @@ -792,12 +797,12 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod var enabled = true if case .banAndPromoteActions = mode { if case .creator = participant.participant { - label = themeAndStrings.1.Channel_Management_LabelOwner + label = presentationData.strings.Channel_Management_LabelOwner enabled = false } } - entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: [], revealed: false, enabled: enabled), section: section, dateTimeFormat: themeAndStrings.4, addIcon: addIcon)) + entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: [], revealed: false, enabled: enabled), section: section, dateTimeFormat: presentationData.dateTimeFormat, addIcon: addIcon)) index += 1 } } @@ -805,7 +810,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod for peer in foundContacts.0 { if !existingPeerIds.contains(peer.id) { existingPeerIds.insert(peer.id) - entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .contacts, dateTimeFormat: themeAndStrings.4)) + entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .contacts, dateTimeFormat: presentationData.dateTimeFormat)) index += 1 } } @@ -814,7 +819,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod let peer = foundPeer.peer if !existingPeerIds.contains(peer.id) && peer is TelegramUser { existingPeerIds.insert(peer.id) - entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: themeAndStrings.4)) + entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: presentationData.dateTimeFormat)) index += 1 } } @@ -823,7 +828,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod let peer = foundPeer.peer if !existingPeerIds.contains(peer.id) && peer is TelegramUser { existingPeerIds.insert(peer.id) - entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: themeAndStrings.4)) + entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: presentationData.dateTimeFormat)) index += 1 } } @@ -901,8 +906,8 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod foundRemotePeers = .single(([], [])) } - return combineLatest(foundGroupMembers, foundMembers, foundRemotePeers, themeAndStringsPromise.get(), statePromise.get()) - |> map { foundGroupMembers, foundMembers, foundRemotePeers, themeAndStrings, state -> [ChannelMembersSearchEntry]? in + return combineLatest(foundGroupMembers, foundMembers, foundRemotePeers, presentationDataPromise.get(), statePromise.get()) + |> map { foundGroupMembers, foundMembers, foundRemotePeers, presentationData, state -> [ChannelMembersSearchEntry]? in var entries: [ChannelMembersSearchEntry] = [] var existingPeerIds = Set() @@ -973,16 +978,16 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod var enabled = true if case .banAndPromoteActions = mode { if case .creator = participant.participant { - label = themeAndStrings.1.Channel_Management_LabelOwner + label = presentationData.strings.Channel_Management_LabelOwner enabled = false } } else if case .searchMembers = mode { switch participant.participant { case .creator: - label = themeAndStrings.1.Channel_Management_LabelOwner + label = presentationData.strings.Channel_Management_LabelOwner case let .member(member): if member.adminInfo != nil { - label = themeAndStrings.1.Channel_Management_LabelEditor + label = presentationData.strings.Channel_Management_LabelEditor } } } @@ -994,11 +999,11 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod var peerActions: [ParticipantRevealAction] = [] /*if case .searchMembers = mode { if canPromote { - peerActions.append(ParticipantRevealAction(type: .neutral, title: themeAndStrings.1.GroupInfo_ActionPromote, action: .promote)) + peerActions.append(ParticipantRevealAction(type: .neutral, title: presentationData.strings.GroupInfo_ActionPromote, action: .promote)) } if canRestrict { - peerActions.append(ParticipantRevealAction(type: .warning, title: themeAndStrings.1.GroupInfo_ActionRestrict, action: .restrict)) - peerActions.append(ParticipantRevealAction(type: .destructive, title: themeAndStrings.1.Common_Delete, action: .remove)) + peerActions.append(ParticipantRevealAction(type: .warning, title: presentationData.strings.GroupInfo_ActionRestrict, action: .restrict)) + peerActions.append(ParticipantRevealAction(type: .destructive, title: presentationData.strings.Common_Delete, action: .remove)) } }*/ @@ -1006,14 +1011,14 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod case .searchAdmins: switch participant.participant { case .creator: - label = themeAndStrings.1.Channel_Management_LabelOwner + label = presentationData.strings.Channel_Management_LabelOwner case let .member(_, _, adminInfo, _, _): if let adminInfo = adminInfo { if let peer = participant.peers[adminInfo.promotedBy] { if peer.id == participant.peer.id { - label = themeAndStrings.1.Channel_Management_LabelAdministrator + label = presentationData.strings.Channel_Management_LabelAdministrator } else { - label = themeAndStrings.1.Channel_Management_PromotedBy(peer.displayTitle(strings: themeAndStrings.1, displayOrder: themeAndStrings.3)).0 + label = presentationData.strings.Channel_Management_PromotedBy(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).0 } } } @@ -1028,7 +1033,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod if !exceptionsString.isEmpty { exceptionsString.append(", ") } - exceptionsString.append(compactStringForGroupPermission(strings: themeAndStrings.1, right: rights)) + exceptionsString.append(compactStringForGroupPermission(strings: presentationData.strings, right: rights)) } } label = exceptionsString @@ -1040,7 +1045,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod switch participant.participant { case let .member(_, _, _, banInfo, _): if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] { - label = themeAndStrings.1.Channel_Management_RemovedBy(peer.displayTitle(strings: themeAndStrings.1, displayOrder: themeAndStrings.3)).0 + label = presentationData.strings.Channel_Management_RemovedBy(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).0 } default: break @@ -1048,7 +1053,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod default: break } - entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: peerActions, revealed: state.revealedPeerId == RevealedPeerId(peerId: participant.peer.id, section: section), enabled: enabled), section: section, dateTimeFormat: themeAndStrings.4)) + entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: peerActions, revealed: state.revealedPeerId == RevealedPeerId(peerId: participant.peer.id, section: section), enabled: enabled), section: section, dateTimeFormat: presentationData.dateTimeFormat)) index += 1 } } @@ -1072,12 +1077,12 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod var enabled = true if case .banAndPromoteActions = mode { if case .creator = participant.participant { - label = themeAndStrings.1.Channel_Management_LabelOwner + label = presentationData.strings.Channel_Management_LabelOwner enabled = false } } - entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: [], revealed: false, enabled: enabled), section: section, dateTimeFormat: themeAndStrings.4, addIcon: addIcon)) + entries.append(ChannelMembersSearchEntry(index: index, content: .participant(participant: participant, label: label, revealActions: [], revealed: false, enabled: enabled), section: section, dateTimeFormat: presentationData.dateTimeFormat, addIcon: addIcon)) index += 1 } } @@ -1086,7 +1091,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod let peer = foundPeer.peer if !existingPeerIds.contains(peer.id) && peer is TelegramUser { existingPeerIds.insert(peer.id) - entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: themeAndStrings.4)) + entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: presentationData.dateTimeFormat)) index += 1 } } @@ -1095,7 +1100,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod let peer = foundPeer.peer if !existingPeerIds.contains(peer.id) && peer is TelegramUser { existingPeerIds.insert(peer.id) - entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: themeAndStrings.4)) + entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: presentationData.dateTimeFormat)) index += 1 } } @@ -1110,29 +1115,29 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod let previousSearchItems = Atomic<[ChannelMembersSearchEntry]?>(value: nil) let previousEmptyQueryItems = Atomic<[ChannelMembersSearchEntry]?>(value: nil) - self.emptyQueryDisposable.set((combineLatest(emptyQueryItems, self.themeAndStringsPromise.get()) - |> deliverOnMainQueue).start(next: { [weak self] entries, themeAndStrings in + self.emptyQueryDisposable.set((combineLatest(emptyQueryItems, self.presentationDataPromise.get()) + |> deliverOnMainQueue).start(next: { [weak self] entries, presentationData in if let strongSelf = self { let previousEntries = previousEmptyQueryItems.swap(entries) let firstTime = previousEntries == nil - let transition = channelMembersSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, account: context.account, theme: themeAndStrings.0, strings: themeAndStrings.1, nameSortOrder: themeAndStrings.2, nameDisplayOrder: themeAndStrings.3, interaction: interaction) + let transition = channelMembersSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, context: context, presentationData: presentationData, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, interaction: interaction) strongSelf.enqueueEmptyQueryTransition(transition, firstTime: firstTime) if entries == nil { strongSelf.emptyQueryListNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) } else { - strongSelf.emptyQueryListNode.backgroundColor = themeAndStrings.0.chatList.backgroundColor + strongSelf.emptyQueryListNode.backgroundColor = presentationData.theme.chatList.backgroundColor } } })) - self.searchDisposable.set((combineLatest(foundItems, self.themeAndStringsPromise.get()) - |> deliverOnMainQueue).start(next: { [weak self] entries, themeAndStrings in + self.searchDisposable.set((combineLatest(foundItems, self.presentationDataPromise.get()) + |> deliverOnMainQueue).start(next: { [weak self] entries, presentationData in if let strongSelf = self { let previousEntries = previousSearchItems.swap(entries) updateActivity(false) let firstTime = previousEntries == nil - let transition = channelMembersSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, account: context.account, theme: themeAndStrings.0, strings: themeAndStrings.1, nameSortOrder: themeAndStrings.2, nameDisplayOrder: themeAndStrings.3, interaction: interaction) + let transition = channelMembersSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, context: context, presentationData: presentationData, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, interaction: interaction) strongSelf.enqueueTransition(transition, firstTime: firstTime) } })) @@ -1165,7 +1170,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod self.removeMemberDisposable.dispose() } - override func didLoad() { + override public func didLoad() { super.didLoad() } @@ -1174,7 +1179,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod self.listNode.backgroundColor = theme.chatList.backgroundColor } - override func searchTextUpdated(text: String) { + override public func searchTextUpdated(text: String) { if text.isEmpty { self.searchQuery.set(.single(nil)) } else { @@ -1239,30 +1244,10 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod } } - override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: nil) - } + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight @@ -1270,10 +1255,10 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod insets.right += layout.safeInsets.right self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.emptyQueryListNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.emptyQueryListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.emptyQueryListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if !hasValidLayout { hasValidLayout = true @@ -1283,7 +1268,7 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod } } - override func scrollToTop() { + override public func scrollToTop() { if self.listNode.isHidden { self.emptyQueryListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } else { diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchController.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchController.swift index a9350d7aba..4125fbe79c 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchController.swift @@ -14,7 +14,7 @@ enum ChannelMembersSearchControllerMode { case ban } -enum ChannelMembersSearchFilter { +public enum ChannelMembersSearchFilter { case exclude([PeerId]) case disable([PeerId]) } diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift index 8d5458953d..aae559a159 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift @@ -14,6 +14,7 @@ import TemporaryCachedPeerDataManager import SearchBarNode import ContactsPeerItem import SearchUI +import ItemListUI private final class ChannelMembersSearchInteraction { let openPeer: (Peer, RenderedChannelParticipant?) -> Void @@ -59,7 +60,7 @@ private enum ChannelMembersSearchEntry: Comparable, Identifiable { } } - func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: ChannelMembersSearchInteraction) -> ListViewItem { + func item(context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: ChannelMembersSearchInteraction) -> ListViewItem { switch self { case let .peer(_, participant, editing, label, enabled): let status: ContactsPeerItemStatus @@ -68,7 +69,7 @@ private enum ChannelMembersSearchEntry: Comparable, Identifiable { } else { status = .none } - return ContactsPeerItem(theme: theme, strings: strings, sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, account: account, peerMode: .peer, peer: .peer(peer: participant.peer, chatPeer: nil), status: status, enabled: enabled, selection: .none, editing: editing, index: nil, header: nil, action: { _ in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: participant.peer, chatPeer: nil), status: status, enabled: enabled, selection: .none, editing: editing, index: nil, header: nil, action: { _ in interaction.openPeer(participant.peer, participant) }) } @@ -82,12 +83,12 @@ private struct ChannelMembersSearchTransition { let initial: Bool } -private func preparedTransition(from fromEntries: [ChannelMembersSearchEntry]?, to toEntries: [ChannelMembersSearchEntry], account: Account, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: ChannelMembersSearchInteraction) -> ChannelMembersSearchTransition { +private func preparedTransition(from fromEntries: [ChannelMembersSearchEntry]?, to toEntries: [ChannelMembersSearchEntry], context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: ChannelMembersSearchInteraction) -> ChannelMembersSearchTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries ?? [], rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) } return ChannelMembersSearchTransition(deletions: deletions, insertions: insertions, updates: updates, initial: fromEntries == nil) } @@ -173,6 +174,9 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { guard let peer = peerView.peers[participant.peerId] else { continue } + if peer.isDeleted { + continue + } var label: String? var enabled = true switch mode { @@ -229,7 +233,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { } let previous = previousEntries.swap(entries) - strongSelf.enqueueTransition(preparedTransition(from: previous, to: entries, account: context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, interaction: interaction)) + strongSelf.enqueueTransition(preparedTransition(from: previous, to: entries, context: context, presentationData: strongSelf.presentationData, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, interaction: interaction)) }) disposableAndLoadMoreControl = (disposable, nil) } else { @@ -241,6 +245,10 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { var index = 0 for participant in state.list { + if participant.peer.isDeleted { + continue + } + var label: String? var enabled = true switch mode { @@ -283,7 +291,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { let previous = previousEntries.swap(entries) - strongSelf.enqueueTransition(preparedTransition(from: previous, to: entries, account: context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, interaction: interaction)) + strongSelf.enqueueTransition(preparedTransition(from: previous, to: entries, context: context, presentationData: strongSelf.presentationData, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, interaction: interaction)) }) } self.disposable = disposableAndLoadMoreControl.0 @@ -320,30 +328,9 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) - - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) diff --git a/submodules/PeerInfoUI/Sources/ChannelOwnershipTransferController.swift b/submodules/PeerInfoUI/Sources/ChannelOwnershipTransferController.swift index 900cfc8abc..0d1e8e5da7 100644 --- a/submodules/PeerInfoUI/Sources/ChannelOwnershipTransferController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelOwnershipTransferController.swift @@ -413,9 +413,11 @@ private func commitChannelOwnershipTransferController(context: AccountContext, p var dismissImpl: (() -> Void)? var proceedImpl: (() -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? + let disposable = MetaDisposable() - let contentNode = ChannelOwnershipTransferAlertContentNode(theme: AlertControllerTheme(presentationTheme: presentationData.theme), ptheme: presentationData.theme, strings: presentationData.strings, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + let contentNode = ChannelOwnershipTransferAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { dismissImpl?() }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: { proceedImpl?() @@ -425,9 +427,9 @@ private func commitChannelOwnershipTransferController(context: AccountContext, p proceedImpl?() } - let controller = AlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), contentNode: contentNode) + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) let presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in - controller?.theme = AlertControllerTheme(presentationTheme: presentationData.theme) + controller?.theme = AlertControllerTheme(presentationData: presentationData) contentNode?.inputFieldNode.updateTheme(presentationData.theme) }) controller.dismissed = { @@ -453,7 +455,14 @@ private func commitChannelOwnershipTransferController(context: AccountContext, p } else if let peer = peer as? TelegramGroup { signal = convertGroupToSupergroup(account: context.account, peerId: peer.id) |> map(Optional.init) - |> mapError { _ in ChannelOwnershipTransferError.generic } + |> mapError { error -> ChannelOwnershipTransferError in + switch error { + case .tooManyChannels: + return .tooMuchJoined + default: + return .generic + } + } |> deliverOnMainQueue |> mapToSignal { upgradedPeerId -> Signal in guard let upgradedPeerId = upgradedPeerId else { @@ -479,6 +488,9 @@ private func commitChannelOwnershipTransferController(context: AccountContext, p var errorTextAndActions: (String, [TextAlertAction])? switch error { + case .tooMuchJoined: + pushControllerImpl?(oldChannelsController(context: context, intent: .upgrade)) + return case .invalidPassword: contentNode?.animateError() case .limitExceeded: @@ -502,12 +514,17 @@ private func commitChannelOwnershipTransferController(context: AccountContext, p } })) } + + pushControllerImpl = { [weak controller] c in + controller?.push(c) + } + return controller } private func confirmChannelOwnershipTransferController(context: AccountContext, peer: Peer, member: TelegramUser, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (PeerId?) -> Void) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let theme = AlertControllerTheme(presentationTheme: presentationData.theme) + let theme = AlertControllerTheme(presentationData: presentationData) var isGroup = true if let channel = peer as? TelegramChannel, case .broadcast = channel.info { @@ -538,9 +555,9 @@ private func confirmChannelOwnershipTransferController(context: AccountContext, func channelOwnershipTransferController(context: AccountContext, peer: Peer, member: TelegramUser, initialError: ChannelOwnershipTransferError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (PeerId?) -> Void) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let theme = AlertControllerTheme(presentationTheme: presentationData.theme) + let theme = AlertControllerTheme(presentationData: presentationData) - var title: NSAttributedString? = NSAttributedString(string: presentationData.strings.OwnershipTransfer_SecurityCheck, font: Font.medium(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + var title: NSAttributedString? = NSAttributedString(string: presentationData.strings.OwnershipTransfer_SecurityCheck, font: Font.medium(presentationData.listsFontSize.itemListBaseFontSize), textColor: theme.primaryColor, paragraphAlignment: .center) var text = presentationData.strings.OwnershipTransfer_SecurityRequirements var isGroup = true diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index 00524a94b3..f1813127e9 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -19,7 +19,7 @@ import TelegramPermissionsUI import ItemListPeerActionItem private final class ChannelPermissionsControllerArguments { - let account: Account + let context: AccountContext let updatePermission: (TelegramChatBannedRightsFlags, Bool) -> Void let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void @@ -31,8 +31,8 @@ private final class ChannelPermissionsControllerArguments { let presentRestrictedPermissionAlert: (TelegramChatBannedRightsFlags) -> Void let updateSlowmode: (Int32) -> Void - init(account: Account, updatePermission: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (ChannelParticipant) -> Void, openPeerInfo: @escaping (Peer) -> Void, openKicked: @escaping () -> Void, presentRestrictedPermissionAlert: @escaping (TelegramChatBannedRightsFlags) -> Void, updateSlowmode: @escaping (Int32) -> Void) { - self.account = account + init(context: AccountContext, updatePermission: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (ChannelParticipant) -> Void, openPeerInfo: @escaping (Peer) -> Void, openKicked: @escaping () -> Void, presentRestrictedPermissionAlert: @escaping (TelegramChatBannedRightsFlags) -> Void, updateSlowmode: @escaping (Int32) -> Void) { + self.context = context self.updatePermission = updatePermission self.addPeer = addPeer self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions @@ -216,13 +216,13 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry { } } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ChannelPermissionsControllerArguments switch self { case let .permissionsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .permission(theme, _, title, value, rights, enabled): - return ItemListSwitchItem(theme: theme, title: title, value: value, type: .icon, enableInteractiveChanges: enabled != nil, enabled: enabled ?? true, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, type: .icon, enableInteractiveChanges: enabled != nil, enabled: enabled ?? true, sectionId: self.section, style: .blocks, updated: { value in if let _ = enabled { arguments.updatePermission(rights, value) } else { @@ -232,21 +232,21 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry { arguments.presentRestrictedPermissionAlert(rights) }) case let .slowmodeHeader(theme, value): - return ItemListSectionHeaderItem(theme: theme, text: value, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: value, sectionId: self.section) case let .slowmode(theme, strings, value): return ChatSlowmodeItem(theme: theme, strings: strings, value: value, enabled: true, sectionId: self.section, updated: { value in arguments.updateSlowmode(value) }) case let .slowmodeInfo(theme, value): - return ItemListTextItem(theme: theme, text: .plain(value), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(value), sectionId: self.section) case let .kicked(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openKicked() }) case let .exceptionsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .add(theme, text): - return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.addPersonIcon(theme), title: text, sectionId: self.section, editing: false, action: { + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addPersonIcon(theme), title: text, sectionId: self.section, editing: false, action: { arguments.addPeer() }) case let .peerItem(theme, strings, dateTimeFormat, nameDisplayOrder, _, participant, editing, enabled, canOpen, defaultBannedRights): @@ -270,7 +270,7 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry { default: break } - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: participant.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: canOpen ? { + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: participant.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: canOpen ? { arguments.openPeer(participant.participant) } : { arguments.openPeerInfo(participant.peer) @@ -335,7 +335,7 @@ func compactStringForGroupPermission(strings: PresentationStrings, right: Telegr } } -let allGroupPermissionList: [(TelegramChatBannedRightsFlags, TelegramChannelPermission)] = [ +public let allGroupPermissionList: [(TelegramChatBannedRightsFlags, TelegramChannelPermission)] = [ (.banSendMessages, .sendMessages), (.banSendMedia, .sendMessages), (.banSendGifs, .sendMessages), @@ -472,6 +472,7 @@ public func channelPermissionsController(context: AccountContext, peerId origina var presentControllerImpl: ((ViewController, Any?) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? var dismissInputImpl: (() -> Void)? + var resetSlowmodeVisualValueImpl: (() -> Void)? let actionsDisposable = DisposableSet() @@ -524,7 +525,7 @@ public func channelPermissionsController(context: AccountContext, peerId origina var upgradedToSupergroupImpl: ((PeerId, @escaping () -> Void) -> Void)? - let arguments = ChannelPermissionsControllerArguments(account: context.account, updatePermission: { rights, value in + let arguments = ChannelPermissionsControllerArguments(context: context, updatePermission: { rights, value in let _ = (peerView.get() |> take(1) |> deliverOnMainQueue).start(next: { view in @@ -665,7 +666,7 @@ public func channelPermissionsController(context: AccountContext, peerId origina }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) }, openPeerInfo: { peer in - if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { pushControllerImpl?(controller) } }, openKicked: { @@ -728,17 +729,23 @@ public func channelPermissionsController(context: AccountContext, peerId origina presentControllerImpl?(progress, nil) let signal = convertGroupToSupergroup(account: context.account, peerId: view.peerId) - |> mapError { _ -> UpdateChannelSlowModeError in - return .generic - } - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) + |> mapError { error -> UpdateChannelSlowModeError in + switch error { + case .tooManyChannels: + Queue.mainQueue().async { + updateState { state in + var state = state + state.modifiedSlowmodeTimeout = nil + return state + } + resetSlowmodeVisualValueImpl?() + } + return .tooManyChannels + default: + return .generic + } } |> mapToSignal { upgradedPeerId -> Signal in - guard let upgradedPeerId = upgradedPeerId else { - return .single(nil) - } return updateChannelSlowModeInteractively(postbox: context.account.postbox, network: context.account.network, accountStateManager: context.account.stateManager, peerId: upgradedPeerId, timeout: modifiedSlowmodeTimeout == 0 ? nil : value) |> mapToSignal { _ -> Signal in return .complete() @@ -752,8 +759,15 @@ public func channelPermissionsController(context: AccountContext, peerId origina upgradedToSupergroupImpl?(peerId, {}) } progress?.dismiss() - }, error: { [weak progress] _ in + }, error: { [weak progress] error in progress?.dismiss() + + switch error { + case .tooManyChannels: + pushControllerImpl?(oldChannelsController(context: context, intent: .upgrade)) + default: + break + } })) } }) @@ -828,8 +842,8 @@ public func channelPermissionsController(context: AccountContext, peerId origina }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.GroupInfo_Permissions_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(entries: channelPermissionsControllerEntries(presentationData: presentationData, view: view, state: state, participants: participants), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: previous != nil && participants != nil && previous!.count >= participants!.count) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.GroupInfo_Permissions_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelPermissionsControllerEntries(presentationData: presentationData, view: view, state: state, participants: participants), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: previous != nil && participants != nil && previous!.count >= participants!.count) return (controllerState, (listState, arguments)) } @@ -853,14 +867,22 @@ public func channelPermissionsController(context: AccountContext, peerId origina dismissInputImpl = { [weak controller] in controller?.view.endEditing(true) } + resetSlowmodeVisualValueImpl = { [weak controller] in + guard let controller = controller else { + return + } + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatSlowmodeItemNode { + itemNode.forceSetValue(0) + } + } + } upgradedToSupergroupImpl = { [weak controller] upgradedPeerId, f in guard let controller = controller, let navigationController = controller.navigationController as? NavigationController else { return } sourcePeerId.set(.single((upgradedPeerId, true))) - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(upgradedPeerId), keepStack: .never, animated: false, completion: { - navigationController.pushViewController(controller, animated: false) - })) + rebuildControllerStackAfterSupergroupUpgrade(controller: controller, navigationController: navigationController) } controller.visibleBottomContentOffsetChanged = { offset in diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index 1cda2e9038..018c87d2d6 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -16,9 +16,10 @@ import AlertUI import PresentationDataUtils import TelegramNotices import ItemListPeerItem +import AccountContext private final class ChannelVisibilityControllerArguments { - let account: Account + let context: AccountContext let updateCurrentType: (CurrentChannelType) -> Void let updatePublicLinkText: (String?, String) -> Void @@ -30,8 +31,8 @@ private final class ChannelVisibilityControllerArguments { let revokePrivateLink: () -> Void let sharePrivateLink: () -> Void - init(account: Account, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, scrollToPublicLinkText: @escaping () -> Void, displayPrivateLinkMenu: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, revokePeerId: @escaping (PeerId) -> Void, copyPrivateLink: @escaping () -> Void, revokePrivateLink: @escaping () -> Void, sharePrivateLink: @escaping () -> Void) { - self.account = account + init(context: AccountContext, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, scrollToPublicLinkText: @escaping () -> Void, displayPrivateLinkMenu: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, revokePeerId: @escaping (PeerId) -> Void, copyPrivateLink: @escaping () -> Void, revokePrivateLink: @escaping () -> Void, sharePrivateLink: @escaping () -> Void) { + self.context = context self.updateCurrentType = updateCurrentType self.updatePublicLinkText = updatePublicLinkText self.scrollToPublicLinkText = scrollToPublicLinkText @@ -263,35 +264,35 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ChannelVisibilityControllerArguments switch self { case let .typeHeader(theme, title): - return ItemListSectionHeaderItem(theme: theme, text: title, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .typePublic(theme, text, selected): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateCurrentType(.publicChannel) }) case let .typePrivate(theme, text, selected): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateCurrentType(.privateChannel) }) case let .typeInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .publicLinkHeader(theme, title): - return ItemListSectionHeaderItem(theme: theme, text: title, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .publicLinkAvailability(theme, text, value): let attr = NSMutableAttributedString(string: text, textColor: value ? theme.list.freeTextColor : theme.list.freeTextErrorColor) attr.addAttribute(.font, value: Font.regular(13), range: NSMakeRange(0, attr.length)) - return ItemListActivityTextItem(displayActivity: value, theme: theme, text: attr, sectionId: self.section) + return ItemListActivityTextItem(displayActivity: value, presentationData: presentationData, text: attr, sectionId: self.section) case let .privateLink(theme, text, value): - return ItemListActionItem(theme: theme, title: text, kind: value != nil ? .neutral : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: value != nil ? .neutral : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { if let value = value { arguments.displayPrivateLinkMenu(value) } }, tag: ChannelVisibilityEntryTag.privateLink) case let .editablePublicLink(theme, strings, placeholder, currentText): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: currentText, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), clearType: .always, tag: ChannelVisibilityEntryTag.publicLink, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: currentText, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), clearType: .always, tag: ChannelVisibilityEntryTag.publicLink, sectionId: self.section, textUpdated: { updatedText in arguments.updatePublicLinkText(currentText, updatedText) }, updatedFocus: { focus in if focus { @@ -300,21 +301,21 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { }, action: { }) case let .privateLinkInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .privateLinkCopy(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.copyPrivateLink() }) case let .privateLinkRevoke(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.revokePrivateLink() }) case let .privateLinkShare(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.sharePrivateLink() }) case let .publicLinkInfo(theme, text): - return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .publicLinkStatus(theme, text, status): var displayActivity = false let color: UIColor @@ -334,15 +335,15 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { color = theme.list.freeTextColor displayActivity = true } - return ItemListActivityTextItem(displayActivity: displayActivity, theme: theme, text: NSAttributedString(string: text, textColor: color), sectionId: self.section) + return ItemListActivityTextItem(displayActivity: displayActivity, presentationData: presentationData, text: NSAttributedString(string: text, textColor: color), sectionId: self.section) case let .existingLinksInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .existingLinkPeerItem(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer, editing, enabled): var label = "" if let addressName = peer.addressName { label = "t.me/" + addressName } - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: nil, text: .text(label), label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .text(label), label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.revokePeerId(peerId) @@ -464,8 +465,10 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa selectedType = .publicChannel } else if let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil { selectedType = .publicChannel - } else { + } else if case .initialSetup = mode { selectedType = .publicChannel + } else { + selectedType = .privateChannel } } } @@ -651,7 +654,12 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa entries.append(.typePublic(presentationData.theme, presentationData.strings.Channel_Setup_TypePublic, selectedType == .publicChannel)) entries.append(.typePrivate(presentationData.theme, presentationData.strings.Channel_Setup_TypePrivate, selectedType == .privateChannel)) - entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePublicHelp)) + switch selectedType { + case .publicChannel: + entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePublicHelp)) + case .privateChannel: + entries.append(.typeInfo(presentationData.theme, presentationData.strings.Group_Setup_TypePrivateHelp)) + } switch selectedType { case .publicChannel: @@ -805,7 +813,7 @@ public enum ChannelVisibilityControllerMode { case privateLink } -public func channelVisibilityController(context: AccountContext, peerId: PeerId, mode: ChannelVisibilityControllerMode, upgradedToSupergroup: @escaping (PeerId, @escaping () -> Void) -> Void) -> ViewController { +public func channelVisibilityController(context: AccountContext, peerId: PeerId, mode: ChannelVisibilityControllerMode, upgradedToSupergroup: @escaping (PeerId, @escaping () -> Void) -> Void, onDismissRemoveController: ViewController? = nil) -> ViewController { let statePromise = ValuePromise(ChannelVisibilityControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: ChannelVisibilityControllerState()) let updateState: ((ChannelVisibilityControllerState) -> ChannelVisibilityControllerState) -> Void = { f in @@ -828,6 +836,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId, var displayPrivateLinkMenuImpl: ((String) -> Void)? var scrollToPublicLinkTextImpl: (() -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? var clearHighlightImpl: (() -> Void)? let actionsDisposable = DisposableSet() @@ -848,7 +857,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId, return ensuredExistingPeerExportedInvitation(account: context.account, peerId: peerId) }).start()) - let arguments = ChannelVisibilityControllerArguments(account: context.account, updateCurrentType: { type in + let arguments = ChannelVisibilityControllerArguments(context: context, updateCurrentType: { type in updateState { state in return state.withUpdatedSelectedType(type) } @@ -922,7 +931,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId, }) }, revokePrivateLink: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -1093,14 +1102,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId, _ = ApplicationSpecificNotice.markAsSeenSetPublicChannelLink(accountManager: context.sharedContext.accountManager).start() let signal = convertGroupToSupergroup(account: context.account, peerId: peerId) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { upgradedPeerId -> Signal in - guard let upgradedPeerId = upgradedPeerId else { - return .single(nil) - } + |> mapToSignal { upgradedPeerId -> Signal in return updateAddressName(account: context.account, domain: .peer(upgradedPeerId), name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue) |> `catch` { _ -> Signal in return .complete() @@ -1109,6 +1111,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId, return .complete() } |> then(.single(upgradedPeerId)) + |> castError(ConvertGroupToSupergroupError.self) } |> deliverOnMainQueue @@ -1121,11 +1124,16 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId, } else { dismissImpl?() } - }, error: { _ in + }, error: { error in updateState { state in return state.withUpdatedUpdatingAddressName(false) } - presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + switch error { + case .tooManyChannels: + pushControllerImpl?(oldChannelsController(context: context, intent: .upgrade)) + default: + presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + } })) } @@ -1214,8 +1222,8 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId, focusItemTag = ChannelVisibilityEntryTag.publicLink } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: entries, style: .blocks, focusItemTag: focusItemTag, crossfadeState: crossfade, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, focusItemTag: focusItemTag, crossfadeState: crossfade, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { @@ -1223,9 +1231,22 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId, } let controller = ItemListController(context: context, state: signal) - dismissImpl = { [weak controller] in - controller?.view.endEditing(true) - controller?.dismiss() + dismissImpl = { [weak controller, weak onDismissRemoveController] in + guard let controller = controller else { + return + } + controller.view.endEditing(true) + if let onDismissRemoveController = onDismissRemoveController, let navigationController = controller.navigationController { + navigationController.setViewControllers(navigationController.viewControllers.filter { c in + if c === controller || c === onDismissRemoveController { + return false + } else { + return true + } + }, animated: true) + } else { + controller.dismiss() + } } dismissInputImpl = { [weak controller] in controller?.view.endEditing(true) @@ -1327,6 +1348,9 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId, presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) } + pushControllerImpl = { [weak controller] c in + controller?.push(c) + } clearHighlightImpl = { [weak controller] in controller?.clearItemNodesHighlight(animated: true) } diff --git a/submodules/PeerInfoUI/Sources/ChatSlowmodeItem.swift b/submodules/PeerInfoUI/Sources/ChatSlowmodeItem.swift index e83f370365..61066bd280 100644 --- a/submodules/PeerInfoUI/Sources/ChatSlowmodeItem.swift +++ b/submodules/PeerInfoUI/Sources/ChatSlowmodeItem.swift @@ -105,6 +105,12 @@ class ChatSlowmodeItemNode: ListViewItemNode { self.textNodes.forEach(self.addSubnode) } + func forceSetValue(_ value: Int32) { + if let sliderView = self.sliderView { + sliderView.value = CGFloat(value) + } + } + func updateSliderView() { if let sliderView = self.sliderView, let item = self.item { sliderView.maximumValue = CGFloat(allowedValues.count - 1) diff --git a/submodules/PeerInfoUI/Sources/ConvertToSupergroupController.swift b/submodules/PeerInfoUI/Sources/ConvertToSupergroupController.swift index 099dff3b95..494e9537c2 100644 --- a/submodules/PeerInfoUI/Sources/ConvertToSupergroupController.swift +++ b/submodules/PeerInfoUI/Sources/ConvertToSupergroupController.swift @@ -77,17 +77,17 @@ private enum ConvertToSupergroupEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ConvertToSupergroupArguments switch self { case let .info(theme, text): - return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .action(theme, title): - return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.convert() }) case let .actionInfo(theme, text): - return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) } } } @@ -166,8 +166,8 @@ public func convertToSupergroupController(context: AccountContext, peerId: PeerI rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ConvertToSupergroup_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: convertToSupergroupEntries(presentationData: presentationData), style: .blocks) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ConvertToSupergroup_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: convertToSupergroupEntries(presentationData: presentationData), style: .blocks) return (controllerState, (listState, arguments)) } diff --git a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift index 7225f52468..4e2d78a55b 100644 --- a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +++ b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift @@ -15,6 +15,7 @@ import AlertUI import PresentationDataUtils import PhotoResources import MediaResources +import LocationResources import ItemListAvatarAndNameInfoItem import Geocoding import ItemListAddressItem @@ -29,7 +30,7 @@ private enum DeviceContactInfoAction { } private final class DeviceContactInfoControllerArguments { - let account: Account + let context: AccountContext let isPlain: Bool let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void let updatePhone: (Int64, String) -> Void @@ -45,8 +46,8 @@ private final class DeviceContactInfoControllerArguments { let displayCopyContextMenu: (DeviceContactInfoEntryTag, String) -> Void let updateShareViaException: (Bool) -> Void - init(account: Account, isPlain: Bool, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updatePhone: @escaping (Int64, String) -> Void, updatePhoneLabel: @escaping (Int64, String) -> Void, deletePhone: @escaping (Int64) -> Void, setPhoneIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, addPhoneNumber: @escaping () -> Void, performAction: @escaping (DeviceContactInfoAction) -> Void, toggleSelection: @escaping (DeviceContactInfoDataId) -> Void, callPhone: @escaping (String) -> Void, openUrl: @escaping (String) -> Void, openAddress: @escaping (DeviceContactAddressData) -> Void, displayCopyContextMenu: @escaping (DeviceContactInfoEntryTag, String) -> Void, updateShareViaException: @escaping (Bool) -> Void) { - self.account = account + init(context: AccountContext, isPlain: Bool, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updatePhone: @escaping (Int64, String) -> Void, updatePhoneLabel: @escaping (Int64, String) -> Void, deletePhone: @escaping (Int64) -> Void, setPhoneIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, addPhoneNumber: @escaping () -> Void, performAction: @escaping (DeviceContactInfoAction) -> Void, toggleSelection: @escaping (DeviceContactInfoDataId) -> Void, callPhone: @escaping (String) -> Void, openUrl: @escaping (String) -> Void, openAddress: @escaping (DeviceContactAddressData) -> Void, displayCopyContextMenu: @escaping (DeviceContactInfoEntryTag, String) -> Void, updateShareViaException: @escaping (Bool) -> Void) { + self.context = context self.isPlain = isPlain self.updateEditingName = updateEditingName self.updatePhone = updatePhone @@ -75,6 +76,7 @@ private enum DeviceContactInfoEntryTag: Equatable, ItemListItemTag { case info(Int) case birthday case editingPhone(Int64) + case note func isEqual(to other: ItemListItemTag) -> Bool { return self == (other as? DeviceContactInfoEntryTag) @@ -90,6 +92,7 @@ private enum DeviceContactInfoDataId: Hashable { case birthday case socialProfile(DeviceContactSocialProfileData) case instantMessenger(DeviceContactInstantMessagingProfileData) + case note } private enum DeviceContactInfoConstantEntryId: Hashable { @@ -104,6 +107,7 @@ private enum DeviceContactInfoConstantEntryId: Hashable { case phoneNumberSharingInfo case phoneNumberShareViaException case phoneNumberShareViaExceptionInfo + case note } private enum DeviceContactInfoEntryId: Hashable { @@ -138,6 +142,7 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { case birthday(Int, PresentationTheme, String, Date, String, Bool?) case socialProfile(Int, Int, PresentationTheme, String, DeviceContactSocialProfileData, String, Bool?) case instantMessenger(Int, Int, PresentationTheme, String, DeviceContactInstantMessagingProfileData, String, Bool?) + case note(Int, PresentationTheme, String, String, Bool?) var section: ItemListSectionId { switch self { @@ -192,6 +197,8 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { return .socialProfile(catIndex) case let .instantMessenger(_, catIndex, _, _, _, _, _): return .instantMessenger(catIndex) + case .note: + return .constant(.note) } } @@ -329,6 +336,12 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { } else { return false } + case let .note(lhsIndex, lhsTheme, lhsTitle, lhsText, lhsSelected): + if case let .note(rhsIndex, rhsTheme, rhsTitle, rhsText, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText, lhsSelected == rhsSelected { + return true + } else { + return false + } } } @@ -370,6 +383,8 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { return index case let .instantMessenger(index, _, _, _, _, _, _): return index + case let .note(index, _, _, _, _): + return index } } @@ -377,35 +392,35 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { return lhs.sortIndex < rhs.sortIndex } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! DeviceContactInfoControllerArguments switch self { case let .info(_, theme, strings, dateTimeFormat, peer, state, jobSummary, isPlain): - return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, mode: .contact, peer: peer, presence: nil, label: jobSummary, cachedData: nil, state: state, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks(withTopInset: false, withExtendedBottomInset: true), editingNameUpdated: { editingName in + return ItemListAvatarAndNameInfoItem(accountContext: arguments.context, presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .contact, peer: peer, presence: nil, label: jobSummary, cachedData: nil, state: state, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks(withTopInset: false, withExtendedBottomInset: true), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { }, context: nil, call: nil) case let .sendMessage(_, theme, title): - return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks, action: { arguments.performAction(.sendMessage) }) case let .invite(_, theme, title): - return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks, action: { arguments.performAction(.invite) }) case let .createContact(_, theme, title): - return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks, action: { arguments.performAction(.createContact) }) case let .addToExisting(_, theme, title): - return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks, action: { arguments.performAction(.addToExisting) }) case let .company(_, theme, title, value, selected): - return ItemListTextWithLabelItem(theme: theme, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: { + return ItemListTextWithLabelItem(presentationData: presentationData, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: { }, tag: nil) case let .phoneNumber(_, index, theme, title, label, value, selected, isInteractionEnabled): - return ItemListTextWithLabelItem(theme: theme, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: false, selected: selected, sectionId: self.section, action: isInteractionEnabled ? { + return ItemListTextWithLabelItem(presentationData: presentationData, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: false, selected: selected, sectionId: self.section, action: isInteractionEnabled ? { if selected != nil { arguments.toggleSelection(.phoneNumber(label, value)) } else { @@ -417,15 +432,15 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { } } : nil, tag: DeviceContactInfoEntryTag.info(index)) case let .phoneNumberSharingInfo(_, theme, text): - return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .phoneNumberShareViaException(_, theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks, updated: { value in arguments.updateShareViaException(value) }) case let .phoneNumberShareViaExceptionInfo(_, theme, text): - return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .editingPhoneNumber(_, theme, strings, id, title, label, value, hasActiveRevealControls): - return UserInfoEditingPhoneItem(theme: theme, strings: strings, id: id, label: title, value: value, editing: UserInfoEditingPhoneItemEditing(editable: true, hasActiveRevealControls: hasActiveRevealControls), sectionId: self.section, setPhoneIdWithRevealedOptions: { lhs, rhs in + return UserInfoEditingPhoneItem(presentationData: presentationData, id: id, label: title, value: value, editing: UserInfoEditingPhoneItemEditing(editable: true, hasActiveRevealControls: hasActiveRevealControls), sectionId: self.section, setPhoneIdWithRevealedOptions: { lhs, rhs in arguments.setPhoneIdWithRevealedOptions(lhs, rhs) }, updated: { value in arguments.updatePhone(id, value) @@ -435,11 +450,11 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { arguments.deletePhone(id) }, tag: DeviceContactInfoEntryTag.editingPhone(id)) case let .addPhoneNumber(_, theme, title): - return UserInfoEditingPhoneActionItem(theme: theme, title: title, sectionId: self.section, action: { + return UserInfoEditingPhoneActionItem(presentationData: presentationData, title: title, sectionId: self.section, action: { arguments.addPhoneNumber() }) case let .email(_, index, theme, title, label, value, selected): - return ItemListTextWithLabelItem(theme: theme, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: false, selected: selected, sectionId: self.section, action: { + return ItemListTextWithLabelItem(presentationData: presentationData, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: false, selected: selected, sectionId: self.section, action: { if selected != nil { arguments.toggleSelection(.email(label, value)) } else { @@ -451,7 +466,7 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { } }, tag: DeviceContactInfoEntryTag.info(index)) case let .url(_, index, theme, title, label, value, selected): - return ItemListTextWithLabelItem(theme: theme, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: false, selected: selected, sectionId: self.section, action: { + return ItemListTextWithLabelItem(presentationData: presentationData, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: false, selected: selected, sectionId: self.section, action: { if selected != nil { arguments.toggleSelection(.url(label, value)) } else { @@ -490,7 +505,7 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { } }, tag: DeviceContactInfoEntryTag.info(index)) case let .birthday(_, theme, title, value, text, selected): - return ItemListTextWithLabelItem(theme: theme, label: title, text: text, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: { + return ItemListTextWithLabelItem(presentationData: presentationData, label: title, text: text, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: { if selected != nil { arguments.toggleSelection(.birthday) } else { @@ -519,7 +534,7 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { } }, tag: DeviceContactInfoEntryTag.birthday) case let .socialProfile(_, index, theme, title, value, text, selected): - return ItemListTextWithLabelItem(theme: theme, label: title, text: text, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: { + return ItemListTextWithLabelItem(presentationData: presentationData, label: title, text: text, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: { if selected != nil { arguments.toggleSelection(.socialProfile(value)) } else if value.url.count > 0 { @@ -531,7 +546,7 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { } }, tag: DeviceContactInfoEntryTag.info(index)) case let .instantMessenger(_, index, theme, title, value, text, selected): - return ItemListTextWithLabelItem(theme: theme, label: title, text: text, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: { + return ItemListTextWithLabelItem(presentationData: presentationData, label: title, text: text, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: { if selected != nil { arguments.toggleSelection(.instantMessenger(value)) } @@ -540,6 +555,16 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { arguments.displayCopyContextMenu(.info(index), text) } }, tag: DeviceContactInfoEntryTag.info(index)) + case let .note(_, theme, title, text, selected): + return ItemListTextWithLabelItem(presentationData: presentationData, label: title, text: text, style: arguments.isPlain ? .plain : .blocks, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: { + if selected != nil { + arguments.toggleSelection(.note) + } + }, longTapAction: { + if selected == nil { + arguments.displayCopyContextMenu(.note, text) + } + }, tag: DeviceContactInfoEntryTag.note) } } } @@ -585,8 +610,8 @@ private func filteredContactData(contactData: DeviceContactExtendedData, exclude }) let includeJob = !excludedComponents.contains(.job) let includeBirthday = !excludedComponents.contains(.birthday) - - return DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumbers: phoneNumbers), middleName: contactData.middleName, prefix: contactData.prefix, suffix: contactData.suffix, organization: includeJob ? contactData.organization : "", jobTitle: includeJob ? contactData.jobTitle : "", department: includeJob ? contactData.department : "", emailAddresses: emailAddresses, urls: urls, addresses: addresses, birthdayDate: includeBirthday ? contactData.birthdayDate : nil, socialProfiles: socialProfiles, instantMessagingProfiles: instantMessagingProfiles) + let includeNote = !excludedComponents.contains(.note) + return DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumbers: phoneNumbers), middleName: contactData.middleName, prefix: contactData.prefix, suffix: contactData.suffix, organization: includeJob ? contactData.organization : "", jobTitle: includeJob ? contactData.jobTitle : "", department: includeJob ? contactData.department : "", emailAddresses: emailAddresses, urls: urls, addresses: addresses, birthdayDate: includeBirthday ? contactData.birthdayDate : nil, socialProfiles: socialProfiles, instantMessagingProfiles: instantMessagingProfiles, note: includeNote ? contactData.note : "") } private func deviceContactInfoEntries(account: Account, presentationData: PresentationData, peer: Peer?, isShare: Bool, shareViaException: Bool, contactData: DeviceContactExtendedData, isContact: Bool, state: DeviceContactInfoState, selecting: Bool, editingPhoneNumbers: Bool) -> [DeviceContactInfoEntry] { @@ -714,14 +739,13 @@ private func deviceContactInfoEntries(account: Account, presentationData: Presen if let birthday = contactData.birthdayDate { let dateText: String let calendar = Calendar(identifier: .gregorian) - var components = calendar.dateComponents(Set([.era, .year, .month, .day]), from: birthday) - components.hour = 12 + let components = calendar.dateComponents(Set([.era, .year, .month, .day]), from: birthday) if let year = components.year, year > 1 { - dateText = stringForDate(timestamp: Int32(birthday.timeIntervalSince1970), strings: presentationData.strings) + dateText = stringForDate(date: birthday, timeZone: TimeZone.current, strings: presentationData.strings) } else { - dateText = stringForDateWithoutYear(date: birthday, strings: presentationData.strings) + dateText = stringForDateWithoutYear(date: birthday, timeZone: TimeZone.current, strings: presentationData.strings) } - entries.append(.birthday(entries.count, presentationData.theme, "birthday", birthday, dateText, selecting ? !state.excludedComponents.contains(.birthday) : nil)) + entries.append(.birthday(entries.count, presentationData.theme, presentationData.strings.ContactInfo_BirthdayLabel, birthday, dateText, selecting ? !state.excludedComponents.contains(.birthday) : nil)) } var socialProfileIndex = 0 @@ -753,6 +777,10 @@ private func deviceContactInfoEntries(account: Account, presentationData: Presen instantMessagingProfileIndex += 1 } + if !contactData.note.isEmpty { + entries.append(.note(entries.count, presentationData.theme, presentationData.strings.ContactInfo_Note, contactData.note, selecting ? !state.excludedComponents.contains(.note) : nil)) + } + return entries } @@ -841,7 +869,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device |> deliverOnMainQueue).start(next: { user in if let user = user, let phone = user.phone, formatPhoneNumber(phone) == formatPhoneNumber(number) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -895,7 +923,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device shareViaException = shareViaExceptionValue } - let arguments = DeviceContactInfoControllerArguments(account: context.account, isPlain: !isShare, updateEditingName: { editingName in + let arguments = DeviceContactInfoControllerArguments(context: context, isPlain: !isShare, updateEditingName: { editingName in updateState { state in var state = state if let _ = state.editingState { @@ -964,7 +992,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device inviteAction(subject.contactData.basicData.phoneNumbers[0].value) } else { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -1071,7 +1099,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device urls.append(appProfile) } } - composedContactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: firstName, lastName: lastName, phoneNumbers: filteredPhoneNumbers), middleName: filteredData.middleName, prefix: filteredData.prefix, suffix: filteredData.suffix, organization: filteredData.organization, jobTitle: filteredData.jobTitle, department: filteredData.department, emailAddresses: filteredData.emailAddresses, urls: urls, addresses: filteredData.addresses, birthdayDate: filteredData.birthdayDate, socialProfiles: filteredData.socialProfiles, instantMessagingProfiles: filteredData.instantMessagingProfiles) + composedContactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: firstName, lastName: lastName, phoneNumbers: filteredPhoneNumbers), middleName: filteredData.middleName, prefix: filteredData.prefix, suffix: filteredData.suffix, organization: filteredData.organization, jobTitle: filteredData.jobTitle, department: filteredData.department, emailAddresses: filteredData.emailAddresses, urls: urls, addresses: filteredData.addresses, birthdayDate: filteredData.birthdayDate, socialProfiles: filteredData.socialProfiles, instantMessagingProfiles: filteredData.instantMessagingProfiles, note: filteredData.note) } rightNavigationButton = ItemListNavigationButton(content: .text(isShare ? presentationData.strings.Common_Done : presentationData.strings.Compose_Create), style: .bold, enabled: (isShare || !filteredPhoneNumbers.isEmpty) && composedContactData != nil, action: { if let composedContactData = composedContactData { @@ -1186,7 +1214,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device selecting = true } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: nil) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: nil) let editingPhoneIds = Set(state.phoneNumbers.map({ $0.id })) let previousPhoneIds = previousEditingPhoneIds.swap(editingPhoneIds) @@ -1208,7 +1236,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device focusItemTag = DeviceContactInfoEntryTag.editingPhone(insertedPhoneId) } - let listState = ItemListNodeState(entries: deviceContactInfoEntries(account: context.account, presentationData: presentationData, peer: peerAndContactData.0, isShare: isShare, shareViaException: shareViaException, contactData: peerAndContactData.2, isContact: peerAndContactData.1 != nil, state: state, selecting: selecting, editingPhoneNumbers: editingPhones), style: isShare ? .blocks : .plain, focusItemTag: focusItemTag) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: deviceContactInfoEntries(account: context.account, presentationData: presentationData, peer: peerAndContactData.0, isShare: isShare, shareViaException: shareViaException, contactData: peerAndContactData.2, isContact: peerAndContactData.1 != nil, state: state, selecting: selecting, editingPhoneNumbers: editingPhones), style: isShare ? .blocks : .plain, focusItemTag: focusItemTag) return (controllerState, (listState, arguments)) } @@ -1374,7 +1402,7 @@ private func addContactToExisting(context: AccountContext, parentController: Vie func addContactOptionsController(context: AccountContext, peer: Peer?, contactData: DeviceContactExtendedData) -> ActionSheetController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } diff --git a/submodules/PeerInfoUI/Sources/GroupInfoController.swift b/submodules/PeerInfoUI/Sources/GroupInfoController.swift index 0c129226d6..367096905c 100644 --- a/submodules/PeerInfoUI/Sources/GroupInfoController.swift +++ b/submodules/PeerInfoUI/Sources/GroupInfoController.swift @@ -21,6 +21,7 @@ import AlertUI import PresentationDataUtils import MediaResources import PhotoResources +import LocationResources import GalleryUI import LegacyUI import LocationUI @@ -492,27 +493,27 @@ private enum GroupInfoEntry: ItemListNodeEntry { return lhs.sortIndex < rhs.sortIndex } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! GroupInfoArguments switch self { case let .info(theme, strings, dateTimeFormat, peer, cachedData, state, updatingAvatar): - return ItemListAvatarAndNameInfoItem(account: arguments.context.account, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .blocks(withTopInset: false, withExtendedBottomInset: false), editingNameUpdated: { editingName in + return ItemListAvatarAndNameInfoItem(accountContext: arguments.context, presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .blocks(withTopInset: false, withExtendedBottomInset: false), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { arguments.tapAvatarAction() }, context: arguments.avatarAndNameInfoContext, updatingImage: updatingAvatar) case let .setGroupPhoto(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.changeProfilePhoto() }) case let .about(theme, text): - return ItemListMultilineTextItem(theme: theme, text: foldMultipleLineBreaks(text), enabledEntityTypes: [.url, .mention, .hashtag], sectionId: self.section, style: .blocks, longTapAction: { + return ItemListMultilineTextItem(presentationData: presentationData, text: foldMultipleLineBreaks(text), enabledEntityTypes: [.url, .mention, .hashtag], sectionId: self.section, style: .blocks, longTapAction: { arguments.displayAboutContextMenu(text) }, linkItemAction: { action, itemLink in arguments.aboutLinkAction(action, itemLink) }, tag: GroupInfoEntryTag.about) case let .locationHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .location(theme, location): let imageSignal = chatMapSnapshotImage(account: arguments.context.account, resource: MapSnapshotMediaResource(latitude: location.latitude, longitude: location.longitude, width: 90, height: 90)) return ItemListAddressItem(theme: theme, label: "", text: location.address.replacingOccurrences(of: ", ", with: "\n"), imageSignal: imageSignal, selected: nil, sectionId: self.section, style: .blocks, action: { @@ -521,53 +522,53 @@ private enum GroupInfoEntry: ItemListNodeEntry { arguments.displayLocationContextMenu(location.address.replacingOccurrences(of: "\n", with: ", ")) }, tag: GroupInfoEntryTag.location) case let .changeLocation(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.changeLocation() }, clearHighlightAutomatically: false) case let .link(theme, url): - return ItemListActionItem(theme: theme, title: url, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: url, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.displayUsernameShareMenu(url) }, longTapAction: { arguments.displayUsernameContextMenu(url) }, tag: GroupInfoEntryTag.link) case let .notifications(theme, title, text): - return ItemListDisclosureItem(theme: theme, title: title, label: text, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: title, label: text, sectionId: self.section, style: .blocks, action: { arguments.changeNotificationMuteSettings() }) case let .stickerPack(theme, title, value): - return ItemListDisclosureItem(theme: theme, title: title, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: title, label: value, sectionId: self.section, style: .blocks, action: { arguments.openStickerPackSetup() }) case let .preHistory(theme, title, value): - return ItemListDisclosureItem(theme: theme, title: title, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: title, label: value, sectionId: self.section, style: .blocks, action: { arguments.openPreHistory() }) case let .sharedMedia(theme, title): - return ItemListDisclosureItem(theme: theme, title: title, label: "", sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: title, label: "", sectionId: self.section, style: .blocks, action: { arguments.openSharedMedia() }) case let .addMember(theme, title, editing): - return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.addPersonIcon(theme), title: title, sectionId: self.section, editing: editing, action: { + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addPersonIcon(theme), title: title, sectionId: self.section, editing: editing, action: { arguments.addMember() }) case let .groupTypeSetup(theme, title, text): - return ItemListDisclosureItem(theme: theme, title: title, label: text, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: title, label: text, sectionId: self.section, style: .blocks, action: { arguments.openGroupTypeSetup() }) case let .linkedChannelSetup(theme, title, text): - return ItemListDisclosureItem(theme: theme, title: title, label: text, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: title, label: text, sectionId: self.section, style: .blocks, action: { arguments.openLinkedChannelSetup() }) case let .groupDescriptionSetup(theme, placeholder, text): - return ItemListMultilineInputItem(theme: theme, text: text, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: 255, display: true), sectionId: self.section, style: .blocks, textUpdated: { updatedText in + return ItemListMultilineInputItem(presentationData: presentationData, text: text, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: 255, display: true), sectionId: self.section, style: .blocks, textUpdated: { updatedText in arguments.updateEditingDescriptionText(updatedText) }) case let .permissions(theme, title, text): - return ItemListDisclosureItem(theme: theme, icon: PresentationResourcesChat.groupInfoPermissionsIcon(theme), title: title, label: text, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesChat.groupInfoPermissionsIcon(theme), title: title, label: text, sectionId: self.section, style: .blocks, action: { arguments.openPermissions() }) case let .administrators(theme, title, text): - return ItemListDisclosureItem(theme: theme, icon: PresentationResourcesChat.groupInfoAdminsIcon(theme), title: title, label: text, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesChat.groupInfoAdminsIcon(theme), title: title, label: text, sectionId: self.section, style: .blocks, action: { arguments.openAdministrators() }) case let .member(theme, strings, dateTimeFormat, nameDisplayOrder, _, _, peer, participant, presence, memberStatus, editing, actions, enabled, selectable): @@ -597,8 +598,8 @@ private enum GroupInfoEntry: ItemListNodeEntry { } })) } - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.context.account, peer: peer, presence: presence, text: .presence, label: label == nil ? .none : .text(label!, .standard), editing: editing, revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: enabled, selectable: selectable, sectionId: self.section, action: { - if let infoController = arguments.context.sharedContext.makePeerInfoController(context: arguments.context, peer: peer, mode: .generic), selectable { + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer, presence: presence, text: .presence, label: label == nil ? .none : .text(label!, .standard), editing: editing, revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: enabled, selectable: selectable, sectionId: self.section, action: { + if let infoController = arguments.context.sharedContext.makePeerInfoController(context: arguments.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false), selectable { arguments.pushController(infoController) } }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in @@ -607,11 +608,11 @@ private enum GroupInfoEntry: ItemListNodeEntry { arguments.removePeer(peerId) }) case let .expand(theme, title): - return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: { + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: { arguments.expandParticipants() }) case let .leave(theme, title): - return ItemListActionItem(theme: theme, title: title, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: title, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.leave() }) default: @@ -1339,6 +1340,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId: var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? + var replaceControllerImpl: ((ViewController?, ViewController) -> Void)? var endEditingImpl: (() -> Void)? var removePeerChatImpl: ((Peer, Bool) -> Void)? var errorImpl: (() -> Void)? @@ -1754,6 +1756,26 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId: presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(peer.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), nil) }) + updateState { state in + var temporaryParticipants = state.temporaryParticipants + for i in 0 ..< temporaryParticipants.count { + if temporaryParticipants[i].peer.id == memberId { + temporaryParticipants.remove(at: i) + break + } + } + var successfullyAddedParticipantIds = state.successfullyAddedParticipantIds + successfullyAddedParticipantIds.remove(memberId) + + return state.withUpdatedTemporaryParticipants(temporaryParticipants).withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds) + } + return .complete() + case .tooManyChannels: + let _ = (context.account.postbox.loadedPeerWithId(memberId) + |> deliverOnMainQueue).start(next: { peer in + presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), nil) + }) + updateState { state in var temporaryParticipants = state.temporaryParticipants for i in 0 ..< temporaryParticipants.count { @@ -1771,7 +1793,15 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId: case .groupFull: let signal = convertGroupToSupergroup(account: context.account, peerId: peerView.peerId) |> map(Optional.init) - |> `catch` { _ -> Signal in + |> `catch` { error -> Signal in + switch error { + case .tooManyChannels: + Queue.mainQueue().async { + pushControllerImpl?(oldChannelsController(context: context, intent: .upgrade)) + } + default: + break + } return .single(nil) } |> mapToSignal { upgradedPeerId -> Signal in @@ -1849,16 +1879,17 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId: } inviteByLinkImpl = { [weak contactsController] in - contactsController?.dismiss() let mode: ChannelVisibilityControllerMode if groupPeer.addressName != nil { mode = .generic } else { mode = .privateLink } - presentControllerImpl?(channelVisibilityController(context: context, peerId: peerView.peerId, mode: mode, upgradedToSupergroup: { updatedPeerId, f in + let controller = channelVisibilityController(context: context, peerId: peerView.peerId, mode: mode, upgradedToSupergroup: { updatedPeerId, f in upgradedToSupergroupImpl?(updatedPeerId, f) - }), ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet)) + }) + controller.navigationPresentation = .modal + replaceControllerImpl?(contactsController, controller) } presentControllerImpl?(contactsController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) @@ -1997,7 +2028,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId: let presentationData = context.sharedContext.currentPresentationData.with { $0 } if let channel = peerView.peers[peerView.peerId] as? TelegramChannel, channel.flags.contains(.isCreator), stateValue.with({ $0 }).editingState != nil { - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -2014,7 +2045,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId: ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } else if let peer = peerView.peers[peerView.peerId] { - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -2311,7 +2342,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId: return state.withUpdatedSearchingMembers(false) } }, openPeer: { peer, _ in - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { arguments.pushController(infoController) } }, pushController: { c in @@ -2321,7 +2352,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId: }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.GroupInfo_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.GroupInfo_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) let entries = groupInfoEntries(account: context.account, presentationData: presentationData, view: view, channelMembers: channelMembers, globalNotificationSettings: globalNotificationSettings, state: state) var memberIds: [PeerId] = [] @@ -2343,7 +2374,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId: } } - let listState = ItemListNodeState(entries: entries, style: .blocks, searchItem: searchItem, animateChanges: animateChanges) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, searchItem: searchItem, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } @@ -2360,6 +2391,16 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId: controller?.view.endEditing(true) controller?.present(value, in: .window(.root), with: presentationArguments, blockInteraction: true) } + replaceControllerImpl = { [weak controller] previous, updated in + if let navigationController = controller?.navigationController as? NavigationController { + var controllers = navigationController.viewControllers + if let previous = previous { + controllers.removeAll(where: { $0 === previous }) + } + controllers.append(updated) + navigationController.setViewControllers(controllers, animated: true) + } + } dismissInputImpl = { [weak controller] in controller?.view.endEditing(true) } @@ -2458,7 +2499,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId: avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { - var result: ((ASDisplayNode, () -> (UIView?, UIView?)), CGRect)? + var result: ((ASDisplayNode, CGRect, () -> (UIView?, UIView?)), CGRect)? controller.forEachItemNode { itemNode in if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { result = itemNode.avatarTransitionNode() diff --git a/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift b/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift index 203081f8f8..f16c20505d 100644 --- a/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift +++ b/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift @@ -78,21 +78,21 @@ private enum GroupPreHistorySetupEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! GroupPreHistorySetupArguments switch self { case let .header(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .visible(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.toggle(true) }) case let .hidden(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.toggle(false) }) case let .info(theme, text): - return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) } } } @@ -124,6 +124,7 @@ public func groupPreHistorySetupController(context: AccountContext, peerId: Peer statePromise.set(stateValue.modify { f($0) }) } + var pushControllerImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? let actionsDisposable = DisposableSet() @@ -161,14 +162,7 @@ public func groupPreHistorySetupController(context: AccountContext, peerId: Peer if let value = value, value != defaultValue { if peerId.namespace == Namespaces.Peer.CloudGroup { let signal = convertGroupToSupergroup(account: context.account, peerId: peerId) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { upgradedPeerId -> Signal in - guard let upgradedPeerId = upgradedPeerId else { - return .single(nil) - } + |> mapToSignal { upgradedPeerId -> Signal in return updateChannelHistoryAvailabilitySettingsInteractively(postbox: context.account.postbox, network: context.account.network, accountStateManager: context.account.stateManager, peerId: upgradedPeerId, historyAvailableForNewMembers: value) |> `catch` { _ -> Signal in return .complete() @@ -177,6 +171,7 @@ public func groupPreHistorySetupController(context: AccountContext, peerId: Peer return .complete() } |> then(.single(upgradedPeerId)) + |> castError(ConvertGroupToSupergroupError.self) } |> deliverOnMainQueue applyDisposable.set((signal @@ -186,6 +181,13 @@ public func groupPreHistorySetupController(context: AccountContext, peerId: Peer dismissImpl?() }) } + }, error: { error in + switch error { + case .tooManyChannels: + pushControllerImpl?(oldChannelsController(context: context, intent: .upgrade)) + default: + break + } })) } else { applyDisposable.set((updateChannelHistoryAvailabilitySettingsInteractively(postbox: context.account.postbox, network: context.account.network, accountStateManager: context.account.stateManager, peerId: peerId, historyAvailableForNewMembers: value) @@ -199,8 +201,8 @@ public func groupPreHistorySetupController(context: AccountContext, peerId: Peer }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Group_Setup_HistoryTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: groupPreHistorySetupEntries(isSupergroup: peerId.namespace == Namespaces.Peer.CloudChannel, presentationData: presentationData, defaultValue: defaultValue, state: state), style: .blocks) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Group_Setup_HistoryTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: groupPreHistorySetupEntries(isSupergroup: peerId.namespace == Namespaces.Peer.CloudChannel, presentationData: presentationData, defaultValue: defaultValue, state: state), style: .blocks) return (controllerState, (listState, arguments)) } @@ -213,5 +215,8 @@ public func groupPreHistorySetupController(context: AccountContext, peerId: Peer controller?.view.endEditing(true) controller?.dismiss() } + pushControllerImpl = { [weak controller] c in + controller?.push(c) + } return controller } diff --git a/submodules/PeerInfoUI/Sources/GroupStickerPackSetupController.swift b/submodules/PeerInfoUI/Sources/GroupStickerPackSetupController.swift index da18984fc7..de91e5b228 100644 --- a/submodules/PeerInfoUI/Sources/GroupStickerPackSetupController.swift +++ b/submodules/PeerInfoUI/Sources/GroupStickerPackSetupController.swift @@ -208,11 +208,11 @@ private enum GroupStickerPackEntry: ItemListNodeEntry { } } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! GroupStickerPackSetupControllerArguments switch self { case let .search(theme, strings, prefix, placeholder, value): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: prefix, textColor: theme.list.itemPrimaryTextColor), text: value, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), spacing: 0.0, clearType: .always, tag: nil, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: prefix, textColor: theme.list.itemPrimaryTextColor), text: value, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), spacing: 0.0, clearType: .always, tag: nil, sectionId: self.section, textUpdated: { value in arguments.updateSearchText(value) }, processPaste: { text in if let url = (URL(string: text) ?? URL(string: "http://" + text)), url.host == "t.me" || url.host == "telegram.me" { @@ -224,11 +224,11 @@ private enum GroupStickerPackEntry: ItemListNodeEntry { return text }, action: {}) case let .searchInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section, linkAction: nil) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section, linkAction: nil) case let .packsTitle(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .pack(_, theme, strings, info, topItem, count, playAnimatedStickers, selected): - return ItemListStickerPackItem(theme: theme, strings: strings, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: selected ? .selection : .none, editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false, reorderable: false), enabled: true, playAnimatedStickers: playAnimatedStickers, sectionId: self.section, action: { + return ItemListStickerPackItem(presentationData: presentationData, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: selected ? .selection : .none, editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false, reorderable: false), enabled: true, playAnimatedStickers: playAnimatedStickers, sectionId: self.section, action: { if selected { arguments.openStickerPack(info) } else { @@ -462,7 +462,7 @@ public func groupStickerPackSetupController(context: AccountContext, peerId: Pee } } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Channel_Info_Stickers), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Channel_Info_Stickers), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let hasData = initialData != nil let hadData = previousHadData.swap(hasData) @@ -472,7 +472,7 @@ public func groupStickerPackSetupController(context: AccountContext, peerId: Pee emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } - let listState = ItemListNodeState(entries: groupStickerPackSetupControllerEntries(presentationData: presentationData, searchText: searchState.0, view: view, initialData: initialData, searchState: searchState.1, stickerSettings: stickerSettings), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: hasData && hadData) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: groupStickerPackSetupControllerEntries(presentationData: presentationData, searchText: searchState.0, view: view, initialData: initialData, searchState: searchState.1, stickerSettings: stickerSettings), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: hasData && hadData) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() @@ -490,7 +490,8 @@ public func groupStickerPackSetupController(context: AccountContext, peerId: Pee } presentStickerPackController = { [weak controller] info in dismissInputImpl?() - presentControllerImpl?(StickerPackPreviewController(context: context, stickerPack: .id(id: info.id.id, accessHash: info.accessHash), parentNavigationController: controller?.navigationController as? NavigationController), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) + presentControllerImpl?(StickerPackScreen(context: context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controller?.navigationController as? NavigationController), nil) } navigateToChatControllerImpl = { [weak controller] peerId in if let controller = controller, let navigationController = controller.navigationController as? NavigationController { diff --git a/submodules/PeerInfoUI/Sources/GroupsInCommonController.swift b/submodules/PeerInfoUI/Sources/GroupsInCommonController.swift index 0ba5f9badc..3cd3935cc2 100644 --- a/submodules/PeerInfoUI/Sources/GroupsInCommonController.swift +++ b/submodules/PeerInfoUI/Sources/GroupsInCommonController.swift @@ -14,13 +14,13 @@ import ItemListPeerItem import ContextUI private final class GroupsInCommonControllerArguments { - let account: Account + let context: AccountContext let openPeer: (PeerId) -> Void let contextAction: (Peer, ASDisplayNode, ContextGesture?) -> Void - init(account: Account, openPeer: @escaping (PeerId) -> Void, contextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) { - self.account = account + init(context: AccountContext, openPeer: @escaping (PeerId) -> Void, contextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) { + self.context = context self.openPeer = openPeer self.contextAction = contextAction } @@ -90,11 +90,11 @@ private enum GroupsInCommonEntry: ItemListNodeEntry { } } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! GroupsInCommonControllerArguments switch self { case let .peerItem(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer): - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { arguments.openPeer(peer.id) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in @@ -141,7 +141,7 @@ public func groupsInCommonController(context: AccountContext, peerId: PeerId) -> var contextActionImpl: ((Peer, ASDisplayNode, ContextGesture?) -> Void)? - let arguments = GroupsInCommonControllerArguments(account: context.account, openPeer: { memberId in + let arguments = GroupsInCommonControllerArguments(context: context, openPeer: { memberId in guard let navigationController = getNavigationControllerImpl?() else { return } @@ -154,7 +154,7 @@ public func groupsInCommonController(context: AccountContext, peerId: PeerId) -> return context.account.postbox.transaction { transaction -> [Peer] in var result: [Peer] = [] for id in peerIds { - if let peer = transaction.getPeer(id) { + if let peer = transaction.getPeer(id.id) { result.append(peer) } } @@ -178,8 +178,8 @@ public func groupsInCommonController(context: AccountContext, peerId: PeerId) -> let previous = previousPeers previousPeers = peers - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.UserInfo_GroupsInCommon), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: groupsInCommonControllerEntries(presentationData: presentationData, state: state, peers: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.UserInfo_GroupsInCommon), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: groupsInCommonControllerEntries(presentationData: presentationData, state: state, peers: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) return (controllerState, (listState, arguments)) } |> afterDisposed { @@ -208,7 +208,7 @@ public func groupsInCommonController(context: AccountContext, peerId: PeerId) -> arguments.openPeer(peer.id) })) ] - let contextController = ContextController(account: context.account, theme: presentationData.theme, strings: presentationData.strings, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) controller.presentInGlobalOverlay(contextController) } return controller diff --git a/submodules/PeerInfoUI/Sources/ItemListCallListItem.swift b/submodules/PeerInfoUI/Sources/ItemListCallListItem.swift index 9cfa007e61..1364132297 100644 --- a/submodules/PeerInfoUI/Sources/ItemListCallListItem.swift +++ b/submodules/PeerInfoUI/Sources/ItemListCallListItem.swift @@ -11,24 +11,24 @@ import ItemListUI import PresentationDataUtils import TelegramStringFormatting -class ItemListCallListItem: ListViewItem, ItemListItem { - let theme: PresentationTheme - let strings: PresentationStrings +public class ItemListCallListItem: ListViewItem, ItemListItem { + let presentationData: ItemListPresentationData let dateTimeFormat: PresentationDateTimeFormat let messages: [Message] - let sectionId: ItemListSectionId + public let sectionId: ItemListSectionId let style: ItemListStyle + let displayDecorations: Bool - init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, messages: [Message], sectionId: ItemListSectionId, style: ItemListStyle) { - self.theme = theme - self.strings = strings + public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, messages: [Message], sectionId: ItemListSectionId, style: ItemListStyle, displayDecorations: Bool = true) { + self.presentationData = presentationData self.dateTimeFormat = dateTimeFormat self.messages = messages self.sectionId = sectionId self.style = style + self.displayDecorations = displayDecorations } - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = ItemListCallListItemNode() let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) @@ -44,7 +44,7 @@ class ItemListCallListItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { if let nodeValue = node() as? ItemListCallListItemNode { let makeLayout = nodeValue.asyncLayout() @@ -62,10 +62,6 @@ class ItemListCallListItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(15.0) -private let font = Font.regular(14.0) -private let typeFont = Font.medium(14.0) - private func stringForCallType(message: Message, strings: PresentationStrings) -> String { var string = "" for media in message.media { @@ -103,7 +99,7 @@ private func stringForCallType(message: Message, strings: PresentationStrings) - return string } -class ItemListCallListItemNode: ListViewItemNode { +public class ItemListCallListItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode @@ -115,11 +111,11 @@ class ItemListCallListItemNode: ListViewItemNode { private var item: ItemListCallListItem? - override var canBeSelected: Bool { + override public var canBeSelected: Bool { return false } - init() { + public init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.backgroundColor = .white @@ -144,7 +140,7 @@ class ItemListCallListItemNode: ListViewItemNode { self.addSubnode(self.accessibilityArea) } - func asyncLayout() -> (_ item: ItemListCallListItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + public func asyncLayout() -> (_ item: ItemListCallListItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let currentItem = self.item @@ -181,13 +177,17 @@ class ItemListCallListItemNode: ListViewItemNode { var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } + let titleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + let font = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + let typeFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + let contentSize: CGSize var contentHeight: CGFloat = 0.0 - let insets: UIEdgeInsets + var insets: UIEdgeInsets let separatorHeight = UIScreenPixel let itemBackgroundColor: UIColor let itemSeparatorColor: UIColor @@ -195,19 +195,23 @@ class ItemListCallListItemNode: ListViewItemNode { let leftInset = 16.0 + params.leftInset switch item.style { - case .plain: - itemBackgroundColor = item.theme.list.plainBackgroundColor - itemSeparatorColor = item.theme.list.itemPlainSeparatorColor - insets = itemListNeighborsPlainInsets(neighbors) - case .blocks: - itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor - itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor - insets = itemListNeighborsGroupedInsets(neighbors) + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + insets = itemListNeighborsGroupedInsets(neighbors) + } + + if !item.displayDecorations { + insets = UIEdgeInsets() } let earliestMessage = item.messages.sorted(by: {$0.timestamp < $1.timestamp}).first! - let titleText = stringForDate(timestamp: earliestMessage.timestamp, strings: item.strings) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleText, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let titleText = stringForDate(timestamp: earliestMessage.timestamp, strings: item.presentationData.strings) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleText, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) contentHeight += titleLayout.size.height + 18.0 @@ -217,11 +221,11 @@ class ItemListCallListItemNode: ListViewItemNode { for message in item.messages { let makeTimeLayout = makeNodesLayout[index].0 let time = stringForMessageTimestamp(timestamp: message.timestamp, dateTimeFormat: item.dateTimeFormat) - let (timeLayout, timeApply) = makeTimeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: time, font: font, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (timeLayout, timeApply) = makeTimeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: time, font: font, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let makeTypeLayout = makeNodesLayout[index].1 - let type = stringForCallType(message: message, strings: item.strings) - let (typeLayout, typeApply) = makeTypeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: type, font: typeFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let type = stringForCallType(message: message, strings: item.presentationData.strings) + let (typeLayout, typeApply) = makeTypeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: type, font: typeFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) nodesLayout.append((timeLayout, typeLayout)) nodesApply.append((timeApply, typeApply)) @@ -279,8 +283,10 @@ class ItemListCallListItemNode: ListViewItemNode { case .sameSection(false): strongSelf.topStripeNode.isHidden = true default: - strongSelf.topStripeNode.isHidden = false + strongSelf.topStripeNode.isHidden = !item.displayDecorations } + strongSelf.bottomStripeNode.isHidden = !item.displayDecorations + strongSelf.backgroundNode.isHidden = !item.displayDecorations let bottomStripeInset: CGFloat switch neighbors.bottom { case .sameSection(false): @@ -313,15 +319,15 @@ class ItemListCallListItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } - override func animateAdded(_ currentTimestamp: Double, duration: Double) { + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } } diff --git a/submodules/PeerInfoUI/Sources/ItemListSecretChatKeyItem.swift b/submodules/PeerInfoUI/Sources/ItemListSecretChatKeyItem.swift index 7482b7d33f..fd8f048e69 100644 --- a/submodules/PeerInfoUI/Sources/ItemListSecretChatKeyItem.swift +++ b/submodules/PeerInfoUI/Sources/ItemListSecretChatKeyItem.swift @@ -11,7 +11,7 @@ import PresentationDataUtils import EncryptionKeyVisualization class ItemListSecretChatKeyItem: ListViewItem, ItemListItem { - let theme: PresentationTheme + let presentationData: ItemListPresentationData let icon: UIImage? let title: String let fingerprint: SecretChatKeyFingerprint @@ -20,8 +20,8 @@ class ItemListSecretChatKeyItem: ListViewItem, ItemListItem { let disclosureStyle: ItemListDisclosureStyle let action: (() -> Void)? - init(theme: PresentationTheme, icon: UIImage? = nil, title: String, fingerprint: SecretChatKeyFingerprint, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?) { - self.theme = theme + init(presentationData: ItemListPresentationData, icon: UIImage? = nil, title: String, fingerprint: SecretChatKeyFingerprint, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?) { + self.presentationData = presentationData self.icon = icon self.title = title self.fingerprint = fingerprint @@ -72,8 +72,6 @@ class ItemListSecretChatKeyItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(17.0) - class ItemListSecretChatKeyItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -150,9 +148,9 @@ class ItemListSecretChatKeyItemNode: ListViewItemNode { var updateArrowImage: UIImage? var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme - updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.theme) + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) } var updateIcon = false @@ -172,25 +170,26 @@ class ItemListSecretChatKeyItemNode: ListViewItemNode { let itemSeparatorColor: UIColor var leftInset = 16.0 + params.leftInset - - switch item.style { - case .plain: - itemBackgroundColor = item.theme.list.plainBackgroundColor - itemSeparatorColor = item.theme.list.itemPlainSeparatorColor - contentSize = CGSize(width: params.width, height: 44.0) - insets = itemListNeighborsPlainInsets(neighbors) - case .blocks: - itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor - itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor - contentSize = CGSize(width: params.width, height: 44.0) - insets = itemListNeighborsGroupedInsets(neighbors) - } - if let _ = item.icon { leftInset += 43.0 } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: 22.0 + titleLayout.size.height) + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: 22.0 + titleLayout.size.height) + insets = itemListNeighborsGroupedInsets(neighbors) + } let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -219,7 +218,7 @@ class ItemListSecretChatKeyItemNode: ListViewItemNode { strongSelf.topStripeNode.backgroundColor = itemSeparatorColor strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } let _ = titleApply() @@ -271,7 +270,7 @@ class ItemListSecretChatKeyItemNode: ListViewItemNode { strongSelf.keyNode.image = updateKeyImage } if let image = strongSelf.keyNode.image { - strongSelf.keyNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - image.size.width, y: 10.0), size: image.size) + strongSelf.keyNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - image.size.width, y: floor((layout.contentSize.height - image.size.height) / 2.0)), size: image.size) } if let arrowImage = strongSelf.arrowNode.image { @@ -285,7 +284,7 @@ class ItemListSecretChatKeyItemNode: ListViewItemNode { strongSelf.arrowNode.isHidden = false } - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel)) } }) } diff --git a/submodules/PeerInfoUI/Sources/OldChannelsController.swift b/submodules/PeerInfoUI/Sources/OldChannelsController.swift new file mode 100644 index 0000000000..2b10efb4ef --- /dev/null +++ b/submodules/PeerInfoUI/Sources/OldChannelsController.swift @@ -0,0 +1,498 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import AccountContext +import ContactsPeerItem +import SearchUI +import SolidRoundedButtonNode + +func localizedOldChannelDate(peer: InactiveChannel, strings: PresentationStrings) -> String { + let timestamp = peer.lastActivityDate + let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + + var t: time_t = time_t(TimeInterval(timestamp)) + var timeinfo: tm = tm() + localtime_r(&t, &timeinfo) + + var now: time_t = time_t(nowTimestamp) + var timeinfoNow: tm = tm() + localtime_r(&now, &timeinfoNow) + + var string: String + + if timeinfoNow.tm_year == timeinfo.tm_year && timeinfoNow.tm_mon == timeinfo.tm_mon { + //weeks + let dif = Int(roundf(Float(timeinfoNow.tm_mday - timeinfo.tm_mday) / 7)) + string = strings.OldChannels_InactiveWeek(Int32(dif)) + } else if timeinfoNow.tm_year == timeinfo.tm_year { + //month + let dif = Int(timeinfoNow.tm_mon - timeinfo.tm_mon) + string = strings.OldChannels_InactiveMonth(Int32(dif)) + } else { + //year + var dif = Int(timeinfoNow.tm_year - timeinfo.tm_year) + + if Int(timeinfoNow.tm_mon - timeinfo.tm_mon) > 6 { + dif += 1 + } + string = strings.OldChannels_InactiveYear(Int32(dif)) + } + + if let channel = peer.peer as? TelegramChannel, case .group = channel.info { + if let participantsCount = peer.participantsCount, participantsCount != 0 { + string = strings.OldChannels_GroupFormat(participantsCount) + string + } else { + string = strings.OldChannels_GroupEmptyFormat + string + } + } else { + string = strings.OldChannels_ChannelFormat + string + } + + return string +} + +private final class OldChannelsItemArguments { + let context: AccountContext + let togglePeer: (PeerId, Bool) -> Void + + init( + context: AccountContext, + togglePeer: @escaping (PeerId, Bool) -> Void + ) { + self.context = context + self.togglePeer = togglePeer + } +} + +private enum OldChannelsSection: Int32 { + case info + case peers +} + +private enum OldChannelsEntryId: Hashable { + case info + case peersHeader + case peer(PeerId) +} + +private enum OldChannelsEntry: ItemListNodeEntry { + case info(String, String) + case peersHeader(String) + case peer(Int, InactiveChannel, Bool) + + var section: ItemListSectionId { + switch self { + case .info: + return OldChannelsSection.info.rawValue + case .peersHeader, .peer: + return OldChannelsSection.peers.rawValue + } + } + + var stableId: OldChannelsEntryId { + switch self { + case .info: + return .info + case .peersHeader: + return .peersHeader + case let .peer(_, peer, _): + return .peer(peer.peer.id) + } + } + + static func ==(lhs: OldChannelsEntry, rhs: OldChannelsEntry) -> Bool { + switch lhs { + case let .info(title, text): + if case .info(title, text) = rhs { + return true + } else { + return false + } + case let .peersHeader(title): + if case .peersHeader(title) = rhs { + return true + } else { + return false + } + case let .peer(lhsIndex, lhsPeer, lhsSelected): + if case let .peer(rhsIndex, rhsPeer, rhsSelected) = rhs { + if lhsIndex != rhsIndex { + return false + } + if lhsPeer != rhsPeer { + return false + } + if lhsSelected != rhsSelected { + return false + } + return true + } else { + return false + } + } + } + + static func <(lhs: OldChannelsEntry, rhs: OldChannelsEntry) -> Bool { + switch lhs { + case .info: + if case .info = rhs { + return false + } else { + return true + } + case .peersHeader: + switch rhs { + case .info, .peersHeader: + return false + case .peer: + return true + } + case let .peer(lhsIndex, _, _): + switch rhs { + case .info, .peersHeader: + return false + case let .peer(rhsIndex, _, _): + return lhsIndex < rhsIndex + } + } + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! OldChannelsItemArguments + switch self { + case let .info(title, text): + return ItemListInfoItem(presentationData: presentationData, title: title, text: .plain(text), style: .blocks, sectionId: self.section, closeAction: nil) + case let .peersHeader(title): + return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) + case let .peer(_, peer, selected): + return ContactsPeerItem(presentationData: presentationData, style: .blocks, sectionId: self.section, sortOrder: .firstLast, displayOrder: .firstLast, context: arguments.context, peerMode: .peer, peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .custom(localizedOldChannelDate(peer: peer, strings: presentationData.strings)), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in + arguments.togglePeer(peer.peer.id, true) + }, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil) + } + } +} + +private struct OldChannelsState: Equatable { + var selectedPeers: Set = Set() + var isSearching: Bool = false +} + +private func oldChannelsEntries(presentationData: PresentationData, state: OldChannelsState, peers: [InactiveChannel]?, intent: OldChannelsControllerIntent) -> [OldChannelsEntry] { + var entries: [OldChannelsEntry] = [] + + let noticeText: String + switch intent { + case .join: + noticeText = presentationData.strings.OldChannels_NoticeText + case .create: + noticeText = presentationData.strings.OldChannels_NoticeCreateText + case .upgrade: + noticeText = presentationData.strings.OldChannels_NoticeUpgradeText + } + entries.append(.info(presentationData.strings.OldChannels_NoticeTitle, noticeText)) + + if let peers = peers, !peers.isEmpty { + entries.append(.peersHeader(presentationData.strings.OldChannels_ChannelsHeader)) + + for peer in peers { + entries.append(.peer(entries.count, peer, state.selectedPeers.contains(peer.peer.id))) + } + } + + return entries +} + +private final class OldChannelsActionPanelNode: ASDisplayNode { + private let separatorNode: ASDisplayNode + let buttonNode: SolidRoundedButtonNode + + init(presentationData: ItemListPresentationData, leaveAction: @escaping () -> Void) { + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor + self.buttonNode = SolidRoundedButtonNode(title: "", icon: nil, theme: SolidRoundedButtonTheme(theme: presentationData.theme), height: 50.0, cornerRadius: 10.0, gloss: false) + + super.init() + + self.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor + + self.addSubnode(self.separatorNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.pressed = { + leaveAction() + } + } + + func updatePresentationData(_ presentationData: ItemListPresentationData) { + self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor + self.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor + } + + func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat { + let sideInset: CGFloat = 16.0 + let verticalInset: CGFloat = 16.0 + let buttonHeight: CGFloat = 50.0 + + let insets = layout.insets(options: [.input]) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + + self.buttonNode.updateLayout(width: layout.size.width - sideInset * 2.0, transition: transition) + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: CGSize(width: layout.size.width, height: buttonHeight))) + + return buttonHeight + verticalInset * 2.0 + insets.bottom + } +} + +private final class OldChannelsControllerImpl: ItemListController { + private let panelNode: OldChannelsActionPanelNode + + private var displayPanel: Bool = false + private var validLayout: ContainerViewLayout? + + private var presentationData: ItemListPresentationData + private var presentationDataDisposable: Disposable? + + var leaveAction: (() -> Void)? + + override init(presentationData: ItemListPresentationData, updatedPresentationData: Signal, state: Signal<(ItemListControllerState, (ItemListNodeState, ItemGenerationArguments)), NoError>, tabBarItem: Signal?) { + self.presentationData = presentationData + + var leaveActionImpl: (() -> Void)? + self.panelNode = OldChannelsActionPanelNode(presentationData: presentationData, leaveAction: { + leaveActionImpl?() + }) + + super.init(presentationData: presentationData, updatedPresentationData: updatedPresentationData, state: state, tabBarItem: tabBarItem) + + self.presentationDataDisposable = (updatedPresentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + guard let strongSelf = self else { + return + } + strongSelf.presentationData = presentationData + strongSelf.panelNode.updatePresentationData(presentationData) + }) + + leaveActionImpl = { [weak self] in + self?.leaveAction?() + } + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + override var navigationBarRequiresEntireLayoutUpdate: Bool { + return false + } + + override func loadDisplayNode() { + super.loadDisplayNode() + + self.displayNode.addSubnode(self.panelNode) + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + + let panelHeight = self.panelNode.updateLayout(layout, transition: transition) + + var additionalInsets = UIEdgeInsets() + additionalInsets.bottom = max(layout.intrinsicInsets.bottom, panelHeight) + + self.additionalInsets = additionalInsets + + super.containerLayoutUpdated(layout, transition: transition) + + transition.updateFrame(node: self.panelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.displayPanel ? (layout.size.height - panelHeight) : layout.size.height), size: CGSize(width: layout.size.width, height: panelHeight)), beginWithCurrentState: true) + } + + func updatePanelPeerCount(_ value: Int) { + self.panelNode.buttonNode.title = self.presentationData.strings.OldChannels_Leave(Int32(value)) + + if self.displayPanel != (value != 0) { + self.displayPanel = (value != 0) + if let layout = self.validLayout { + self.containerLayoutUpdated(layout, transition: .animated(duration: 0.3, curve: .spring)) + } + } + } +} + +public enum OldChannelsControllerIntent { + case join + case create + case upgrade +} + +public func oldChannelsController(context: AccountContext, intent: OldChannelsControllerIntent, completed: @escaping (Bool) -> Void = { _ in }) -> ViewController { + let initialState = OldChannelsState() + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((OldChannelsState) -> OldChannelsState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var updateSelectedPeersImpl: ((Int) -> Void)? + + var dismissImpl: (() -> Void)? + var setDisplayNavigationBarImpl: ((Bool) -> Void)? + + var ensurePeerVisibleImpl: ((PeerId) -> Void)? + + let actionsDisposable = DisposableSet() + + let arguments = OldChannelsItemArguments( + context: context, + togglePeer: { peerId, ensureVisible in + var selectedPeerCount = 0 + var didSelect = false + updateState { state in + var state = state + if state.selectedPeers.contains(peerId) { + state.selectedPeers.remove(peerId) + } else { + state.selectedPeers.insert(peerId) + didSelect = true + } + selectedPeerCount = state.selectedPeers.count + return state + } + updateSelectedPeersImpl?(selectedPeerCount) + if didSelect && ensureVisible { + ensurePeerVisibleImpl?(peerId) + } + } + ) + + let selectedPeerIds = statePromise.get() + |> map { $0.selectedPeers } + |> distinctUntilChanged + + let peersSignal: Signal<[InactiveChannel]?, NoError> = .single(nil) + |> then( + inactiveChannelList(network: context.account.network) + |> map { peers -> [InactiveChannel]? in + return peers.sorted(by: { lhs, rhs in + return lhs.lastActivityDate < rhs.lastActivityDate + }) + } + ) + + let peersPromise = Promise<[InactiveChannel]?>() + peersPromise.set(peersSignal) + + var previousPeersWereEmpty = true + + let signal = combineLatest( + queue: Queue.mainQueue(), + context.sharedContext.presentationData, + statePromise.get(), + peersPromise.get() + ) + |> map { presentationData, state, peers -> (ItemListControllerState, (ItemListNodeState, Any)) in + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + dismissImpl?() + }) + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.OldChannels_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + + var searchItem: OldChannelsSearchItem? + searchItem = OldChannelsSearchItem(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) + } + }, peers: peersPromise.get() |> map { $0 ?? [] }, selectedPeerIds: selectedPeerIds, togglePeer: { peerId in + arguments.togglePeer(peerId, false) + }) + + let peersAreEmpty = peers == nil + let peersAreEmptyUpdated = previousPeersWereEmpty != peersAreEmpty + previousPeersWereEmpty = peersAreEmpty + + var emptyStateItem: ItemListControllerEmptyStateItem? + if peersAreEmpty { + emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) + } + + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: oldChannelsEntries(presentationData: presentationData, state: state, peers: peers, intent: intent), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, initialScrollToItem: ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up), crossfadeState: peersAreEmptyUpdated, animateChanges: false) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = OldChannelsControllerImpl(context: context, state: signal) + controller.navigationPresentation = .modal + + updateSelectedPeersImpl = { [weak controller] value in + controller?.updatePanelPeerCount(value) + } + + controller.leaveAction = { + let state = stateValue.with { $0 } + let _ = (peersPromise.get() + |> take(1) + |> mapToSignal { peers in + return context.account.postbox.transaction { transaction -> Void in + if let peers = peers { + for peer in peers { + if state.selectedPeers.contains(peer.peer.id) { + if transaction.getPeer(peer.peer.id) == nil { + updatePeers(transaction: transaction, peers: [peer.peer], update: { _, updated in + return updated + }) + } + removePeerChat(account: context.account, transaction: transaction, mediaBox: context.account.postbox.mediaBox, peerId: peer.peer.id, reportChatSpam: false, deleteGloballyIfPossible: false) + } + } + } + } + } + |> deliverOnMainQueue).start(completed: { + completed(true) + dismissImpl?() + }) + } + + dismissImpl = { [weak controller] in + controller?.dismiss() + } + setDisplayNavigationBarImpl = { [weak controller] display in + controller?.setDisplayNavigationBar(display, transition: .animated(duration: 0.5, curve: .spring)) + } + ensurePeerVisibleImpl = { [weak controller] peerId in + guard let controller = controller else { + return + } + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ContactsPeerItemNode, let peer = itemNode.chatPeer, peer.id == peerId { + controller.ensureItemNodeVisible(itemNode, curve: .Spring(duration: 0.3)) + } + } + } + + return controller +} diff --git a/submodules/PeerInfoUI/Sources/OldChannelsSearch.swift b/submodules/PeerInfoUI/Sources/OldChannelsSearch.swift new file mode 100644 index 0000000000..7d51f393be --- /dev/null +++ b/submodules/PeerInfoUI/Sources/OldChannelsSearch.swift @@ -0,0 +1,426 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SyncCore +import SwiftSignalKit +import TelegramPresentationData +import MergeLists +import ItemListUI +import PresentationDataUtils +import AccountContext +import SearchBarNode +import SearchUI +import ChatListSearchItemHeader +import ContactsPeerItem + +extension NavigationBarSearchContentNode: ItemListControllerSearchNavigationContentNode { + public func activate() { + } + + public func deactivate() { + } + + public func setQueryUpdated(_ f: @escaping (String) -> Void) { + } +} + +final class OldChannelsSearchItem: ItemListControllerSearch { + let context: AccountContext + let theme: PresentationTheme + let placeholder: String + let activated: Bool + let updateActivated: (Bool) -> Void + let peers: Signal<[InactiveChannel], NoError> + let selectedPeerIds: Signal, NoError> + let togglePeer: (PeerId) -> Void + + private var updateActivity: ((Bool) -> Void)? + private var activity: ValuePromise = ValuePromise(ignoreRepeated: false) + private let activityDisposable = MetaDisposable() + + init(context: AccountContext, theme: PresentationTheme, placeholder: String, activated: Bool, updateActivated: @escaping (Bool) -> Void, peers: Signal<[InactiveChannel], NoError>, selectedPeerIds: Signal, NoError>, togglePeer: @escaping (PeerId) -> Void) { + self.context = context + self.theme = theme + self.placeholder = placeholder + self.activated = activated + self.updateActivated = updateActivated + self.peers = peers + self.selectedPeerIds = selectedPeerIds + self.togglePeer = togglePeer + } + + deinit { + self.activityDisposable.dispose() + } + + func isEqual(to: ItemListControllerSearch) -> Bool { + if let to = to as? OldChannelsSearchItem { + if self.context !== to.context || self.theme !== to.theme || self.placeholder != to.placeholder || self.activated != to.activated { + return false + } + return true + } else { + return false + } + } + + func titleContentNode(current: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> NavigationBarContentNode & ItemListControllerSearchNavigationContentNode { + let updateActivated: (Bool) -> Void = self.updateActivated + if let current = current as? NavigationBarSearchContentNode { + current.updateThemeAndPlaceholder(theme: self.theme, placeholder: self.placeholder) + return current + } else { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + return NavigationBarSearchContentNode(theme: presentationData.theme, placeholder: presentationData.strings.Settings_Search, activate: { + updateActivated(true) + }) + } + } + + func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode { + let updateActivated: (Bool) -> Void = self.updateActivated + + if let current = current as? OldChannelsSearchItemNode, let titleContentNode = titleContentNode as? NavigationBarSearchContentNode { + current.updatePresentationData(self.context.sharedContext.currentPresentationData.with { $0 }) + if current.isSearching != self.activated { + if self.activated { + current.activateSearch(placeholderNode: titleContentNode.placeholderNode) + } else { + current.deactivateSearch(placeholderNode: titleContentNode.placeholderNode) + } + } + return current + } else { + return OldChannelsSearchItemNode(context: self.context, cancel: { + updateActivated(false) + }, peers: self.peers, selectedPeerIds: self.selectedPeerIds, togglePeer: self.togglePeer) + } + } +} + +private final class OldChannelsSearchInteraction { + let togglePeer: (PeerId) -> Void + + init(togglePeer: @escaping (PeerId) -> Void) { + self.togglePeer = togglePeer + } +} + +private enum OldChannelsSearchEntry: Comparable, Identifiable { + case peer(Int, InactiveChannel, Bool) + + var stableId: PeerId { + switch self { + case let .peer(_, peer, _): + return peer.peer.id + } + } + + private func index() -> Int { + switch self { + case let .peer(index, _, _): + return index + } + } + + static func <(lhs: OldChannelsSearchEntry, rhs: OldChannelsSearchEntry) -> Bool { + return lhs.index() < rhs.index() + } + + static func ==(lhs: OldChannelsSearchEntry, rhs: OldChannelsSearchEntry) -> Bool { + if case let .peer(index, peer, isSelected) = lhs { + if case .peer(index, peer, isSelected) = rhs { + return true + } + } + return false + } + + func item(context: AccountContext, presentationData: ItemListPresentationData, interaction: OldChannelsSearchInteraction) -> ListViewItem { + switch self { + case let .peer(_, peer, selected): + return ContactsPeerItem(presentationData: presentationData, style: .plain, sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .peer, peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .custom(localizedOldChannelDate(peer: peer, strings: presentationData.strings)), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in + interaction.togglePeer(peer.peer.id) + }, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil) + } + } +} + +private struct OldChannelsSearchContainerTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let isSearching: Bool +} + +private func preparedOldChannelsSearchContainerTransition(presentationData: ItemListPresentationData, from fromEntries: [OldChannelsSearchEntry], to toEntries: [OldChannelsSearchEntry], context: AccountContext, interaction: OldChannelsSearchInteraction, isSearching: Bool, forceUpdate: Bool) -> OldChannelsSearchContainerTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } + + return OldChannelsSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) +} + +private final class OldChannelsSearchContainerNode: SearchDisplayControllerContentNode { + private let listNode: ListView + + private var enqueuedTransitions: [OldChannelsSearchContainerTransition] = [] + private var hasValidLayout = false + + private let searchQuery = Promise() + private let searchDisposable = MetaDisposable() + + private var recentDisposable: Disposable? + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + private let presentationDataPromise: Promise + + init(context: AccountContext, peers: Signal<[InactiveChannel], NoError>, selectedPeerIds: Signal, NoError>, togglePeer: @escaping (PeerId) -> Void) { + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationDataPromise = Promise(self.presentationData) + + self.listNode = ListView() + self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor + self.listNode.isHidden = true + + super.init() + + self.backgroundColor = self.presentationData.theme.chatList.backgroundColor + + self.addSubnode(self.listNode) + + let interaction = OldChannelsSearchInteraction(togglePeer: { [weak self] peerId in + togglePeer(peerId) + + if let strongSelf = self { + strongSelf.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ContactsPeerItemNode, let peer = itemNode.chatPeer, peer.id == peerId { + strongSelf.listNode.ensureItemNodeVisible(itemNode, curve: .Spring(duration: 0.3)) + } + } + } + }) + + let queryAndFoundItems: Signal<(String, [OldChannelsSearchEntry])?, NoError> = combineLatest(self.searchQuery.get(), peers, selectedPeerIds) + |> mapToSignal { query, peers, selectedPeerIds -> Signal<(String, [OldChannelsSearchEntry])?, NoError> in + if let query = query, !query.isEmpty { + var results: [OldChannelsSearchEntry] = [] + let normalizedQuery = query.lowercased() + for peer in peers { + if peer.peer.indexName.matchesByTokens(normalizedQuery) { + results.append(.peer(results.count, peer, selectedPeerIds.contains(peer.peer.id))) + } + } + return .single((query, results)) + } else { + return .single(nil) + } + } + + let previousEntriesHolder = Atomic<([OldChannelsSearchEntry], PresentationTheme, PresentationStrings)?>(value: nil) + self.searchDisposable.set(combineLatest(queue: .mainQueue(), queryAndFoundItems, self.presentationDataPromise.get()).start(next: { [weak self] queryAndFoundItems, presentationData in + guard let strongSelf = self else { + return + } + var currentQuery: String? + var entries: [OldChannelsSearchEntry] = [] + if let (query, items) = queryAndFoundItems { + currentQuery = query + for item in items { + entries.append(item) + } + } + + if !entries.isEmpty || currentQuery == nil { + let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings)) + let transition = preparedOldChannelsSearchContainerTransition(presentationData: ItemListPresentationData(presentationData), from: previousEntriesAndPresentationData?.0 ?? [], to: entries, context: context, interaction: interaction, isSearching: queryAndFoundItems != nil, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings) + strongSelf.enqueueTransition(transition) + } + })) + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) + strongSelf.presentationDataPromise.set(.single(presentationData)) + } + } + }) + + self.listNode.beganInteractiveDragging = { [weak self] in + self?.dismissInput?() + } + } + + deinit { + self.searchDisposable.dispose() + self.recentDisposable?.dispose() + self.presentationDataDisposable?.dispose() + } + + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.listNode.backgroundColor = theme.chatList.backgroundColor + } + + override func searchTextUpdated(text: String) { + if text.isEmpty { + self.searchQuery.set(.single(nil)) + } else { + self.searchQuery.set(.single(text)) + } + } + + private func enqueueTransition(_ transition: OldChannelsSearchContainerTransition) { + self.enqueuedTransitions.append(transition) + + if self.hasValidLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + if let transition = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + options.insert(.Synchronous) + options.insert(.PreferSynchronousDrawing) + options.insert(.PreferSynchronousResourceLoading) + + let isSearching = transition.isSearching + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + self?.listNode.isHidden = !isSearching + }) + } + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + + var insets = layout.insets(options: [.input]) + insets.top += navigationBarHeight + insets.left += layout.safeInsets.left + insets.right += layout.safeInsets.right + + self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if !self.hasValidLayout { + self.hasValidLayout = true + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + override func scrollToTop() { + let listNodeToScroll: ListView = self.listNode + listNodeToScroll.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancel?() + } + } +} + +private final class OldChannelsSearchItemNode: ItemListControllerSearchNode { + private let context: AccountContext + private var presentationData: PresentationData + private var containerLayout: (ContainerViewLayout, CGFloat)? + private var searchDisplayController: SearchDisplayController? + + var cancel: () -> Void + private let peers: Signal<[InactiveChannel], NoError> + private let selectedPeerIds: Signal, NoError> + private let togglePeer: (PeerId) -> Void + + init(context: AccountContext, cancel: @escaping () -> Void, peers: Signal<[InactiveChannel], NoError>, selectedPeerIds: Signal, NoError>, togglePeer: @escaping (PeerId) -> Void) { + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.cancel = cancel + self.peers = peers + self.selectedPeerIds = selectedPeerIds + self.togglePeer = togglePeer + + super.init() + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + self.searchDisplayController?.updatePresentationData(presentationData) + } + + func activateSearch(placeholderNode: SearchBarPlaceholderNode) { + guard let (containerLayout, navigationBarHeight) = self.containerLayout, self.searchDisplayController == nil else { + return + } + + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: OldChannelsSearchContainerNode(context: self.context, peers: self.peers, selectedPeerIds: self.selectedPeerIds, togglePeer: self.togglePeer), cancel: { [weak self] in + self?.cancel() + }) + + self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in + if let strongSelf = self, let strongPlaceholderNode = placeholderNode { + if isSearchBar { + strongPlaceholderNode.supernode?.insertSubnode(subnode, aboveSubnode: strongPlaceholderNode) + } else { + strongSelf.addSubnode(subnode) + } + } + }, placeholder: placeholderNode) + } + + func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) { + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.deactivate(placeholder: placeholderNode) + self.searchDisplayController = nil + } + } + + var isSearching: Bool { + return self.searchDisplayController != nil + } + + override func scrollToTop() { + self.searchDisplayController?.contentNode.scrollToTop() + } + + override func queryUpdated(_ query: String) { + } + + override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.containerLayout = (layout, navigationBarHeight) + + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let searchDisplayController = self.searchDisplayController, let result = searchDisplayController.contentNode.hitTest(self.view.convert(point, to: searchDisplayController.contentNode.view), with: event) { + return result + } + + return super.hitTest(point, with: event) + } +} + diff --git a/submodules/PeerInfoUI/Sources/PeerBanTimeoutController.swift b/submodules/PeerInfoUI/Sources/PeerBanTimeoutController.swift index e0dfa93163..b6a7459613 100644 --- a/submodules/PeerInfoUI/Sources/PeerBanTimeoutController.swift +++ b/submodules/PeerInfoUI/Sources/PeerBanTimeoutController.swift @@ -23,13 +23,13 @@ final class PeerBanTimeoutController: ActionSheetController { let theme = presentationData.theme let strings = presentationData.strings - super.init(theme: ActionSheetControllerTheme(presentationTheme: theme)) + super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) self._ready.set(.single(true)) self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationTheme: presentationData.theme) + strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) } }) diff --git a/submodules/PeerInfoUI/Sources/PeerInfoController.swift b/submodules/PeerInfoUI/Sources/PeerInfoController.swift index f34ef9a845..62e5703def 100644 --- a/submodules/PeerInfoUI/Sources/PeerInfoController.swift +++ b/submodules/PeerInfoUI/Sources/PeerInfoController.swift @@ -7,17 +7,3 @@ import TelegramCore import SyncCore import AccountContext -public func peerInfoControllerImpl(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode) -> ViewController? { - if let _ = peer as? TelegramGroup { - return groupInfoController(context: context, peerId: peer.id) - } else if let channel = peer as? TelegramChannel { - if case .group = channel.info { - return groupInfoController(context: context, peerId: peer.id) - } else { - return channelInfoController(context: context, peerId: peer.id) - } - } else if peer is TelegramUser || peer is TelegramSecretChat { - return userInfoController(context: context, peerId: peer.id, mode: mode) - } - return nil -} diff --git a/submodules/PeerInfoUI/Sources/PeerReportController.swift b/submodules/PeerInfoUI/Sources/PeerReportController.swift index 759c2d8d84..a4f1859090 100644 --- a/submodules/PeerInfoUI/Sources/PeerReportController.swift +++ b/submodules/PeerInfoUI/Sources/PeerReportController.swift @@ -18,26 +18,18 @@ public enum PeerReportSubject { case messages([MessageId]) } -private enum PeerReportOption { +public enum PeerReportOption { case spam case violence case copyright - case pornoghraphy + case pornography case childAbuse case other } -public func presentPeerReportOptions(context: AccountContext, parent: ViewController, contextController: ContextController?, subject: PeerReportSubject, completion: @escaping (Bool) -> Void) { +public func presentPeerReportOptions(context: AccountContext, parent: ViewController, contextController: ContextController?, subject: PeerReportSubject, options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .other], completion: @escaping (Bool) -> Void) { if let contextController = contextController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let options: [PeerReportOption] = [ - .spam, - .violence, - .pornoghraphy, - .childAbuse, - .copyright, - .other - ] var items: [ContextMenuItem] = [] for option in options { let title: String @@ -47,7 +39,7 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro title = presentationData.strings.ReportPeer_ReasonSpam case .violence: title = presentationData.strings.ReportPeer_ReasonViolence - case .pornoghraphy: + case .pornography: title = presentationData.strings.ReportPeer_ReasonPornography case .childAbuse: title = presentationData.strings.ReportPeer_ReasonChildAbuse @@ -67,7 +59,7 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro reportReason = .spam case .violence: reportReason = .violence - case .pornoghraphy: + case .pornography: reportReason = .porno case .childAbuse: reportReason = .childAbuse @@ -109,19 +101,10 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro } } -public func peerReportOptionsController(context: AccountContext, subject: PeerReportSubject, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void, completion: @escaping (Bool) -> Void) -> ViewController { +public func peerReportOptionsController(context: AccountContext, subject: PeerReportSubject, options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .other], present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void, completion: @escaping (Bool) -> Void) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme)) - - let options: [PeerReportOption] = [ - .spam, - .violence, - .pornoghraphy, - .childAbuse, - .copyright, - .other - ] - + let controller = ActionSheetController(theme: ActionSheetControllerTheme(presentationData: presentationData)) + var items: [ActionSheetItem] = [] for option in options { let title: String @@ -131,7 +114,7 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe title = presentationData.strings.ReportPeer_ReasonSpam case .violence: title = presentationData.strings.ReportPeer_ReasonViolence - case .pornoghraphy: + case .pornography: title = presentationData.strings.ReportPeer_ReasonPornography case .childAbuse: title = presentationData.strings.ReportPeer_ReasonChildAbuse @@ -147,7 +130,7 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe reportReason = .spam case .violence: reportReason = .violence - case .pornoghraphy: + case .pornography: reportReason = .porno case .childAbuse: reportReason = .childAbuse @@ -254,11 +237,11 @@ private enum PeerReportControllerEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! PeerReportControllerArguments switch self { case let .text(theme, title, value): - return ItemListMultilineInputItem(theme: theme, text: value, placeholder: title, maxLength: nil, sectionId: self.section, style: .blocks, textUpdated: { text in + return ItemListMultilineInputItem(presentationData: presentationData, text: value, placeholder: title, maxLength: nil, sectionId: self.section, style: .blocks, textUpdated: { text in arguments.updateText(text) }, tag: PeerReportControllerEntryTag.text) } @@ -338,11 +321,11 @@ private func peerReportController(context: AccountContext, subject: PeerReportSu }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ReportPeer_ReasonOther_Title), leftNavigationButton: ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ReportPeer_ReasonOther_Title), leftNavigationButton: ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() completion(false) }), rightNavigationButton: rightButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: peerReportControllerEntries(presentationData: presentationData, state: state), style: .blocks, focusItemTag: PeerReportControllerEntryTag.text) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: peerReportControllerEntries(presentationData: presentationData, state: state), style: .blocks, focusItemTag: PeerReportControllerEntryTag.text) return (controllerState, (listState, arguments)) } diff --git a/submodules/PeerInfoUI/Sources/PhoneLabelController.swift b/submodules/PeerInfoUI/Sources/PhoneLabelController.swift index ef3da80093..d0e1659679 100644 --- a/submodules/PeerInfoUI/Sources/PhoneLabelController.swift +++ b/submodules/PeerInfoUI/Sources/PhoneLabelController.swift @@ -57,11 +57,11 @@ private enum PhoneLabelEntry: ItemListNodeEntry { return lhs.index < rhs.index } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! PhoneLabelArguments switch self { case let .label(_, theme, value, text, selected): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.selectLabel(value) }) } @@ -120,8 +120,8 @@ public func phoneLabelController(context: AccountContext, currentLabel: String, arguments.complete() }) - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.PhoneLabel_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: phoneLabelEntries(presentationData: presentationData, state: state), style: .blocks) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PhoneLabel_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: phoneLabelEntries(presentationData: presentationData, state: state), style: .blocks) return (controllerState, (listState, arguments)) } diff --git a/submodules/PeerInfoUI/Sources/SecretChatKeyController.swift b/submodules/PeerInfoUI/Sources/SecretChatKeyController.swift index baec51f9b6..fb309cd499 100644 --- a/submodules/PeerInfoUI/Sources/SecretChatKeyController.swift +++ b/submodules/PeerInfoUI/Sources/SecretChatKeyController.swift @@ -8,7 +8,7 @@ import Postbox import TelegramPresentationData import AccountContext -final class SecretChatKeyController: ViewController { +public final class SecretChatKeyController: ViewController { private var controllerNode: SecretChatKeyControllerNode { return self.displayNode as! SecretChatKeyControllerNode } @@ -19,7 +19,7 @@ final class SecretChatKeyController: ViewController { private var presentationData: PresentationData - init(context: AccountContext, fingerprint: SecretChatKeyFingerprint, peer: Peer) { + public init(context: AccountContext, fingerprint: SecretChatKeyFingerprint, peer: Peer) { self.context = context self.fingerprint = fingerprint self.peer = peer @@ -34,11 +34,11 @@ final class SecretChatKeyController: ViewController { self.title = self.presentationData.strings.EncryptionKey_Title } - required init(coder aDecoder: NSCoder) { + required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func loadDisplayNode() { + override public func loadDisplayNode() { self.displayNode = SecretChatKeyControllerNode(context: self.context, presentationData: self.presentationData, fingerprint: self.fingerprint, peer: self.peer, getNavigationController: { [weak self] in return self?.navigationController as? NavigationController }) diff --git a/submodules/PeerInfoUI/Sources/UserInfoController.swift b/submodules/PeerInfoUI/Sources/UserInfoController.swift index 8497888085..8603e1295e 100644 --- a/submodules/PeerInfoUI/Sources/UserInfoController.swift +++ b/submodules/PeerInfoUI/Sources/UserInfoController.swift @@ -27,9 +27,10 @@ import NotificationSoundSelectionUI import Markdown import LocalizedPeerData import PhoneNumberFormat +import TelegramIntents private final class UserInfoControllerArguments { - let account: Account + let context: AccountContext let avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void let tapAvatarAction: () -> Void @@ -58,8 +59,8 @@ private final class UserInfoControllerArguments { let botPrivacy: () -> Void let report: () -> Void - init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, tapAvatarAction: @escaping () -> Void, openChat: @escaping () -> Void, addContact: @escaping () -> Void, shareContact: @escaping () -> Void, shareMyContact: @escaping () -> Void, startSecretChat: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void, displayCopyContextMenu: @escaping (UserInfoEntryTag, String) -> Void, call: @escaping () -> Void, openCallMenu: @escaping (String) -> Void, requestPhoneNumber: @escaping () -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void, displayAboutContextMenu: @escaping (String) -> Void, openEncryptionKey: @escaping (SecretChatKeyFingerprint) -> Void, addBotToGroup: @escaping () -> Void, shareBot: @escaping () -> Void, botSettings: @escaping () -> Void, botHelp: @escaping () -> Void, botPrivacy: @escaping () -> Void, report: @escaping () -> Void) { - self.account = account + init(context: AccountContext, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, tapAvatarAction: @escaping () -> Void, openChat: @escaping () -> Void, addContact: @escaping () -> Void, shareContact: @escaping () -> Void, shareMyContact: @escaping () -> Void, startSecretChat: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void, displayCopyContextMenu: @escaping (UserInfoEntryTag, String) -> Void, call: @escaping () -> Void, openCallMenu: @escaping (String) -> Void, requestPhoneNumber: @escaping () -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void, displayAboutContextMenu: @escaping (String) -> Void, openEncryptionKey: @escaping (SecretChatKeyFingerprint) -> Void, addBotToGroup: @escaping () -> Void, shareBot: @escaping () -> Void, botSettings: @escaping () -> Void, botHelp: @escaping () -> Void, botPrivacy: @escaping () -> Void, report: @escaping () -> Void) { + self.context = context self.avatarAndNameInfoContext = avatarAndNameInfoContext self.updateEditingName = updateEditingName self.tapAvatarAction = tapAvatarAction @@ -393,11 +394,11 @@ private enum UserInfoEntry: ItemListNodeEntry { return lhs.sortIndex < rhs.sortIndex } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! UserInfoControllerArguments switch self { case let .info(theme, strings, dateTimeFormat, peer, presence, cachedData, state, displayCall): - return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: presence, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in + return ItemListAvatarAndNameInfoItem(accountContext: arguments.context, presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: presence, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { arguments.tapAvatarAction() @@ -405,95 +406,95 @@ private enum UserInfoEntry: ItemListNodeEntry { arguments.call() } : nil) case let .calls(theme, strings, dateTimeFormat, messages): - return ItemListCallListItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, messages: messages, sectionId: self.section, style: .plain) + return ItemListCallListItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, messages: messages, sectionId: self.section, style: .plain) case let .about(theme, peer, text, value): var enabledEntityTypes: EnabledEntityTypes = [] if let peer = peer as? TelegramUser, let _ = peer.botInfo { enabledEntityTypes = [.url, .mention, .hashtag] } - return ItemListTextWithLabelItem(theme: theme, label: text, text: foldMultipleLineBreaks(value), enabledEntityTypes: enabledEntityTypes, multiline: true, sectionId: self.section, action: nil, longTapAction: { + return ItemListTextWithLabelItem(presentationData: presentationData, label: text, text: foldMultipleLineBreaks(value), enabledEntityTypes: enabledEntityTypes, multiline: true, sectionId: self.section, action: nil, longTapAction: { arguments.displayAboutContextMenu(value) }, linkItemAction: { action, itemLink in arguments.aboutLinkAction(action, itemLink) }, tag: UserInfoEntryTag.about) case let .phoneNumber(theme, _, label, value, isMain): - return ItemListTextWithLabelItem(theme: theme, label: label, text: value, textColor: isMain ? .highlighted : .accent, enabledEntityTypes: [], multiline: false, sectionId: self.section, action: { + return ItemListTextWithLabelItem(presentationData: presentationData, label: label, text: value, textColor: isMain ? .highlighted : .accent, enabledEntityTypes: [], multiline: false, sectionId: self.section, action: { arguments.openCallMenu(value) }, longTapAction: { arguments.displayCopyContextMenu(.phoneNumber, value) }, tag: UserInfoEntryTag.phoneNumber) case let .requestPhoneNumber(theme, label, value): - return ItemListTextWithLabelItem(theme: theme, label: label, text: value, textColor: .accent, enabledEntityTypes: [], multiline: false, sectionId: self.section, action: { + return ItemListTextWithLabelItem(presentationData: presentationData, label: label, text: value, textColor: .accent, enabledEntityTypes: [], multiline: false, sectionId: self.section, action: { arguments.requestPhoneNumber() }) case let .userName(theme, text, value): - return ItemListTextWithLabelItem(theme: theme, label: text, text: "@\(value)", textColor: .accent, enabledEntityTypes: [], multiline: false, sectionId: self.section, action: { + return ItemListTextWithLabelItem(presentationData: presentationData, label: text, text: "@\(value)", textColor: .accent, enabledEntityTypes: [], multiline: false, sectionId: self.section, action: { arguments.displayUsernameContextMenu("@\(value)") }, longTapAction: { arguments.displayCopyContextMenu(.username, "@\(value)") }, tag: UserInfoEntryTag.username) case let .sendMessage(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.openChat() }) case let .addContact(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.addContact() }) case let .shareContact(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.shareContact() }) case let .shareMyContact(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.shareMyContact() }) case let .startSecretChat(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.startSecretChat() }) case let .sharedMedia(theme, text): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .plain, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .plain, action: { arguments.openSharedMedia() }) case let .notifications(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .plain, action: { arguments.changeNotificationMuteSettings() }) case let .groupsInCommon(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .plain, action: { arguments.openGroupsInCommon() }) case let .secretEncryptionKey(theme, text, fingerprint): - return ItemListSecretChatKeyItem(theme: theme, title: text, fingerprint: fingerprint, sectionId: self.section, style: .plain, action: { + return ItemListSecretChatKeyItem(presentationData: presentationData, title: text, fingerprint: fingerprint, sectionId: self.section, style: .plain, action: { arguments.openEncryptionKey(fingerprint) }) case let .botAddToGroup(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.addBotToGroup() }) case let .botShare(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.shareBot() }) case let .botSettings(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.botSettings() }) case let .botHelp(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.botHelp() }) case let .botPrivacy(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.botPrivacy() }) case let .botReport(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.report() }) case let .block(theme, text, action): - return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { switch action { case .block: arguments.updatePeerBlocked(true) @@ -633,7 +634,7 @@ private func userInfoEntries(account: Account, presentationData: PresentationDat } } if !found { - entries.append(UserInfoEntry.phoneNumber(presentationData.theme, index, "home", formattedNumber, false)) + entries.append(UserInfoEntry.phoneNumber(presentationData.theme, index, presentationData.strings.ContactInfo_PhoneLabelMobile, formattedNumber, false)) index += 1 } else { for (label, number, isMain) in phoneNumbers { @@ -892,7 +893,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe }) } - let arguments = UserInfoControllerArguments(account: context.account, avatarAndNameInfoContext: avatarAndNameInfoContext, updateEditingName: { editingName in + let arguments = UserInfoControllerArguments(context: context, avatarAndNameInfoContext: avatarAndNameInfoContext, updateEditingName: { editingName in updateState { state in if let _ = state.editingState { return state.withUpdatedEditingState(UserInfoEditingState(editingName: editingName)) @@ -990,7 +991,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe } else { if value { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -1026,6 +1027,8 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe } else if reportSpam { let _ = reportPeer(account: context.account, peerId: peerId, reason: .spam).start() } + + deleteSendMessageIntents(peerId: peerId) }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) @@ -1046,7 +1049,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe }) }, deleteContact: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -1094,6 +1097,8 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe |> deliverOnMainQueue).start(completed: { dismissImpl?() })) + + deleteSendMessageIntents(peerId: peerId) }) }) ]), @@ -1112,7 +1117,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe |> deliverOnMainQueue).start(next: { peer, _ in if let peer = peer as? TelegramUser, let peerPhoneNumber = peer.phone, formatPhoneNumber(number) == formatPhoneNumber(peerPhoneNumber) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -1281,7 +1286,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe var signals: [Signal] = [] if let contactDataManager = context.sharedContext.contactDataManager { for (id, basicData) in records { - signals.append(contactDataManager.appendContactData(DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: firstName, lastName: lastName, phoneNumbers: basicData.phoneNumbers), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: []), to: id)) + signals.append(contactDataManager.appendContactData(DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: firstName, lastName: lastName, phoneNumbers: basicData.phoneNumbers), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: ""), to: id)) } } return combineLatest(signals) @@ -1304,8 +1309,8 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.UserInfo_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: nil) - let listState = ItemListNodeState(entries: userInfoEntries(account: context.account, presentationData: presentationData, view: view.0, cachedPeerData: view.1, deviceContacts: deviceContacts, mode: mode, state: state, peerChatState: (combinedView.views[.peerChatState(peerId: peerId)] as? PeerChatStateView)?.chatState, globalNotificationSettings: globalNotificationSettings), style: .plain) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.UserInfo_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: nil) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: userInfoEntries(account: context.account, presentationData: presentationData, view: view.0, cachedPeerData: view.1, deviceContacts: deviceContacts, mode: mode, state: state, peerChatState: (combinedView.views[.peerChatState(peerId: peerId)] as? PeerChatStateView)?.chatState, globalNotificationSettings: globalNotificationSettings), style: .plain) return (controllerState, (listState, arguments)) } @@ -1445,7 +1450,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe presentControllerImpl?(c, a) }, dismissInput: { dismissInputImpl?() - }) + }, contentContext: nil) } shareBotImpl = { [weak controller] in let _ = (getUserPeer(postbox: context.account.postbox, peerId: peerId) @@ -1458,7 +1463,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe } avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { - var result: ((ASDisplayNode, () -> (UIView?, UIView?)), CGRect)? + var result: ((ASDisplayNode, CGRect, () -> (UIView?, UIView?)), CGRect)? controller.forEachItemNode { itemNode in if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { result = itemNode.avatarTransitionNode() @@ -1584,7 +1589,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe let presentationData = context.sharedContext.currentPresentationData.with { $0 } let text: String = presentationData.strings.UserInfo_TapToCall - let tooltipController = TooltipController(content: .text(text), dismissByTapOutside: true) + let tooltipController = TooltipController(content: .text(text), baseFontSize: presentationData.listsFontSize.baseDisplaySize, dismissByTapOutside: true) controller.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in if let resultItemNode = resultItemNode { return (resultItemNode, callButtonFrame) diff --git a/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneActionItem.swift b/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneActionItem.swift index 1901973af6..2a81752ebf 100644 --- a/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneActionItem.swift +++ b/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneActionItem.swift @@ -8,13 +8,13 @@ import ItemListUI import PresentationDataUtils class UserInfoEditingPhoneActionItem: ListViewItem, ItemListItem { - let theme: PresentationTheme + let presentationData: ItemListPresentationData let title: String let sectionId: ItemListSectionId let action: () -> Void - init(theme: PresentationTheme, title: String, sectionId: ItemListSectionId, action: @escaping () -> Void, tag: Any? = nil) { - self.theme = theme + init(presentationData: ItemListPresentationData, title: String, sectionId: ItemListSectionId, action: @escaping () -> Void, tag: Any? = nil) { + self.presentationData = presentationData self.title = title self.sectionId = sectionId self.action = action @@ -61,8 +61,6 @@ class UserInfoEditingPhoneActionItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(15.0) - class UserInfoEditingPhoneActionItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -114,15 +112,17 @@ class UserInfoEditingPhoneActionItemNode: ListViewItemNode { let currentItem = self.item return { item, params, neighbors in + let titleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } - let textColor = item.theme.list.itemAccentColor + let textColor = item.presentationData.theme.list.itemAccentColor - let iconImage = PresentationResourcesItemList.addPhoneIcon(item.theme) + let iconImage = PresentationResourcesItemList.addPhoneIcon(item.presentationData.theme) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -133,9 +133,9 @@ class UserInfoEditingPhoneActionItemNode: ListViewItemNode { let itemBackgroundColor: UIColor let itemSeparatorColor: UIColor - itemBackgroundColor = item.theme.list.plainBackgroundColor - itemSeparatorColor = item.theme.list.itemPlainSeparatorColor - contentSize = CGSize(width: params.width, height: 44.0) + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: 22.0 + titleLayout.size.height) insets = itemListNeighborsPlainInsets(neighbors) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -148,7 +148,7 @@ class UserInfoEditingPhoneActionItemNode: ListViewItemNode { strongSelf.topStripeNode.backgroundColor = itemSeparatorColor strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } strongSelf.iconNode.image = iconImage diff --git a/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneItem.swift b/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneItem.swift index cc173f5780..0c27148ed1 100644 --- a/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneItem.swift +++ b/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneItem.swift @@ -19,8 +19,7 @@ struct UserInfoEditingPhoneItemEditing { } class UserInfoEditingPhoneItem: ListViewItem, ItemListItem { - let theme: PresentationTheme - let strings: PresentationStrings + let presentationData: ItemListPresentationData let id: Int64 let label: String let value: String @@ -32,9 +31,8 @@ class UserInfoEditingPhoneItem: ListViewItem, ItemListItem { let delete: () -> Void let tag: ItemListItemTag? - init(theme: PresentationTheme, strings: PresentationStrings, id: Int64, label: String, value: String, editing: UserInfoEditingPhoneItemEditing, sectionId: ItemListSectionId, setPhoneIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, updated: @escaping (String) -> Void, selectLabel: (() -> Void)?, delete: @escaping () -> Void, tag: ItemListItemTag?) { - self.theme = theme - self.strings = strings + init(presentationData: ItemListPresentationData, id: Int64, label: String, value: String, editing: UserInfoEditingPhoneItemEditing, sectionId: ItemListSectionId, setPhoneIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, updated: @escaping (String) -> Void, selectLabel: (() -> Void)?, delete: @escaping () -> Void, tag: ItemListItemTag?) { + self.presentationData = presentationData self.id = id self.label = label self.value = value @@ -83,8 +81,6 @@ class UserInfoEditingPhoneItem: ListViewItem, ItemListItem { var selectable: Bool = false } -private let titleFont = Font.regular(15.0) - class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, ItemListItemFocusableNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -184,9 +180,11 @@ class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode, ItemListItemN super.didLoad() if let item = self.item { - self.phoneNode.numberField?.textField.textColor = item.theme.list.itemPrimaryTextColor - self.phoneNode.numberField?.textField.keyboardAppearance = item.theme.rootController.keyboardColor.keyboardAppearance - self.phoneNode.numberField?.textField.tintColor = item.theme.list.itemAccentColor + self.phoneNode.numberField?.textField.textColor = item.presentationData.theme.list.itemPrimaryTextColor + self.phoneNode.numberField?.textField.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance + self.phoneNode.numberField?.textField.tintColor = item.presentationData.theme.list.itemAccentColor + let titleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + self.phoneNode.numberField?.textField.font = titleFont } } @@ -197,15 +195,17 @@ class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode, ItemListItemN let currentItem = self.item return { item, params, neighbors in + let titleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } - let controlSizeAndApply = editableControlLayout(44.0, item.theme, false) + let controlSizeAndApply = editableControlLayout(item.presentationData.theme, false) - let textColor = item.theme.list.itemAccentColor + let textColor = item.presentationData.theme.list.itemAccentColor let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: titleFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -216,9 +216,9 @@ class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode, ItemListItemN let itemBackgroundColor: UIColor let itemSeparatorColor: UIColor - itemBackgroundColor = item.theme.list.plainBackgroundColor - itemSeparatorColor = item.theme.list.itemPlainSeparatorColor - contentSize = CGSize(width: params.width, height: 44.0) + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: 22.0 + labelLayout.size.height) insets = itemListNeighborsPlainInsets(neighbors) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -236,7 +236,8 @@ class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode, ItemListItemN strongSelf.phoneNode.numberField?.textField.textColor = updatedTheme.list.itemPrimaryTextColor strongSelf.phoneNode.numberField?.textField.keyboardAppearance = updatedTheme.rootController.keyboardColor.keyboardAppearance - strongSelf.phoneNode.numberField?.textField.tintColor = item.theme.list.itemAccentColor + strongSelf.phoneNode.numberField?.textField.tintColor = item.presentationData.theme.list.itemAccentColor + strongSelf.phoneNode.numberField?.textField.font = titleFont strongSelf.clearButton.setImage(generateClearIcon(color: updatedTheme.list.inputClearButtonColor), for: []) } @@ -261,8 +262,8 @@ class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode, ItemListItemN strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) - let _ = controlSizeAndApply.1() - let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + 4.0 + revealOffset, y: 0.0), size: controlSizeAndApply.0) + let _ = controlSizeAndApply.1(layout.contentSize.height) + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + 4.0 + revealOffset, y: 0.0), size: CGSize(width: controlSizeAndApply.0, height: layout.contentSize.height)) strongSelf.editableControlNode.frame = editableControlFrame let labelFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset + 30.0, y: 12.0), size: labelLayout.size) @@ -283,7 +284,7 @@ class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode, ItemListItemN strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)])) } }) } diff --git a/submodules/PeersNearbyUI/BUCK b/submodules/PeersNearbyUI/BUCK index 5a6ca7965b..b8e216e8a7 100644 --- a/submodules/PeersNearbyUI/BUCK +++ b/submodules/PeersNearbyUI/BUCK @@ -26,6 +26,9 @@ static_library( "//submodules/PeersNearbyIconNode:PeersNearbyIconNode", "//submodules/Geocoding:Geocoding", "//submodules/AppBundle:AppBundle", + "//submodules/AnimatedStickerNode:AnimatedStickerNode", + "//submodules/TelegramStringFormatting:TelegramStringFormatting", + "//submodules/TelegramNotices:TelegramNotices", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift index d5dfe4cf05..1c8e880f0f 100644 --- a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift +++ b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift @@ -20,6 +20,11 @@ import TelegramPermissionsUI import ItemListPeerActionItem import Geocoding import AppBundle +import ContextUI +import TelegramNotices +import TelegramStringFormatting + +private let maxUsersDisplayedLimit: Int32 = 5 private struct PeerNearbyEntry { let peer: (Peer, CachedPeerData?) @@ -49,13 +54,21 @@ private func arePeerNearbyArraysEqual(_ lhs: [PeerNearbyEntry], _ rhs: [PeerNear private final class PeersNearbyControllerArguments { let context: AccountContext + let toggleVisibility: (Bool) -> Void + let openProfile: (Peer) -> Void let openChat: (Peer) -> Void let openCreateGroup: (Double, Double, String?) -> Void + let contextAction: (Peer, ASDisplayNode, ContextGesture?) -> Void + let expandUsers: () -> Void - init(context: AccountContext, openChat: @escaping (Peer) -> Void, openCreateGroup: @escaping (Double, Double, String?) -> Void) { + init(context: AccountContext, toggleVisibility: @escaping (Bool) -> Void, openProfile: @escaping (Peer) -> Void, openChat: @escaping (Peer) -> Void, openCreateGroup: @escaping (Double, Double, String?) -> Void, contextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, expandUsers: @escaping () -> Void) { self.context = context + self.toggleVisibility = toggleVisibility + self.openProfile = openProfile self.openChat = openChat self.openCreateGroup = openCreateGroup + self.contextAction = contextAction + self.expandUsers = expandUsers } } @@ -71,7 +84,9 @@ private enum PeersNearbyEntry: ItemListNodeEntry { case usersHeader(PresentationTheme, String, Bool) case empty(PresentationTheme, String) + case visibility(PresentationTheme, String, Bool) case user(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PeerNearbyEntry) + case expand(PresentationTheme, String) case groupsHeader(PresentationTheme, String, Bool) case createGroup(PresentationTheme, String, Double?, Double?, String?) @@ -84,7 +99,7 @@ private enum PeersNearbyEntry: ItemListNodeEntry { switch self { case .header: return PeersNearbySection.header.rawValue - case .usersHeader, .empty, .user: + case .usersHeader, .empty, .visibility, .user, .expand: return PeersNearbySection.users.rawValue case .groupsHeader, .createGroup, .group: return PeersNearbySection.groups.rawValue @@ -101,14 +116,18 @@ private enum PeersNearbyEntry: ItemListNodeEntry { return 1 case .empty: return 2 + case .visibility: + return 3 case let .user(index, _, _, _, _, _): - return 3 + index - case .groupsHeader: + return 4 + index + case .expand: return 1000 - case .createGroup: + case .groupsHeader: return 1001 + case .createGroup: + return 1002 case let .group(index, _, _, _, _, _): - return 1002 + index + return 1003 + index case .channelsHeader: return 2000 case let .channel(index, _, _, _, _, _): @@ -136,12 +155,25 @@ private enum PeersNearbyEntry: ItemListNodeEntry { } else { return false } + case let .visibility(lhsTheme, lhsText, lhsStop): + if case let .visibility(rhsTheme, rhsText, rhsStop) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsStop == rhsStop { + return true + } else { + return false + } + case let .user(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsDisplayOrder, lhsPeer): if case let .user(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsDisplayOrder, rhsPeer) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsDisplayOrder == rhsDisplayOrder, arePeersNearbyEqual(lhsPeer, rhsPeer) { return true } else { return false } + case let .expand(lhsTheme, lhsText): + if case let .expand(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .groupsHeader(lhsTheme, lhsText, lhsLoading): if case let .groupsHeader(rhsTheme, rhsText, rhsLoading) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsLoading == rhsLoading { return true @@ -190,23 +222,38 @@ private enum PeersNearbyEntry: ItemListNodeEntry { return result } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! PeersNearbyControllerArguments switch self { case let .header(theme, text): return PeersNearbyHeaderItem(theme: theme, text: text, sectionId: self.section) case let .usersHeader(theme, text, loading): - return ItemListSectionHeaderItem(theme: theme, text: text, activityIndicator: loading ? .left : .none, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, activityIndicator: loading ? .left : .none, sectionId: self.section) case let .empty(theme, text): return ItemListPlaceholderItem(theme: theme, text: text, sectionId: self.section, style: .blocks) + case let .visibility(theme, title, stop): + return ItemListPeerActionItem(presentationData: presentationData, icon: stop ? PresentationResourcesItemList.makeInvisibleIcon(theme) : PresentationResourcesItemList.makeVisibleIcon(theme), title: title, alwaysPlain: false, sectionId: self.section, color: stop ? .destructive : .accent, editing: false, action: { + arguments.toggleVisibility(!stop) + }) case let .user(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer): - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.context.account, peer: peer.peer.0, aliasHandling: .standard, nameColor: .primary, nameStyle: .distinctBold, presence: nil, text: .text(strings.Map_DistanceAway(stringForDistance(peer.distance)).0), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { - arguments.openChat(peer.peer.0) - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, hasTopGroupInset: false, tag: nil) + var text = strings.Map_DistanceAway(stringForDistance(peer.distance)).0 + let isSelfPeer = peer.peer.0.id == arguments.context.account.peerId + if isSelfPeer { + text = strings.PeopleNearby_VisibleUntil(humanReadableStringForTimestamp(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: peer.expires)).0 + } + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer.peer.0, aliasHandling: .standard, nameColor: .primary, nameStyle: .distinctBold, presence: nil, text: .text(text), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: !isSelfPeer, sectionId: self.section, action: { + if !isSelfPeer { + arguments.openProfile(peer.peer.0) + } + }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, contextAction: nil, hasTopGroupInset: false, tag: nil) + case let .expand(theme, title): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: { + arguments.expandUsers() + }) case let .groupsHeader(theme, text, loading): - return ItemListSectionHeaderItem(theme: theme, text: text, activityIndicator: loading ? .left : .none, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, activityIndicator: loading ? .left : .none, sectionId: self.section) case let .createGroup(theme, title, latitude, longitude, address): - return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.createGroupIcon(theme), title: title, alwaysPlain: false, sectionId: self.section, editing: false, action: { + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.createGroupIcon(theme), title: title, alwaysPlain: false, sectionId: self.section, editing: false, action: { if let latitude = latitude, let longitude = longitude { arguments.openCreateGroup(latitude, longitude, address) } @@ -218,11 +265,13 @@ private enum PeersNearbyEntry: ItemListNodeEntry { } else { text = .text(strings.Map_DistanceAway(stringForDistance(peer.distance)).0) } - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.context.account, peer: peer.peer.0, aliasHandling: .standard, nameColor: .primary, nameStyle: .distinctBold, presence: nil, text: text, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer.peer.0, aliasHandling: .standard, nameColor: .primary, nameStyle: .distinctBold, presence: nil, text: text, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { arguments.openChat(peer.peer.0) - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, hasTopGroupInset: false, tag: nil) + }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, contextAction: { node, gesture in + arguments.contextAction(peer.peer.0, node, gesture) + }, hasTopGroupInset: false, tag: nil) case let .channelsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .channel(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer): var text: ItemListPeerItemText if let cachedData = peer.peer.1 as? CachedChannelData, let memberCount = cachedData.participantsSummary.memberCount { @@ -230,9 +279,11 @@ private enum PeersNearbyEntry: ItemListNodeEntry { } else { text = .text(strings.Map_DistanceAway(stringForDistance(peer.distance)).0) } - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.context.account, peer: peer.peer.0, aliasHandling: .standard, nameColor: .primary, nameStyle: .distinctBold, presence: nil, text: text, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer.peer.0, aliasHandling: .standard, nameColor: .primary, nameStyle: .distinctBold, presence: nil, text: text, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { arguments.openChat(peer.peer.0) - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, hasTopGroupInset: false, tag: nil) + }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, contextAction: { node, gesture in + arguments.contextAction(peer.peer.0, node, gesture) + }, hasTopGroupInset: false, tag: nil) } } } @@ -241,37 +292,57 @@ private struct PeersNearbyData: Equatable { let latitude: Double let longitude: Double let address: String? + let visible: Bool + let accountPeerId: PeerId let users: [PeerNearbyEntry] let groups: [PeerNearbyEntry] let channels: [PeerNearbyEntry] - init(latitude: Double, longitude: Double, address: String?, users: [PeerNearbyEntry], groups: [PeerNearbyEntry], channels: [PeerNearbyEntry]) { + init(latitude: Double, longitude: Double, address: String?, visible: Bool, accountPeerId: PeerId, users: [PeerNearbyEntry], groups: [PeerNearbyEntry], channels: [PeerNearbyEntry]) { self.latitude = latitude self.longitude = longitude self.address = address + self.visible = visible + self.accountPeerId = accountPeerId self.users = users self.groups = groups self.channels = channels } static func ==(lhs: PeersNearbyData, rhs: PeersNearbyData) -> Bool { - return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude && lhs.address == rhs.address && arePeerNearbyArraysEqual(lhs.users, rhs.users) && arePeerNearbyArraysEqual(lhs.groups, rhs.groups) && arePeerNearbyArraysEqual(lhs.channels, rhs.channels) + return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude && lhs.address == rhs.address && lhs.visible == rhs.visible && lhs.accountPeerId == rhs.accountPeerId && arePeerNearbyArraysEqual(lhs.users, rhs.users) && arePeerNearbyArraysEqual(lhs.groups, rhs.groups) && arePeerNearbyArraysEqual(lhs.channels, rhs.channels) } } -private func peersNearbyControllerEntries(data: PeersNearbyData?, presentationData: PresentationData, displayLoading: Bool) -> [PeersNearbyEntry] { +private func peersNearbyControllerEntries(data: PeersNearbyData?, state: PeersNearbyState, presentationData: PresentationData, displayLoading: Bool, expanded: Bool) -> [PeersNearbyEntry] { var entries: [PeersNearbyEntry] = [] - entries.append(.header(presentationData.theme, presentationData.strings.PeopleNearby_Description)) + entries.append(.header(presentationData.theme, presentationData.strings.PeopleNearby_DiscoverDescription)) entries.append(.usersHeader(presentationData.theme, presentationData.strings.PeopleNearby_Users.uppercased(), displayLoading && data == nil)) + + let visible = state.visibilityExpires != nil + entries.append(.visibility(presentationData.theme, visible ? presentationData.strings.PeopleNearby_MakeInvisible : presentationData.strings.PeopleNearby_MakeVisible, visible)) + if let data = data, !data.users.isEmpty { var i: Int32 = 0 - for user in data.users { - entries.append(.user(i, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, user)) - i += 1 + var users = data.users + var effectiveExpanded = expanded + if users.count > maxUsersDisplayedLimit && !expanded { + users = Array(users.prefix(Int(maxUsersDisplayedLimit))) + } else { + effectiveExpanded = true + } + + for user in users { + if user.peer.0.id != data.accountPeerId { + entries.append(.user(i, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, user)) + i += 1 + } + } + + if !effectiveExpanded { + entries.append(.expand(presentationData.theme, presentationData.strings.PeopleNearby_ShowMorePeople(Int32(data.users.count) - maxUsersDisplayedLimit))) } - } else { - entries.append(.empty(presentationData.theme, presentationData.strings.PeopleNearby_UsersEmpty)) } entries.append(.groupsHeader(presentationData.theme, presentationData.strings.PeopleNearby_Groups.uppercased(), displayLoading && data == nil)) @@ -295,11 +366,94 @@ private func peersNearbyControllerEntries(data: PeersNearbyData?, presentationDa return entries } +private final class ContextControllerContentSourceImpl: ContextControllerContentSource { + let controller: ViewController + weak var sourceNode: ASDisplayNode? + + let navigationController: NavigationController? = nil + + let passthroughTouches: Bool = true + + 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 func peerNearbyContextMenuItems(context: AccountContext, peerId: PeerId, present: @escaping (ViewController) -> Void) -> Signal<[ContextMenuItem], NoError> { + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + return context.account.postbox.transaction { transaction -> [ContextMenuItem] in + var items: [ContextMenuItem] = [] +// +// let peer = transaction.getPeer(peerId) +// +// if let peer = peer as? TelegramUser { +// items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChatList_Context_AddToContacts, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { _, f in +// f(.default) +// }))) +// } else { +// items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeopleNearby_Context_JoinGroup, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { _, f in +// let _ = (joinChannel(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(next: { participant in +// f(.default) +// }, error: { error in +//// if let strongSelf = self { +//// if case .tooMuchJoined = error { +//// if let parentNavigationController = strongSelf.parentNavigationController { +//// let context = strongSelf.context +//// let link = strongSelf.link +//// let navigateToPeer = strongSelf.navigateToPeer +//// let resolvedState = strongSelf.resolvedState +//// parentNavigationController.pushViewController(oldChannelsController(context: strongSelf.context, intent: .join, completed: { [weak parentNavigationController] value in +//// if value { +//// (parentNavigationController?.viewControllers.last as? ViewController)?.present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: navigateToPeer, parentNavigationController: parentNavigationController, resolvedState: resolvedState), in: .window(.root)) +//// } +//// })) +//// } else { +//// strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Join_ChannelsTooMuch, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) +//// } +//// strongSelf.dismiss() +//// } +//// } +// }) +// }))) +// +// items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeopleNearby_Context_UnrelatedLocation, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor) }, action: { _, f in +// let _ = (TelegramCore.reportPeer(account: context.account, peerId: peerId, reason: .irrelevantLocation) +// |> deliverOnMainQueue).start(completed: { +// let _ = ApplicationSpecificNotice.setIrrelevantPeerGeoReport(postbox: context.account.postbox, peerId: peerId).start() +// +// present(textAlertController(context: context, title: nil, text: presentationData.strings.ReportPeer_AlertSuccess, actions: [TextAlertAction(type: TextAlertActionType.defaultAction, title: presentationData.strings.Common_OK, action: {})])) +// }) +// f(.default) +// }))) +// } + + return items + } +} + + public func peersNearbyController(context: AccountContext) -> ViewController { var pushControllerImpl: ((ViewController) -> Void)? var replaceAllButRootControllerImpl: ((ViewController, Bool) -> Void)? var replaceTopControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var presentInGlobalOverlayImpl: ((ViewController) -> Void)? + var navigateToProfileImpl: ((Peer) -> Void)? var navigateToChatImpl: ((Peer) -> Void)? let actionsDisposable = DisposableSet() @@ -308,8 +462,31 @@ public func peersNearbyController(context: AccountContext) -> ViewController { let dataPromise = Promise(nil) let addressPromise = Promise(nil) + let expandedPromise = ValuePromise(false) - let arguments = PeersNearbyControllerArguments(context: context, openChat: { peer in + let coordinatePromise = Promise(nil) + coordinatePromise.set(.single(nil) |> then(currentLocationManagerCoordinate(manager: context.sharedContext.locationManager!, timeout: 5.0))) + + let arguments = PeersNearbyControllerArguments(context: context, toggleVisibility: { visible in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + if visible { + presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.PeopleNearby_MakeVisibleTitle, text: presentationData.strings.PeopleNearby_MakeVisibleDescription, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + let _ = (coordinatePromise.get() + |> deliverOnMainQueue).start(next: { coordinate in + if let coordinate = coordinate { + let _ = peersNearbyUpdateVisibility(account: context.account, update: .visible(latitude: coordinate.latitude, longitude: coordinate.longitude), background: false).start() + } + }) + })]), nil) + + + } else { + let _ = peersNearbyUpdateVisibility(account: context.account, update: .invisible, background: false).start() + } + }, openProfile: { peer in + navigateToProfileImpl?(peer) + }, openChat: { peer in navigateToChatImpl?(peer) }, openCreateGroup: { latitude, longitude, address in let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -350,16 +527,26 @@ public func peersNearbyController(context: AccountContext) -> ViewController { presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.CreateGroup_ErrorLocatedGroupsTooMuch, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } })) + }, contextAction: { peer, node, gesture in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true)) + chatController.canReadHistory.set(false) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: peerNearbyContextMenuItems(context: context, peerId: peer.id, present: { c in + presentControllerImpl?(c, nil) + }), reactionItems: [], gesture: gesture) + presentInGlobalOverlayImpl?(contextController) + }, expandUsers: { + expandedPromise.set(true) }) - let dataSignal: Signal = currentLocationManagerCoordinate(manager: context.sharedContext.locationManager!, timeout: 5.0) + let dataSignal: Signal = coordinatePromise.get() |> mapToSignal { coordinate -> Signal in guard let coordinate = coordinate else { return .single(nil) } return Signal { subscriber in - let peersNearbyContext = PeersNearbyContext(network: context.account.network, accountStateManager: context.account.stateManager, coordinate: (latitude: coordinate.latitude, longitude: coordinate.longitude)) + let peersNearbyContext = PeersNearbyContext(network: context.account.network, stateManager: context.account.stateManager, coordinate: (latitude: coordinate.latitude, longitude: coordinate.longitude)) let peersNearby: Signal = combineLatest(peersNearbyContext.get(), addressPromise.get()) |> mapToSignal { peersNearby, address -> Signal<([PeerNearby]?, String?), NoError> in @@ -379,17 +566,26 @@ public func peersNearbyController(context: AccountContext) -> ViewController { return context.account.postbox.transaction { transaction -> PeersNearbyData? in var users: [PeerNearbyEntry] = [] var groups: [PeerNearbyEntry] = [] + var visible = false for peerNearby in peersNearby { - if peerNearby.id != context.account.peerId, let peer = transaction.getPeer(peerNearby.id) { - if peerNearby.id.namespace == Namespaces.Peer.CloudUser { - users.append(PeerNearbyEntry(peer: (peer, nil), expires: peerNearby.expires, distance: peerNearby.distance)) - } else { - let cachedData = transaction.getPeerCachedData(peerId: peerNearby.id) as? CachedChannelData - groups.append(PeerNearbyEntry(peer: (peer, cachedData), expires: peerNearby.expires, distance: peerNearby.distance)) - } + switch peerNearby { + case let .peer(id, expires, distance): + if let peer = transaction.getPeer(id) { + if id.namespace == Namespaces.Peer.CloudUser { + users.append(PeerNearbyEntry(peer: (peer, nil), expires: expires, distance: distance)) + } else { + let cachedData = transaction.getPeerCachedData(peerId: id) as? CachedChannelData + groups.append(PeerNearbyEntry(peer: (peer, cachedData), expires: expires, distance: distance)) + } + } + case let .selfPeer(expires): + visible = true + if let peer = transaction.getPeer(context.account.peerId) { + users.append(PeerNearbyEntry(peer: (peer, nil), expires: expires, distance: 0)) + } } } - return PeersNearbyData(latitude: coordinate.latitude, longitude: coordinate.longitude, address: address, users: users, groups: groups, channels: []) + return PeersNearbyData(latitude: coordinate.latitude, longitude: coordinate.longitude, address: address, visible: visible, accountPeerId: context.account.peerId, users: users, groups: groups, channels: []) } } @@ -411,11 +607,12 @@ public func peersNearbyController(context: AccountContext) -> ViewController { .single(true) |> delay(1.0, queue: Queue.mainQueue()) ) - - let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), displayLoading) + + let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), displayLoading, expandedPromise.get(), context.account.postbox.preferencesView(keys: [PreferencesKeys.peersNearby])) |> deliverOnMainQueue - |> map { presentationData, data, displayLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, data, displayLoading, expanded, view -> (ItemListControllerState, (ItemListNodeState, Any)) in let previous = previousData.swap(data) + let state = view.values[PreferencesKeys.peersNearby] as? PeersNearbyState ?? .default var crossfade = false if (data?.users.isEmpty ?? true) != (previous?.users.isEmpty ?? true) { @@ -425,8 +622,8 @@ public func peersNearbyController(context: AccountContext) -> ViewController { crossfade = true } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.PeopleNearby_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(entries: peersNearbyControllerEntries(data: data, presentationData: presentationData, displayLoading: displayLoading), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: !crossfade) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PeopleNearby_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: peersNearbyControllerEntries(data: data, state: state, presentationData: presentationData, displayLoading: displayLoading, expanded: expanded), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: !crossfade) return (controllerState, (listState, arguments)) } @@ -438,6 +635,11 @@ public func peersNearbyController(context: AccountContext) -> ViewController { controller.didDisappear = { [weak controller] _ in controller?.clearItemNodesHighlight(animated: true) } + navigateToProfileImpl = { [weak controller] peer in + if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .nearbyPeer, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false) { + (navigationController as? NavigationController)?.pushViewController(controller) + } + } navigateToChatImpl = { [weak controller] peer in if let navigationController = controller?.navigationController as? NavigationController { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer.id), keepStack: .always, purposefulAction: { [weak navigationController] in @@ -467,6 +669,10 @@ public func peersNearbyController(context: AccountContext) -> ViewController { controller.present(c, in: .window(.root), with: p) } } - + presentInGlobalOverlayImpl = { [weak controller] c in + if let controller = controller { + controller.presentInGlobalOverlay(c) + } + } return controller } diff --git a/submodules/PeersNearbyUI/Sources/PeersNearbyHeaderItem.swift b/submodules/PeersNearbyUI/Sources/PeersNearbyHeaderItem.swift index 7bbb9e9428..ed5a84dc13 100644 --- a/submodules/PeersNearbyUI/Sources/PeersNearbyHeaderItem.swift +++ b/submodules/PeersNearbyUI/Sources/PeersNearbyHeaderItem.swift @@ -6,7 +6,8 @@ import SwiftSignalKit import TelegramPresentationData import ItemListUI import PresentationDataUtils -import PeersNearbyIconNode +import AnimatedStickerNode +import AppBundle class PeersNearbyHeaderItem: ListViewItem, ItemListItem { let theme: PresentationTheme @@ -60,7 +61,7 @@ private let titleFont = Font.regular(13.0) class PeersNearbyHeaderItemNode: ListViewItemNode { private let titleNode: TextNode - private var iconNode: PeersNearbyIconNode? + private var animationNode: AnimatedStickerNode private var item: PeersNearbyHeaderItem? @@ -70,24 +71,29 @@ class PeersNearbyHeaderItemNode: ListViewItemNode { self.titleNode.contentMode = .left self.titleNode.contentsScale = UIScreen.main.scale + self.animationNode = AnimatedStickerNode() + if let path = getAppBundle().path(forResource: "Compass", ofType: "tgs") { + self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct) + self.animationNode.visibility = true + } + super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.titleNode) + self.addSubnode(self.animationNode) } func asyncLayout() -> (_ item: PeersNearbyHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) return { item, params, neighbors in - let leftInset: CGFloat = 48.0 + params.leftInset + let leftInset: CGFloat = 32.0 + params.leftInset let topInset: CGFloat = 92.0 let attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.freeTextColor) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - let contentSize: CGSize - - contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height) + let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height) let insets = itemListNeighborsGroupedInsets(neighbors) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -96,22 +102,13 @@ class PeersNearbyHeaderItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item strongSelf.accessibilityLabel = attributedText.string - - let iconNode: PeersNearbyIconNode - if let node = strongSelf.iconNode { - iconNode = node - iconNode.updateTheme(item.theme) - } else { - iconNode = PeersNearbyIconNode(theme: item.theme) - strongSelf.iconNode = iconNode - strongSelf.addSubnode(iconNode) - } - - let iconSize = CGSize(width: 60.0, height: 60.0) - iconNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: 5.0), size: iconSize) + + let iconSize = CGSize(width: 96.0, height: 96.0) + strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize) + strongSelf.animationNode.updateLayout(size: iconSize) let _ = titleApply() - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset), size: titleLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset + 8.0), size: titleLayout.size) } }) } diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 1ffb182d03..5aa2304795 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -403,270 +403,6 @@ private func chatMessageVideoDatas(postbox: Postbox, fileReference: FileMediaRef return signal } -private enum Corner: Hashable { - case TopLeft(Int), TopRight(Int), BottomLeft(Int), BottomRight(Int) - - var hashValue: Int { - switch self { - case let .TopLeft(radius): - return radius | (1 << 24) - case let .TopRight(radius): - return radius | (2 << 24) - case let .BottomLeft(radius): - return radius | (3 << 24) - case let .BottomRight(radius): - return radius | (4 << 24) - } - } - - var radius: Int { - switch self { - case let .TopLeft(radius): - return radius - case let .TopRight(radius): - return radius - case let .BottomLeft(radius): - return radius - case let .BottomRight(radius): - return radius - } - } -} - -private func ==(lhs: Corner, rhs: Corner) -> Bool { - switch lhs { - case let .TopLeft(lhsRadius): - switch rhs { - case let .TopLeft(rhsRadius) where rhsRadius == lhsRadius: - return true - default: - return false - } - case let .TopRight(lhsRadius): - switch rhs { - case let .TopRight(rhsRadius) where rhsRadius == lhsRadius: - return true - default: - return false - } - case let .BottomLeft(lhsRadius): - switch rhs { - case let .BottomLeft(rhsRadius) where rhsRadius == lhsRadius: - return true - default: - return false - } - case let .BottomRight(lhsRadius): - switch rhs { - case let .BottomRight(rhsRadius) where rhsRadius == lhsRadius: - return true - default: - return false - } - } -} - -private enum Tail: Hashable { - case BottomLeft(Int) - case BottomRight(Int) - - var hashValue: Int { - switch self { - case let .BottomLeft(radius): - return radius | (1 << 24) - case let .BottomRight(radius): - return radius | (2 << 24) - } - } - - var radius: Int { - switch self { - case let .BottomLeft(radius): - return radius - case let .BottomRight(radius): - return radius - } - } -} - -private func ==(lhs: Tail, rhs: Tail) -> Bool { - switch lhs { - case let .BottomLeft(lhsRadius): - switch rhs { - case let .BottomLeft(rhsRadius) where rhsRadius == lhsRadius: - return true - default: - return false - } - case let .BottomRight(lhsRadius): - switch rhs { - case let .BottomRight(rhsRadius) where rhsRadius == lhsRadius: - return true - default: - return false - } - } -} - -private var cachedCorners = Atomic<[Corner: DrawingContext]>(value: [:]) -private var cachedTails = Atomic<[Tail: DrawingContext]>(value: [:]) - -private func cornerContext(_ corner: Corner) -> DrawingContext { - let cached: DrawingContext? = cachedCorners.with { - return $0[corner] - } - - if let cached = cached { - return cached - } else { - let context = DrawingContext(size: CGSize(width: CGFloat(corner.radius), height: CGFloat(corner.radius)), clear: true) - - context.withContext { c in - c.setBlendMode(.copy) - c.setFillColor(UIColor.black.cgColor) - let rect: CGRect - switch corner { - case let .TopLeft(radius): - rect = CGRect(origin: CGPoint(), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) - case let .TopRight(radius): - rect = CGRect(origin: CGPoint(x: -CGFloat(radius), y: 0.0), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) - case let .BottomLeft(radius): - rect = CGRect(origin: CGPoint(x: 0.0, y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) - case let .BottomRight(radius): - rect = CGRect(origin: CGPoint(x: -CGFloat(radius), y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) - } - c.fillEllipse(in: rect) - } - - let _ = cachedCorners.modify { current in - var current = current - current[corner] = context - return current - } - - return context - } -} - -private func tailContext(_ tail: Tail) -> DrawingContext { - let cached: DrawingContext? = cachedTails.with { - return $0[tail] - } - - if let cached = cached { - return cached - } else { - let context = DrawingContext(size: CGSize(width: CGFloat(tail.radius) + 3.0, height: CGFloat(tail.radius)), clear: true) - - context.withContext { c in - c.setBlendMode(.copy) - c.setFillColor(UIColor.black.cgColor) - let rect: CGRect - switch tail { - case let .BottomLeft(radius): - rect = CGRect(origin: CGPoint(x: 3.0, y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) - - c.move(to: CGPoint(x: 3.0, y: 1.0)) - c.addLine(to: CGPoint(x: 3.0, y: 11.0)) - c.addLine(to: CGPoint(x: 2.3, y: 13.0)) - c.addLine(to: CGPoint(x: 0.0, y: 16.6)) - c.addLine(to: CGPoint(x: 4.5, y: 15.5)) - c.addLine(to: CGPoint(x: 6.5, y: 14.3)) - c.addLine(to: CGPoint(x: 9.0, y: 12.5)) - c.closePath() - c.fillPath() - case let .BottomRight(radius): - rect = CGRect(origin: CGPoint(x: 3.0, y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) - - c.translateBy(x: context.size.width / 2.0, y: context.size.height / 2.0) - c.scaleBy(x: -1.0, y: 1.0) - c.translateBy(x: -context.size.width / 2.0, y: -context.size.height / 2.0) - - c.move(to: CGPoint(x: 3.0, y: 1.0)) - c.addLine(to: CGPoint(x: 3.0, y: 11.0)) - c.addLine(to: CGPoint(x: 2.3, y: 13.0)) - c.addLine(to: CGPoint(x: 0.0, y: 16.6)) - c.addLine(to: CGPoint(x: 4.5, y: 15.5)) - c.addLine(to: CGPoint(x: 6.5, y: 14.3)) - c.addLine(to: CGPoint(x: 9.0, y: 12.5)) - c.closePath() - c.fillPath() - } - c.fillEllipse(in: rect) - } - - let _ = cachedTails.modify { current in - var current = current - current[tail] = context - return current - } - return context - } -} - -public func addCorners(_ context: DrawingContext, arguments: TransformImageArguments) { - let corners = arguments.corners - let drawingRect = arguments.drawingRect - if case let .Corner(radius) = corners.topLeft, radius > CGFloat.ulpOfOne { - let corner = cornerContext(.TopLeft(Int(radius))) - context.blt(corner, at: CGPoint(x: drawingRect.minX, y: drawingRect.minY)) - } - - if case let .Corner(radius) = corners.topRight, radius > CGFloat.ulpOfOne { - let corner = cornerContext(.TopRight(Int(radius))) - context.blt(corner, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.minY)) - } - - switch corners.bottomLeft { - case let .Corner(radius): - if radius > CGFloat.ulpOfOne { - let corner = cornerContext(.BottomLeft(Int(radius))) - context.blt(corner, at: CGPoint(x: drawingRect.minX, y: drawingRect.maxY - radius)) - } - case let .Tail(radius, enabled): - if radius > CGFloat.ulpOfOne { - if enabled { - let tail = tailContext(.BottomLeft(Int(radius))) - let color = context.colorAt(CGPoint(x: drawingRect.minX, y: drawingRect.maxY - 1.0)) - context.withContext { c in - c.clear(CGRect(x: drawingRect.minX - 3.0, y: 0.0, width: 3.0, height: drawingRect.maxY - 6.0)) - c.setFillColor(color.cgColor) - c.fill(CGRect(x: 0.0, y: drawingRect.maxY - 6.0, width: 3.0, height: 6.0)) - } - context.blt(tail, at: CGPoint(x: drawingRect.minX - 3.0, y: drawingRect.maxY - radius)) - } else { - let corner = cornerContext(.BottomLeft(Int(radius))) - context.blt(corner, at: CGPoint(x: drawingRect.minX, y: drawingRect.maxY - radius)) - } - } - - } - - switch corners.bottomRight { - case let .Corner(radius): - if radius > CGFloat.ulpOfOne { - let corner = cornerContext(.BottomRight(Int(radius))) - context.blt(corner, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.maxY - radius)) - } - case let .Tail(radius, enabled): - if radius > CGFloat.ulpOfOne { - if enabled { - let tail = tailContext(.BottomRight(Int(radius))) - let color = context.colorAt(CGPoint(x: drawingRect.maxX - 1.0, y: drawingRect.maxY - 1.0)) - context.withContext { c in - c.clear(CGRect(x: drawingRect.maxX, y: 0.0, width: 3.0, height: drawingRect.maxY - 6.0)) - c.setFillColor(color.cgColor) - c.fill(CGRect(x: drawingRect.maxX, y: drawingRect.maxY - 6.0, width: 3.0, height: 6.0)) - } - context.blt(tail, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.maxY - radius)) - } else { - let corner = cornerContext(.BottomRight(Int(radius))) - context.blt(corner, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.maxY - radius)) - } - } - } -} - public func rawMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference) -> Signal { return chatMessagePhotoDatas(postbox: postbox, photoReference: photoReference, autoFetchFullSize: true) |> map { value -> UIImage? in @@ -2284,10 +2020,10 @@ public func instantPageImageFile(account: Account, fileReference: FileMediaRefer } } -private func avatarGalleryPhotoDatas(account: Account, fileReference: FileMediaReference? = nil, representations: [ImageRepresentationWithReference], autoFetchFullSize: Bool = false) -> Signal, NoError> { +private func avatarGalleryPhotoDatas(account: Account, fileReference: FileMediaReference? = nil, representations: [ImageRepresentationWithReference], autoFetchFullSize: Bool = false, attemptSynchronously: Bool = false) -> Signal, NoError> { if let smallestRepresentation = smallestImageRepresentation(representations.map({ $0.representation })), let largestRepresentation = largestImageRepresentation(representations.map({ $0.representation })), let smallestIndex = representations.firstIndex(where: { $0.representation == smallestRepresentation }), let largestIndex = representations.firstIndex(where: { $0.representation == largestRepresentation }) { - let maybeFullSize = account.postbox.mediaBox.resourceData(largestRepresentation.resource) + let maybeFullSize = account.postbox.mediaBox.resourceData(largestRepresentation.resource, attemptSynchronously: attemptSynchronously) let signal = maybeFullSize |> take(1) @@ -2301,7 +2037,7 @@ private func avatarGalleryPhotoDatas(account: Account, fileReference: FileMediaR let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() - let thumbnailDisposable = account.postbox.mediaBox.resourceData(smallestRepresentation.resource).start(next: { next in + let thumbnailDisposable = account.postbox.mediaBox.resourceData(smallestRepresentation.resource, attemptSynchronously: attemptSynchronously).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) @@ -2316,7 +2052,7 @@ private func avatarGalleryPhotoDatas(account: Account, fileReference: FileMediaR if autoFetchFullSize { fullSizeData = Signal, NoError> { subscriber in let fetchedFullSizeDisposable = fetchedFullSize.start() - let fullSizeDisposable = account.postbox.mediaBox.resourceData(largestRepresentation.resource).start(next: { next in + let fullSizeDisposable = account.postbox.mediaBox.resourceData(largestRepresentation.resource, attemptSynchronously: attemptSynchronously).start(next: { next in subscriber.putNext(Tuple(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) }, error: subscriber.putError, completed: subscriber.putCompletion) @@ -2346,8 +2082,8 @@ private func avatarGalleryPhotoDatas(account: Account, fileReference: FileMediaR } } -public func chatAvatarGalleryPhoto(account: Account, representations: [ImageRepresentationWithReference], autoFetchFullSize: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = avatarGalleryPhotoDatas(account: account, representations: representations, autoFetchFullSize: autoFetchFullSize) +public func chatAvatarGalleryPhoto(account: Account, representations: [ImageRepresentationWithReference], autoFetchFullSize: Bool = false, attemptSynchronously: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = avatarGalleryPhotoDatas(account: account, representations: representations, autoFetchFullSize: autoFetchFullSize, attemptSynchronously: attemptSynchronously) return signal |> map { value in @@ -2469,90 +2205,6 @@ public func chatAvatarGalleryPhoto(account: Account, representations: [ImageRepr } } -public func chatMapSnapshotData(account: Account, resource: MapSnapshotMediaResource) -> Signal { - return Signal { subscriber in - let dataDisposable = account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: MapSnapshotMediaResourceRepresentation(), complete: true).start(next: { next in - if next.size != 0 { - subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) - } - }, error: subscriber.putError, completed: subscriber.putCompletion) - - return ActionDisposable { - dataDisposable.dispose() - } - } -} - -private let locationPinImage = UIImage(named: "ModernMessageLocationPin")?.precomposed() - -public func chatMapSnapshotImage(account: Account, resource: MapSnapshotMediaResource) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMapSnapshotData(account: account, resource: resource) - - return signal |> map { fullSizeData in - return { arguments in - let context = DrawingContext(size: arguments.drawingSize, clear: true) - - var fullSizeImage: CGImage? - var imageOrientation: UIImage.Orientation = .up - if let fullSizeData = fullSizeData { - let options = NSMutableDictionary() - options[kCGImageSourceShouldCache as NSString] = false as NSNumber - if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { - imageOrientation = imageOrientationFromSource(imageSource) - fullSizeImage = image - } - - if let fullSizeImage = fullSizeImage { - let drawingRect = arguments.drawingRect - var fittedSize = CGSize(width: CGFloat(fullSizeImage.width), height: CGFloat(fullSizeImage.height)).aspectFilled(drawingRect.size) - if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { - fittedSize.width = arguments.boundingSize.width - } - if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { - fittedSize.height = arguments.boundingSize.height - } - - let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) - - context.withFlippedContext { c in - c.setBlendMode(.copy) - if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { - c.fill(arguments.drawingRect) - } - - c.setBlendMode(.copy) - - c.interpolationQuality = .medium - drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) - - c.setBlendMode(.normal) - - if let locationPinImage = locationPinImage { - c.draw(locationPinImage.cgImage!, in: CGRect(origin: CGPoint(x: floor((arguments.drawingSize.width - locationPinImage.size.width) / 2.0), y: floor((arguments.drawingSize.height - locationPinImage.size.height) / 2.0) - 5.0), size: locationPinImage.size)) - } - } - } else { - context.withFlippedContext { c in - c.setBlendMode(.copy) - c.setFillColor((arguments.emptyColor ?? UIColor.white).cgColor) - c.fill(arguments.drawingRect) - - c.setBlendMode(.normal) - - if let locationPinImage = locationPinImage { - c.draw(locationPinImage.cgImage!, in: CGRect(origin: CGPoint(x: floor((arguments.drawingSize.width - locationPinImage.size.width) / 2.0), y: floor((arguments.drawingSize.height - locationPinImage.size.height) / 2.0) - 5.0), size: locationPinImage.size)) - } - } - } - } - - addCorners(context, arguments: arguments) - - return context - } - } -} - public func chatWebFileImage(account: Account, file: TelegramMediaWebFile) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return account.postbox.mediaBox.resourceData(file.resource) |> map { fullSizeData in diff --git a/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift b/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift index 3d36594562..c20aade7be 100644 --- a/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift +++ b/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift @@ -1,13 +1,14 @@ import Foundation import TelegramCore import SyncCore -import SyncCore public extension RestrictedContentMessageAttribute { - func platformText(platform: String) -> String? { + func platformText(platform: String, contentSettings: ContentSettings) -> String? { for rule in self.rules { if rule.platform == "all" || rule.platform == "ios" { - return rule.text + if !contentSettings.ignoreContentRestrictionReasons.contains(rule.reason) { + return rule.text + } } } return nil diff --git a/submodules/Postbox/Sources/AllChatListHolesView.swift b/submodules/Postbox/Sources/AllChatListHolesView.swift new file mode 100644 index 0000000000..6f40b007c8 --- /dev/null +++ b/submodules/Postbox/Sources/AllChatListHolesView.swift @@ -0,0 +1,63 @@ +import Foundation + +final class MutableAllChatListHolesView: MutablePostboxView { + fileprivate let groupId: PeerGroupId + private var holes = Set() + fileprivate var latestHole: ChatListHole? + + init(postbox: Postbox, groupId: PeerGroupId) { + self.groupId = groupId + self.holes = Set(postbox.chatListTable.allHoles(groupId: groupId)) + self.latestHole = self.holes.max(by: { $0.index < $1.index }) + } + + func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool { + if let operations = transaction.chatListOperations[self.groupId] { + var updated = false + for operation in operations { + switch operation { + case let .InsertHole(hole): + if !self.holes.contains(hole) { + self.holes.insert(hole) + updated = true + } + case let .RemoveHoles(indices): + for index in indices { + if self.holes.contains(ChatListHole(index: index.messageIndex)) { + self.holes.remove(ChatListHole(index: index.messageIndex)) + updated = true + } + } + default: + break + } + } + + if updated { + let updatedLatestHole = self.holes.max(by: { $0.index < $1.index }) + if updatedLatestHole != self.latestHole { + self.latestHole = updatedLatestHole + return true + } else { + return false + } + } else { + return false + } + } else { + return false + } + } + + func immutableView() -> PostboxView { + return AllChatListHolesView(self) + } +} + +public final class AllChatListHolesView: PostboxView { + public let latestHole: ChatListHole? + + init(_ view: MutableAllChatListHolesView) { + self.latestHole = view.latestHole + } +} diff --git a/submodules/Postbox/Sources/ChatListIndexTable.swift b/submodules/Postbox/Sources/ChatListIndexTable.swift index 9e1a41e51e..1a66b80bd7 100644 --- a/submodules/Postbox/Sources/ChatListIndexTable.swift +++ b/submodules/Postbox/Sources/ChatListIndexTable.swift @@ -286,6 +286,20 @@ final class ChatListIndexTable: Table { alteredPeerIds.insert(peerId) } + var additionalAlteredPeerIds = Set() + for peerId in alteredPeerIds { + guard let peer = postbox.peerTable.get(peerId) else { + continue + } + if let associatedPeerId = peer.associatedPeerId { + additionalAlteredPeerIds.insert(associatedPeerId) + } + if let reverseAssociatedPeerId = postbox.reverseAssociatedPeerTable.get(peerId: peerId).first { + additionalAlteredPeerIds.insert(reverseAssociatedPeerId) + } + } + alteredPeerIds.formUnion(additionalAlteredPeerIds) + func alterTags(_ totalUnreadState: inout ChatListTotalUnreadState, _ peerId: PeerId, _ tag: PeerSummaryCounterTags, _ f: (ChatListTotalUnreadCounters, ChatListTotalUnreadCounters) -> (ChatListTotalUnreadCounters, ChatListTotalUnreadCounters)) { if totalUnreadState.absoluteCounters[tag] == nil { totalUnreadState.absoluteCounters[tag] = ChatListTotalUnreadCounters(messageCount: 0, chatCount: 0) @@ -404,10 +418,10 @@ final class ChatListIndexTable: Table { var initialFilteredStates: CombinedPeerReadState = initialStates var currentFilteredStates: CombinedPeerReadState = currentStates - if transactionParticipationInTotalUnreadCountUpdates.added.contains(peerId) { + if transactionParticipationInTotalUnreadCountUpdates.added.contains(peerId) || transactionParticipationInTotalUnreadCountUpdates.added.contains(notificationPeerId) { initialFilteredValue = (0, false, false) initialFilteredStates = CombinedPeerReadState(states: []) - } else if transactionParticipationInTotalUnreadCountUpdates.removed.contains(peerId) { + } else if transactionParticipationInTotalUnreadCountUpdates.removed.contains(peerId) || transactionParticipationInTotalUnreadCountUpdates.removed.contains(notificationPeerId) { currentFilteredValue = (0, false, false) currentFilteredStates = CombinedPeerReadState(states: []) } else { @@ -556,13 +570,10 @@ final class ChatListIndexTable: Table { func debugReindexUnreadCounts(postbox: Postbox) -> (ChatListTotalUnreadState, [PeerGroupId: PeerGroupUnreadCountersCombinedSummary]) { var peerIds: [PeerId] = [] - self.valueBox.scanInt64(self.table, values: { key, _ in - let peerId = PeerId(key) - if peerId.namespace != Int32.max { - peerIds.append(peerId) - } - return true - }) + for groupId in postbox.chatListTable.existingGroups() + [.root] { + let groupPeerIds = postbox.chatListTable.allPeerIds(groupId: groupId) + peerIds.append(contentsOf: groupPeerIds) + } var rootState = ChatListTotalUnreadState(absoluteCounters: [:], filteredCounters: [:]) var summaries: [PeerGroupId: PeerGroupUnreadCountersCombinedSummary] = [:] for peerId in peerIds { diff --git a/submodules/Postbox/Sources/ChatListTable.swift b/submodules/Postbox/Sources/ChatListTable.swift index 6b9682e432..b44e99fc82 100644 --- a/submodules/Postbox/Sources/ChatListTable.swift +++ b/submodules/Postbox/Sources/ChatListTable.swift @@ -381,7 +381,7 @@ final class ChatListTable: Table { self.valueBox.remove(self.table, key: self.key(groupId: groupId, index: ChatListIndex(pinningIndex: nil, messageIndex: index), type: .hole), secure: false) } - func entriesAround(groupId: PeerGroupId, index: ChatListIndex, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int) -> (entries: [ChatListIntermediateEntry], lower: ChatListIntermediateEntry?, upper: ChatListIntermediateEntry?) { + func entriesAround(groupId: PeerGroupId, index: ChatListIndex, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int, predicate: ((ChatListIntermediateEntry) -> Bool)?) -> (entries: [ChatListIntermediateEntry], lower: ChatListIntermediateEntry?, upper: ChatListIntermediateEntry?) { self.ensureInitialized(groupId: groupId) var lowerEntries: [ChatListIntermediateEntry] = [] @@ -389,18 +389,38 @@ final class ChatListTable: Table { var lower: ChatListIntermediateEntry? var upper: ChatListIntermediateEntry? - self.valueBox.range(self.table, start: self.key(groupId: groupId, index: index, type: .message), end: self.lowerBound(groupId: groupId), values: { key, value in - lowerEntries.append(readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)) - return true + self.valueBox.filteredRange(self.table, start: self.key(groupId: groupId, index: index, type: .message), end: self.lowerBound(groupId: groupId), values: { key, value in + let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value) + if let predicate = predicate { + if predicate(entry) { + lowerEntries.append(entry) + return .accept + } else { + return .skip + } + } else { + lowerEntries.append(entry) + return .accept + } }, limit: count / 2 + 1) if lowerEntries.count >= count / 2 + 1 { lower = lowerEntries.last lowerEntries.removeLast() } - self.valueBox.range(self.table, start: self.key(groupId: groupId, index: index, type: .message).predecessor, end: self.upperBound(groupId: groupId), values: { key, value in - upperEntries.append(readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)) - return true + self.valueBox.filteredRange(self.table, start: self.key(groupId: groupId, index: index, type: .message).predecessor, end: self.upperBound(groupId: groupId), values: { key, value in + let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value) + if let predicate = predicate { + if predicate(entry) { + upperEntries.append(entry) + return .accept + } else { + return .skip + } + } else { + upperEntries.append(entry) + return .accept + } }, limit: count - lowerEntries.count + 1) if upperEntries.count >= count - lowerEntries.count + 1 { upper = upperEntries.last @@ -415,12 +435,20 @@ final class ChatListTable: Table { startEntryType = .message case .hole: startEntryType = .hole - /*case .groupReference: - startEntryType = .groupReference*/ } - self.valueBox.range(self.table, start: self.key(groupId: groupId, index: lowerEntries.last!.index, type: startEntryType), end: self.lowerBound(groupId: groupId), values: { key, value in - additionalLowerEntries.append(readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)) - return true + self.valueBox.filteredRange(self.table, start: self.key(groupId: groupId, index: lowerEntries.last!.index, type: startEntryType), end: self.lowerBound(groupId: groupId), values: { key, value in + let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value) + if let predicate = predicate { + if predicate(entry) { + additionalLowerEntries.append(entry) + return .accept + } else { + return .skip + } + } else { + additionalLowerEntries.append(entry) + return .accept + } }, limit: count - lowerEntries.count - upperEntries.count + 1) if additionalLowerEntries.count >= count - lowerEntries.count + upperEntries.count + 1 { lower = additionalLowerEntries.last @@ -502,7 +530,7 @@ final class ChatListTable: Table { return result } - func earlierEntries(groupId: PeerGroupId, index: (ChatListIndex, Bool)?, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int) -> [ChatListIntermediateEntry] { + func earlierEntries(groupId: PeerGroupId, index: (ChatListIndex, Bool)?, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int, predicate: ((ChatListIntermediateEntry) -> Bool)?) -> [ChatListIntermediateEntry] { self.ensureInitialized(groupId: groupId) var entries: [ChatListIntermediateEntry] = [] @@ -513,9 +541,19 @@ final class ChatListTable: Table { key = self.upperBound(groupId: groupId) } - self.valueBox.range(self.table, start: key, end: self.lowerBound(groupId: groupId), values: { key, value in - entries.append(readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)) - return true + self.valueBox.filteredRange(self.table, start: key, end: self.lowerBound(groupId: groupId), values: { key, value in + let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value) + if let predicate = predicate { + if predicate(entry) { + entries.append(entry) + return .accept + } else { + return .skip + } + } else { + entries.append(entry) + return .accept + } }, limit: count) return entries } @@ -556,7 +594,7 @@ final class ChatListTable: Table { return entries } - func laterEntries(groupId: PeerGroupId, index: (ChatListIndex, Bool)?, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int) -> [ChatListIntermediateEntry] { + func laterEntries(groupId: PeerGroupId, index: (ChatListIndex, Bool)?, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int, predicate: ((ChatListIntermediateEntry) -> Bool)?) -> [ChatListIntermediateEntry] { self.ensureInitialized(groupId: groupId) var entries: [ChatListIntermediateEntry] = [] @@ -567,9 +605,19 @@ final class ChatListTable: Table { key = self.lowerBound(groupId: groupId) } - self.valueBox.range(self.table, start: key, end: self.upperBound(groupId: groupId), values: { key, value in - entries.append(readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)) - return true + self.valueBox.filteredRange(self.table, start: key, end: self.upperBound(groupId: groupId), values: { key, value in + let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value) + if let predicate = predicate { + if predicate(entry) { + entries.append(entry) + return .accept + } else { + return .skip + } + } else { + entries.append(entry) + return .accept + } }, limit: count) return entries } @@ -590,6 +638,19 @@ final class ChatListTable: Table { return nil } + func getEntry(groupId: PeerGroupId, peerId: PeerId, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable) -> ChatListIntermediateEntry? { + if let (peerGroupId, index) = self.getPeerChatListIndex(peerId: peerId), peerGroupId == groupId { + let key = self.key(groupId: groupId, index: index, type: .message) + if let value = self.valueBox.get(self.table, key: key) { + return readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value) + } else { + return nil + } + } else { + return nil + } + } + func allEntries(groupId: PeerGroupId) -> [ChatListEntryInfo] { var entries: [ChatListEntryInfo] = [] self.valueBox.range(self.table, start: self.upperBound(groupId: groupId), end: self.lowerBound(groupId: groupId), values: { key, value in @@ -619,6 +680,35 @@ final class ChatListTable: Table { return entries } + func allPeerIds(groupId: PeerGroupId) -> [PeerId] { + var peerIds: [PeerId] = [] + self.valueBox.range(self.table, start: self.upperBound(groupId: groupId), end: self.lowerBound(groupId: groupId), keys: { key in + let (keyGroupId, pinningIndex, messageIndex, type) = extractKey(key) + assert(groupId == keyGroupId) + + let index = ChatListIndex(pinningIndex: pinningIndex, messageIndex: messageIndex) + if type == ChatListEntryType.message.rawValue { + peerIds.append(messageIndex.id.peerId) + } + return true + }, limit: 0) + return peerIds + } + + func allHoles(groupId: PeerGroupId) -> [ChatListHole] { + var entries: [ChatListHole] = [] + self.valueBox.range(self.table, start: self.upperBound(groupId: groupId), end: self.lowerBound(groupId: groupId), keys: { key in + let (keyGroupId, pinningIndex, messageIndex, type) = extractKey(key) + assert(groupId == keyGroupId) + if type == ChatListEntryType.hole.rawValue { + let index = ChatListIndex(pinningIndex: pinningIndex, messageIndex: messageIndex) + entries.append(ChatListHole(index: index.messageIndex)) + } + return true + }, limit: 0) + return entries + } + func entriesInRange(groupId: PeerGroupId, upperBound: ChatListIndex, lowerBound: ChatListIndex) -> [ChatListEntryInfo] { var entries: [ChatListEntryInfo] = [] let upperBoundKey: ValueBoxKey @@ -710,7 +800,7 @@ final class ChatListTable: Table { } func debugList(groupId: PeerGroupId, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable) -> [ChatListIntermediateEntry] { - return self.laterEntries(groupId: groupId, index: (ChatListIndex.absoluteLowerBound, true), messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, count: 1000) + return self.laterEntries(groupId: groupId, index: (ChatListIndex.absoluteLowerBound, true), messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, count: 1000, predicate: nil) } func getNamespaceEntries(groupId: PeerGroupId, namespace: MessageId.Namespace, summaryTag: MessageTags?, messageIndexTable: MessageHistoryIndexTable, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, readStateTable: MessageHistoryReadStateTable, summaryTable: MessageHistoryTagsSummaryTable) -> [ChatListNamespaceEntry] { diff --git a/submodules/Postbox/Sources/ChatListView.swift b/submodules/Postbox/Sources/ChatListView.swift index 0d9ff1487f..920d88b95b 100644 --- a/submodules/Postbox/Sources/ChatListView.swift +++ b/submodules/Postbox/Sources/ChatListView.swift @@ -88,12 +88,12 @@ public struct ChatListGroupReferenceEntry: Equatable { } public enum ChatListEntry: Comparable { - case MessageEntry(ChatListIndex, Message?, CombinedPeerReadState?, PeerNotificationSettings?, PeerChatListEmbeddedInterfaceState?, RenderedPeer, PeerPresence?, ChatListMessageTagSummaryInfo) + case MessageEntry(ChatListIndex, Message?, CombinedPeerReadState?, PeerNotificationSettings?, PeerChatListEmbeddedInterfaceState?, RenderedPeer, PeerPresence?, ChatListMessageTagSummaryInfo, Bool) case HoleEntry(ChatListHole) public var index: ChatListIndex { switch self { - case let .MessageEntry(index, _, _, _, _, _, _, _): + case let .MessageEntry(index, _, _, _, _, _, _, _, _): return index case let .HoleEntry(hole): return ChatListIndex(pinningIndex: nil, messageIndex: hole.index) @@ -102,9 +102,9 @@ public enum ChatListEntry: Comparable { public static func ==(lhs: ChatListEntry, rhs: ChatListEntry) -> Bool { switch lhs { - case let .MessageEntry(lhsIndex, lhsMessage, lhsReadState, lhsSettings, lhsEmbeddedState, lhsPeer, lhsPresence, lhsInfo): + case let .MessageEntry(lhsIndex, lhsMessage, lhsReadState, lhsSettings, lhsEmbeddedState, lhsPeer, lhsPresence, lhsInfo, lhsHasFailed): switch rhs { - case let .MessageEntry(rhsIndex, rhsMessage, rhsReadState, rhsSettings, rhsEmbeddedState, rhsPeer, rhsPresence, rhsInfo): + case let .MessageEntry(rhsIndex, rhsMessage, rhsReadState, rhsSettings, rhsEmbeddedState, rhsPeer, rhsPresence, rhsInfo, rhsHasFailed): if lhsIndex != rhsIndex { return false } @@ -141,6 +141,9 @@ public enum ChatListEntry: Comparable { if lhsInfo != rhsInfo { return false } + if lhsHasFailed != rhsHasFailed { + return false + } return true default: return false @@ -178,7 +181,7 @@ private func processedChatListEntry(_ entry: MutableChatListEntry, cachedDataTab enum MutableChatListEntry: Equatable { case IntermediateMessageEntry(ChatListIndex, IntermediateMessage?, CombinedPeerReadState?, PeerChatListEmbeddedInterfaceState?) - case MessageEntry(ChatListIndex, Message?, CombinedPeerReadState?, PeerNotificationSettings?, PeerChatListEmbeddedInterfaceState?, RenderedPeer, PeerPresence?, ChatListMessageTagSummaryInfo) + case MessageEntry(ChatListIndex, Message?, CombinedPeerReadState?, PeerNotificationSettings?, PeerChatListEmbeddedInterfaceState?, RenderedPeer, PeerPresence?, ChatListMessageTagSummaryInfo, Bool) case HoleEntry(ChatListHole) init(_ intermediateEntry: ChatListIntermediateEntry, cachedDataTable: CachedPeerDataTable, readStateTable: MessageHistoryReadStateTable, messageHistoryTable: MessageHistoryTable) { @@ -194,7 +197,7 @@ enum MutableChatListEntry: Equatable { switch self { case let .IntermediateMessageEntry(index, _, _, _): return index - case let .MessageEntry(index, _, _, _, _, _, _, _): + case let .MessageEntry(index, _, _, _, _, _, _, _, _): return index case let .HoleEntry(hole): return ChatListIndex(pinningIndex: nil, messageIndex: hole.index) @@ -294,6 +297,7 @@ private func updatedRenderedPeer(_ renderedPeer: RenderedPeer, updatedPeers: [Pe final class MutableChatListView { let groupId: PeerGroupId + let filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)? private let summaryComponents: ChatListEntrySummaryComponents fileprivate var additionalItemIds: Set fileprivate var additionalItemEntries: [MutableChatListEntry] @@ -303,8 +307,11 @@ final class MutableChatListView { fileprivate var groupEntries: [ChatListGroupReferenceEntry] private var count: Int - init(postbox: Postbox, groupId: PeerGroupId, earlier: MutableChatListEntry?, entries: [MutableChatListEntry], later: MutableChatListEntry?, count: Int, summaryComponents: ChatListEntrySummaryComponents) { + init(postbox: Postbox, groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)?, aroundIndex: ChatListIndex, count: Int, summaryComponents: ChatListEntrySummaryComponents) { + let (entries, earlier, later) = postbox.fetchAroundChatEntries(groupId: groupId, index: aroundIndex, count: count, filterPredicate: filterPredicate) + self.groupId = groupId + self.filterPredicate = filterPredicate self.earlier = earlier self.entries = entries self.later = later @@ -402,7 +409,7 @@ final class MutableChatListView { index = self.entries[self.entries.count / 2].index } - let (entries, earlier, later) = postbox.fetchAroundChatEntries(groupId: self.groupId, index: index, count: self.count) + let (entries, earlier, later) = postbox.fetchAroundChatEntries(groupId: self.groupId, index: index, count: self.count, filterPredicate: self.filterPredicate) let currentGroupEntries = self.groupEntries self.reloadGroups(postbox: postbox) @@ -423,7 +430,7 @@ final class MutableChatListView { return updated } - func replay(postbox: Postbox, operations: [PeerGroupId: [ChatListOperation]], updatedPeerNotificationSettings: [PeerId: PeerNotificationSettings], updatedPeers: [PeerId: Peer], updatedPeerPresences: [PeerId: PeerPresence], transaction: PostboxTransaction, context: MutableChatListViewReplayContext) -> Bool { + func replay(postbox: Postbox, operations: [PeerGroupId: [ChatListOperation]], updatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)], updatedPeers: [PeerId: Peer], updatedPeerPresences: [PeerId: PeerPresence], transaction: PostboxTransaction, context: MutableChatListViewReplayContext) -> Bool { var hasChanges = false if let groupOperations = operations[self.groupId] { @@ -486,15 +493,53 @@ final class MutableChatListView { } if !updatedPeerNotificationSettings.isEmpty { + if let filterPredicate = self.filterPredicate { + for (peerId, settingsChange) in updatedPeerNotificationSettings { + if let peer = postbox.peerTable.get(peerId) { + let isUnread = postbox.readStateTable.getCombinedState(peerId)?.isUnread ?? false + let wasIncluded = filterPredicate(peer, settingsChange.0, isUnread) + let isIncluded = filterPredicate(peer, settingsChange.1, isUnread) + if wasIncluded != isIncluded { + if isIncluded { + if let entry = postbox.chatListTable.getEntry(groupId: self.groupId, peerId: peerId, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable) { + switch entry { + case let .message(index, message, embeddedState): + let combinedReadState = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId) + if self.add(.IntermediateMessageEntry(entry.index, message, combinedReadState, embeddedState), postbox: postbox) { + hasChanges = true + } + default: + break + } + } + } else { + loop: for i in 0 ..< self.entries.count { + switch self.entries[i] { + case .MessageEntry(let index, _, _, _, _, _, _, _, _), .IntermediateMessageEntry(let index, _, _, _): + if index.messageIndex.id.peerId == peerId { + self.entries.remove(at: i) + hasChanges = true + break loop + } + default: + break + } + } + } + } + } + } + } + for i in 0 ..< self.entries.count { switch self.entries[i] { - case let .MessageEntry(index, message, readState, _, embeddedState, peer, peerPresence, summaryInfo): + case let .MessageEntry(index, message, readState, _, embeddedState, peer, peerPresence, summaryInfo, hasFailed): var notificationSettingsPeerId = peer.peerId if let peer = peer.peers[peer.peerId], let associatedPeerId = peer.associatedPeerId { notificationSettingsPeerId = associatedPeerId } - if let settings = updatedPeerNotificationSettings[notificationSettingsPeerId] { - self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo) + if let (_, settings) = updatedPeerNotificationSettings[notificationSettingsPeerId] { + self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed) hasChanges = true } default: @@ -502,17 +547,35 @@ final class MutableChatListView { } } } + + if !transaction.updatedFailedMessagePeerIds.isEmpty { + for i in 0 ..< self.entries.count { + switch self.entries[i] { + case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, previousHasFailed): + if transaction.updatedFailedMessagePeerIds.contains(index.messageIndex.id.peerId) { + let hasFailed = postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId) + if previousHasFailed != hasFailed { + self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed) + hasChanges = true + } + } + default: + continue + } + } + } + if !updatedPeers.isEmpty { for i in 0 ..< self.entries.count { switch self.entries[i] { - case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo): + case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed): var updatedMessage: Message? if let message = message { updatedMessage = updateMessagePeers(message, updatedPeers: updatedPeers) } let updatedPeer = updatedRenderedPeer(peer, updatedPeers: updatedPeers) if updatedMessage != nil || updatedPeer != nil { - self.entries[i] = .MessageEntry(index, updatedMessage ?? message, readState, settings, embeddedState, updatedPeer ?? peer, peerPresence, summaryInfo) + self.entries[i] = .MessageEntry(index, updatedMessage ?? message, readState, settings, embeddedState, updatedPeer ?? peer, peerPresence, summaryInfo, hasFailed) hasChanges = true } default: @@ -523,13 +586,13 @@ final class MutableChatListView { if !updatedPeerPresences.isEmpty { for i in 0 ..< self.entries.count { switch self.entries[i] { - case let .MessageEntry(index, message, readState, settings, embeddedState, peer, _, summaryInfo): + case let .MessageEntry(index, message, readState, settings, embeddedState, peer, _, summaryInfo, hasFailed): var presencePeerId = peer.peerId if let peer = peer.peers[peer.peerId], let associatedPeerId = peer.associatedPeerId { presencePeerId = associatedPeerId } if let presence = updatedPeerPresences[presencePeerId] { - self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, presence, summaryInfo) + self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, presence, summaryInfo, hasFailed) hasChanges = true } default: @@ -540,7 +603,7 @@ final class MutableChatListView { if !transaction.currentUpdatedMessageTagSummaries.isEmpty || !transaction.currentUpdatedMessageActionsSummaries.isEmpty { for i in 0 ..< self.entries.count { switch self.entries[i] { - case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, currentSummary): + case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, currentSummary, hasFailed): var updatedTagSummaryCount: Int32? var updatedActionsSummaryCount: Int32? @@ -561,7 +624,7 @@ final class MutableChatListView { if updatedTagSummaryCount != nil || updatedActionsSummaryCount != nil { let summaryInfo = ChatListMessageTagSummaryInfo(tagSummaryCount: updatedTagSummaryCount ?? currentSummary.tagSummaryCount, actionsSummaryCount: updatedActionsSummaryCount ?? currentSummary.actionsSummaryCount) - self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo) + self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo, hasFailed) hasChanges = true } default: @@ -598,7 +661,25 @@ final class MutableChatListView { } func add(_ initialEntry: MutableChatListEntry, postbox: Postbox) -> Bool { + if let filterPredicate = self.filterPredicate { + switch initialEntry { + case .IntermediateMessageEntry(let index, _, _, _), .MessageEntry(let index, _, _, _, _, _, _, _, _): + if let peer = postbox.peerTable.get(index.messageIndex.id.peerId) { + let isUnread = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false + if !filterPredicate(peer, postbox.peerNotificationSettingsTable.getEffective(index.messageIndex.id.peerId), isUnread) { + return false + } + } else { + return false + } + break + default: + break + } + } + let entry = processedChatListEntry(initialEntry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable) + if self.entries.count == 0 { self.entries.append(entry) return true @@ -735,10 +816,10 @@ final class MutableChatListView { } if let later = self.later { - addedEntries += postbox.fetchLaterChatEntries(groupId: self.groupId, index: later.index.predecessor, count: self.count) + addedEntries += postbox.fetchLaterChatEntries(groupId: self.groupId, index: later.index.predecessor, count: self.count, filterPredicate: filterPredicate) } if let earlier = self.earlier { - addedEntries += postbox.fetchEarlierChatEntries(groupId: self.groupId, index: earlier.index.successor, count: self.count) + addedEntries += postbox.fetchEarlierChatEntries(groupId: self.groupId, index: earlier.index.successor, count: self.count, filterPredicate: filterPredicate) } addedEntries += self.entries @@ -787,7 +868,7 @@ final class MutableChatListView { earlyId = self.entries[i].index } - let earlierEntries = postbox.fetchEarlierChatEntries(groupId: self.groupId, index: earlyId, count: 1) + let earlierEntries = postbox.fetchEarlierChatEntries(groupId: self.groupId, index: earlyId, count: 1, filterPredicate: self.filterPredicate) self.earlier = earlierEntries.first } @@ -798,7 +879,7 @@ final class MutableChatListView { laterId = self.entries[i].index } - let laterEntries = postbox.fetchLaterChatEntries(groupId: self.groupId, index: laterId, count: 1) + let laterEntries = postbox.fetchLaterChatEntries(groupId: self.groupId, index: laterId, count: 1, filterPredicate: self.filterPredicate) self.later = laterEntries.first } } @@ -816,51 +897,48 @@ final class MutableChatListView { private func renderEntry(_ entry: MutableChatListEntry, postbox: Postbox, renderMessage: (IntermediateMessage) -> Message, getPeer: (PeerId) -> Peer?, getPeerNotificationSettings: (PeerId) -> PeerNotificationSettings?, getPeerPresence: (PeerId) -> PeerPresence?) -> MutableChatListEntry? { switch entry { - case let .IntermediateMessageEntry(index, message, combinedReadState, embeddedState): - let renderedMessage: Message? - if let message = message { - renderedMessage = renderMessage(message) + case let .IntermediateMessageEntry(index, message, combinedReadState, embeddedState): + let renderedMessage: Message? + if let message = message { + renderedMessage = renderMessage(message) + } else { + renderedMessage = nil + } + var peers = SimpleDictionary() + var notificationSettings: PeerNotificationSettings? + var presence: PeerPresence? + if let peer = getPeer(index.messageIndex.id.peerId) { + peers[peer.id] = peer + if let associatedPeerId = peer.associatedPeerId { + if let associatedPeer = getPeer(associatedPeerId) { + peers[associatedPeer.id] = associatedPeer + } + notificationSettings = getPeerNotificationSettings(associatedPeerId) + presence = getPeerPresence(associatedPeerId) } else { - renderedMessage = nil + notificationSettings = getPeerNotificationSettings(index.messageIndex.id.peerId) + presence = getPeerPresence(index.messageIndex.id.peerId) } - var peers = SimpleDictionary() - var notificationSettings: PeerNotificationSettings? - var presence: PeerPresence? - if let peer = getPeer(index.messageIndex.id.peerId) { - peers[peer.id] = peer - if let associatedPeerId = peer.associatedPeerId { - if let associatedPeer = getPeer(associatedPeerId) { - peers[associatedPeer.id] = associatedPeer - } - notificationSettings = getPeerNotificationSettings(associatedPeerId) - presence = getPeerPresence(associatedPeerId) - } else { - notificationSettings = getPeerNotificationSettings(index.messageIndex.id.peerId) - presence = getPeerPresence(index.messageIndex.id.peerId) - } + } + + var tagSummaryCount: Int32? + var actionsSummaryCount: Int32? + + if let tagSummary = self.summaryComponents.tagSummary { + let key = MessageHistoryTagsSummaryKey(tag: tagSummary.tag, peerId: index.messageIndex.id.peerId, namespace: tagSummary.namespace) + if let summary = postbox.messageHistoryTagsSummaryTable.get(key) { + tagSummaryCount = summary.count } - - var tagSummaryCount: Int32? - var actionsSummaryCount: Int32? - - if let tagSummary = self.summaryComponents.tagSummary { - let key = MessageHistoryTagsSummaryKey(tag: tagSummary.tag, peerId: index.messageIndex.id.peerId, namespace: tagSummary.namespace) - if let summary = postbox.messageHistoryTagsSummaryTable.get(key) { - tagSummaryCount = summary.count - } - } - - if let actionsSummary = self.summaryComponents.actionsSummary { - let key = PendingMessageActionsSummaryKey(type: actionsSummary.type, peerId: index.messageIndex.id.peerId, namespace: actionsSummary.namespace) - actionsSummaryCount = postbox.pendingMessageActionsMetadataTable.getCount(.peerNamespaceAction(key.peerId, key.namespace, key.type)) - } - - return .MessageEntry(index, renderedMessage, combinedReadState, notificationSettings, embeddedState, RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers), presence, ChatListMessageTagSummaryInfo(tagSummaryCount: tagSummaryCount, actionsSummaryCount: actionsSummaryCount)) - /*case let .IntermediateGroupReferenceEntry(groupId, index, counters): - let message = postbox.messageHistoryTable.getMessage(index.messageIndex).flatMap(postbox.renderIntermediateMessage) - return .GroupReferenceEntry(groupId, index, message, ChatListGroupReferenceTopPeers(postbox: postbox, groupId: groupId), counters ?? ChatListGroupReferenceUnreadCounters(postbox: postbox, groupId: groupId))*/ - default: - return nil + } + + if let actionsSummary = self.summaryComponents.actionsSummary { + let key = PendingMessageActionsSummaryKey(type: actionsSummary.type, peerId: index.messageIndex.id.peerId, namespace: actionsSummary.namespace) + actionsSummaryCount = postbox.pendingMessageActionsMetadataTable.getCount(.peerNamespaceAction(key.peerId, key.namespace, key.type)) + } + + return .MessageEntry(index, renderedMessage, combinedReadState, notificationSettings, embeddedState, RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers), presence, ChatListMessageTagSummaryInfo(tagSummaryCount: tagSummaryCount, actionsSummaryCount: actionsSummaryCount), postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId)) + default: + return nil } } @@ -892,8 +970,8 @@ public final class ChatListView { var entries: [ChatListEntry] = [] for entry in mutableView.entries { switch entry { - case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo): - entries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo)) + case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed): + entries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed)) case let .HoleEntry(hole): entries.append(.HoleEntry(hole)) /*case let .GroupReferenceEntry(groupId, index, message, topPeers, counters): @@ -912,8 +990,8 @@ public final class ChatListView { var additionalItemEntries: [ChatListEntry] = [] for entry in mutableView.additionalItemEntries { switch entry { - case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo): - additionalItemEntries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo)) + case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed): + additionalItemEntries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo, hasFailed)) case .HoleEntry: assertionFailure() /*case .GroupReferenceEntry: diff --git a/submodules/Postbox/Sources/Coding.swift b/submodules/Postbox/Sources/Coding.swift index d69995fb19..e25c870781 100644 --- a/submodules/Postbox/Sources/Coding.swift +++ b/submodules/Postbox/Sources/Coding.swift @@ -77,7 +77,7 @@ public class MemoryBuffer: Equatable, CustomStringConvertible { data.copyBytes(to: self.memory.assumingMemoryBound(to: UInt8.self), count: data.count) self.capacity = data.count self.length = data.count - self.freeWhenDone = false + self.freeWhenDone = true } } @@ -148,7 +148,7 @@ public final class WriteBuffer: MemoryBuffer { self.offset = 0 } - public func write(_ data: UnsafeRawPointer, offset: Int, length: Int) { + public func write(_ data: UnsafeRawPointer, offset: Int = 0, length: Int) { if self.offset + length > self.capacity { self.capacity = self.offset + length + 256 if self.length == 0 { @@ -484,6 +484,22 @@ public final class PostboxEncoder { } } + public func encodeDataArray(_ value: [Data], forKey key: StaticString) { + self.encodeKey(key) + var type: Int8 = ValueType.BytesArray.rawValue + self.buffer.write(&type, offset: 0, length: 1) + var length: Int32 = Int32(value.count) + self.buffer.write(&length, offset: 0, length: 4) + + for object in value { + var length: Int32 = Int32(object.count) + self.buffer.write(&length, offset: 0, length: 4) + object.withUnsafeBytes { (bytes: UnsafePointer) -> Void in + self.buffer.write(bytes, offset: 0, length: Int(length)) + } + } + } + public func encodeObjectDictionary(_ value: [K : V], forKey key: StaticString) where K: PostboxCoding { self.encodeKey(key) var t: Int8 = ValueType.ObjectDictionary.rawValue @@ -1173,6 +1189,31 @@ public final class PostboxDecoder { } } + public func decodeOptionalDataArrayForKey(_ key: StaticString) -> [Data]? { + if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .BytesArray) { + var length: Int32 = 0 + memcpy(&length, self.buffer.memory + self.offset, 4) + self.offset += 4 + + var array: [Data] = [] + array.reserveCapacity(Int(length)) + + var i: Int32 = 0 + while i < length { + var length: Int32 = 0 + memcpy(&length, self.buffer.memory + self.offset, 4) + array.append(Data(bytes: self.buffer.memory.advanced(by: self.offset + 4), count: Int(length))) + self.offset += 4 + Int(length) + + i += 1 + } + + return array + } else { + return nil + } + } + public func decodeObjectArrayForKey(_ key: StaticString) -> [T] where T: PostboxCoding { if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .ObjectArray) { var length: Int32 = 0 diff --git a/submodules/Postbox/Sources/FailedMessagesView.swift b/submodules/Postbox/Sources/FailedMessagesView.swift new file mode 100644 index 0000000000..1a1105f41e --- /dev/null +++ b/submodules/Postbox/Sources/FailedMessagesView.swift @@ -0,0 +1,30 @@ +final class MutableFailedMessageIdsView { + let peerId: PeerId + var ids: Set + + init(peerId: PeerId, ids: [MessageId]) { + self.peerId = peerId + self.ids = Set(ids) + } + func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool { + let ids = transaction.updatedFailedMessageIds.filter { $0.peerId == self.peerId } + let updated = ids != self.ids + self.ids = ids + return updated + } + + func immutableView() -> FailedMessageIdsView { + return FailedMessageIdsView(self.ids) + } + +} + + + +public final class FailedMessageIdsView { + public let ids: Set + + fileprivate init(_ ids: Set) { + self.ids = ids + } +} diff --git a/submodules/Postbox/Sources/GlobalMessageTagsView.swift b/submodules/Postbox/Sources/GlobalMessageTagsView.swift index 0941829082..c52fa1f315 100644 --- a/submodules/Postbox/Sources/GlobalMessageTagsView.swift +++ b/submodules/Postbox/Sources/GlobalMessageTagsView.swift @@ -82,6 +82,7 @@ final class MutableGlobalMessageTagsViewReplayContext { final class MutableGlobalMessageTagsView: MutablePostboxView { private let globalTag: GlobalMessageTags + private let position: MessageIndex private let count: Int private let groupingPredicate: ((Message, Message) -> Bool)? @@ -91,6 +92,7 @@ final class MutableGlobalMessageTagsView: MutablePostboxView { init(postbox: Postbox, globalTag: GlobalMessageTags, position: MessageIndex, count: Int, groupingPredicate: ((Message, Message) -> Bool)?) { self.globalTag = globalTag + self.position = position self.count = count self.groupingPredicate = groupingPredicate @@ -115,63 +117,83 @@ final class MutableGlobalMessageTagsView: MutablePostboxView { let context = MutableGlobalMessageTagsViewReplayContext() + var wasSingleHole = false + if self.entries.count == 1, case .hole = self.entries[0] { + wasSingleHole = true + } + for operation in transaction.currentGlobalTagsOperations { switch operation { - case let .insertMessage(tags, message): + case let .insertMessage(tags, message): + if (self.globalTag.rawValue & tags.rawValue) != 0 { + if self.add(.intermediateMessage(message)) { + hasChanges = true + } + } + case let .insertHole(tags, index): + if (self.globalTag.rawValue & tags.rawValue) != 0 { + if self.add(.hole(index)) { + hasChanges = true + } + } + case let .remove(tagsAndIndices): + var indices = Set() + for (tags, index) in tagsAndIndices { if (self.globalTag.rawValue & tags.rawValue) != 0 { - if self.add(.intermediateMessage(message)) { - hasChanges = true - } + indices.insert(index) } - case let .insertHole(tags, index): - if (self.globalTag.rawValue & tags.rawValue) != 0 { - if self.add(.hole(index)) { - hasChanges = true - } + } + if !indices.isEmpty { + if self.remove(indices, context: context) { + hasChanges = true } - case let .remove(tagsAndIndices): - var indices = Set() - for (tags, index) in tagsAndIndices { - if (self.globalTag.rawValue & tags.rawValue) != 0 { - indices.insert(index) - } - } - if !indices.isEmpty { - if self.remove(indices, context: context) { - hasChanges = true - } - } - case let .updateTimestamp(tags, previousIndex, updatedTimestamp): - if (self.globalTag.rawValue & tags.rawValue) != 0 { - inner: for i in 0 ..< self.entries.count { - let entry = self.entries[i] - if entry.index == previousIndex { - let updatedIndex = MessageIndex(id: entry.index.id, timestamp: updatedTimestamp) - if self.remove(Set([entry.index]), context: context) { + } + case let .updateTimestamp(tags, previousIndex, updatedTimestamp): + if (self.globalTag.rawValue & tags.rawValue) != 0 { + inner: for i in 0 ..< self.entries.count { + let entry = self.entries[i] + if entry.index == previousIndex { + let updatedIndex = MessageIndex(id: entry.index.id, timestamp: updatedTimestamp) + if self.remove(Set([entry.index]), context: context) { + hasChanges = true + } + switch entry { + case .hole: + if self.add(.hole(updatedIndex)) { hasChanges = true } - switch entry { - case .hole: - if self.add(.hole(updatedIndex)) { - hasChanges = true - } - case let .intermediateMessage(message): - if self.add(.intermediateMessage(IntermediateMessage(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: updatedTimestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, authorId: message.authorId, text: message.text, attributesData: message.attributesData, embeddedMediaData: message.embeddedMediaData, referencedMedia: message.referencedMedia))) { - hasChanges = true - } - case let .message(message): - if self.add(.message(Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: updatedTimestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: message.media, peers: message.peers, associatedMessages: message.associatedMessages, associatedMessageIds: message.associatedMessageIds))) { - hasChanges = true - } + case let .intermediateMessage(message): + if self.add(.intermediateMessage(IntermediateMessage(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: updatedTimestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, authorId: message.authorId, text: message.text, attributesData: message.attributesData, embeddedMediaData: message.embeddedMediaData, referencedMedia: message.referencedMedia))) { + hasChanges = true + } + case let .message(message): + if self.add(.message(Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: updatedTimestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: message.media, peers: message.peers, associatedMessages: message.associatedMessages, associatedMessageIds: message.associatedMessageIds))) { + hasChanges = true } - break inner } + break inner } } + } } } if hasChanges || !context.empty() { + if wasSingleHole { + let (entries, lower, upper) = postbox.messageHistoryTable.entriesAround(globalTagMask: self.globalTag, index: self.position, count: self.count) + + self.entries = entries.map { entry -> InternalGlobalMessageTagsEntry in + switch entry { + case let .message(message): + return .intermediateMessage(message) + case let .hole(index): + return .hole(index) + } + } + self.earlier = lower + self.later = upper + } + self.complete(postbox: postbox, context: context) self.render(postbox: postbox) diff --git a/submodules/Postbox/Sources/HistoryTagInfoView.swift b/submodules/Postbox/Sources/HistoryTagInfoView.swift new file mode 100644 index 0000000000..4b145600b2 --- /dev/null +++ b/submodules/Postbox/Sources/HistoryTagInfoView.swift @@ -0,0 +1,76 @@ +import Foundation + +final class MutableHistoryTagInfoView: MutablePostboxView { + fileprivate let peerId: PeerId + fileprivate let tag: MessageTags + + fileprivate var currentIndex: MessageIndex? + + init(postbox: Postbox, peerId: PeerId, tag: MessageTags) { + self.peerId = peerId + self.tag = tag + for namespace in postbox.messageHistoryIndexTable.existingNamespaces(peerId: self.peerId) { + if let index = postbox.messageHistoryTagsTable.latestIndex(tag: self.tag, peerId: self.peerId, namespace: namespace) { + self.currentIndex = index + break + } + } + } + + func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool { + if let operations = transaction.currentOperationsByPeerId[self.peerId] { + var updated = false + var refresh = false + for operation in operations { + switch operation { + case let .InsertMessage(message): + if self.currentIndex == nil { + if message.tags.contains(self.tag) { + self.currentIndex = message.index + updated = true + } + } + case let .Remove(indicesAndTags): + if self.currentIndex != nil { + for (index, tags) in indicesAndTags { + if tags.contains(self.tag) { + if index == self.currentIndex { + self.currentIndex = nil + updated = true + refresh = true + } + } + } + } + default: + break + } + } + + if refresh { + for namespace in postbox.messageHistoryIndexTable.existingNamespaces(peerId: self.peerId) { + if let index = postbox.messageHistoryTagsTable.latestIndex(tag: self.tag, peerId: self.peerId, namespace: namespace) { + self.currentIndex = index + break + } + } + } + + return updated + } else { + return false + } + } + + func immutableView() -> PostboxView { + return HistoryTagInfoView(self) + } +} + +public final class HistoryTagInfoView: PostboxView { + public let isEmpty: Bool + + init(_ view: MutableHistoryTagInfoView) { + self.isEmpty = view.currentIndex == nil + } +} diff --git a/submodules/Postbox/Sources/ItemCacheTable.swift b/submodules/Postbox/Sources/ItemCacheTable.swift index 60bfed7d1b..82a782081f 100644 --- a/submodules/Postbox/Sources/ItemCacheTable.swift +++ b/submodules/Postbox/Sources/ItemCacheTable.swift @@ -37,6 +37,17 @@ final class ItemCacheTable: Table { return key } + private func lowerBound(collectionId: ItemCacheCollectionId) -> ValueBoxKey { + let key = ValueBoxKey(length: 1 + 1) + key.setInt8(0, value: ItemCacheSection.items.rawValue) + key.setInt8(1, value: collectionId) + return key + } + + private func upperBound(collectionId: ItemCacheCollectionId) -> ValueBoxKey { + return self.lowerBound(collectionId: collectionId).successor + } + private func itemIdToAccessIndexKey(id: ItemCacheEntryId) -> ValueBoxKey { let key = ValueBoxKey(length: 1 + 1 + id.key.length) key.setInt8(0, value: ItemCacheSection.accessIndexToItemId.rawValue) @@ -72,6 +83,10 @@ final class ItemCacheTable: Table { self.valueBox.remove(self.table, key: self.itemKey(id: id), secure: false) } + func removeAll(collectionId: ItemCacheCollectionId) { + self.valueBox.removeRange(self.table, start: self.lowerBound(collectionId: collectionId), end: self.upperBound(collectionId: collectionId)) + } + override func clearMemoryCache() { } diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index a12e7a9b8f..43da45a7c8 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -85,6 +85,9 @@ public enum MediaResourceDataFetchError { } public enum CachedMediaResourceRepresentationResult { + case reset + case data(Data) + case done case temporaryPath(String) case tempFile(TempBoxFile) } @@ -107,9 +110,19 @@ private struct CachedMediaResourceRepresentationKey: Hashable { } } +private final class CachedMediaResourceRepresentationSubscriber { + let update: (MediaResourceData) -> Void + let onlyComplete: Bool + + init(update: @escaping (MediaResourceData) -> Void, onlyComplete: Bool) { + self.update = update + self.onlyComplete = onlyComplete + } +} + private final class CachedMediaResourceRepresentationContext { var currentData: MediaResourceData? - let dataSubscribers = Bag<(MediaResourceData) -> Void>() + let dataSubscribers = Bag() let disposable = MetaDisposable() var initialized = false } @@ -191,7 +204,7 @@ public final class MediaBox { return ResourceStorePaths(partial: "\(self.basePath)/\(fileNameForId(id))_partial", complete: "\(self.basePath)/\(fileNameForId(id))") } - private func cachedRepresentationPathForId(_ id: MediaResourceId, representation: CachedMediaResourceRepresentation) -> String { + private func cachedRepresentationPathsForId(_ id: MediaResourceId, representation: CachedMediaResourceRepresentation) -> ResourceStorePaths { let cacheString: String switch representation.keepDuration { case .general: @@ -199,6 +212,17 @@ public final class MediaBox { case .shortLived: cacheString = "short-cache" } + return ResourceStorePaths(partial: "\(self.basePath)/\(cacheString)/\(fileNameForId(id))_partial:\(representation.uniqueId)", complete: "\(self.basePath)/\(cacheString)/\(fileNameForId(id)):\(representation.uniqueId)") + } + + public func cachedRepresentationCompletePath(_ id: MediaResourceId, representation: CachedMediaResourceRepresentation) -> String { + let cacheString: String + switch representation.keepDuration { + case .general: + cacheString = "cache" + case .shortLived: + cacheString = "short-cache" + } return "\(self.basePath)/\(cacheString)/\(fileNameForId(id)):\(representation.uniqueId)" } @@ -229,11 +253,16 @@ public final class MediaBox { } public func moveResourceData(from: MediaResourceId, to: MediaResourceId) { + if from.isEqual(to: to) { + return + } self.dataQueue.async { let pathsFrom = self.storePathsForId(from) let pathsTo = self.storePathsForId(to) link(pathsFrom.partial, pathsTo.partial) link(pathsFrom.complete, pathsTo.complete) + unlink(pathsFrom.partial) + unlink(pathsFrom.complete) } } @@ -657,7 +686,7 @@ public final class MediaBox { public func storeCachedResourceRepresentation(_ resource: MediaResource, representation: CachedMediaResourceRepresentation, data: Data) { self.dataQueue.async { - let path = self.cachedRepresentationPathForId(resource.id, representation: representation) + let path = self.cachedRepresentationPathsForId(resource.id, representation: representation).complete let _ = try? data.write(to: URL(fileURLWithPath: path)) } } @@ -667,26 +696,26 @@ public final class MediaBox { let disposable = MetaDisposable() let begin: () -> Void = { - let path = self.cachedRepresentationPathForId(resource.id, representation: representation) - if let size = fileSize(path) { + let paths = self.cachedRepresentationPathsForId(resource.id, representation: representation) + if let size = fileSize(paths.complete) { self.timeBasedCleanup.touch(paths: [ - path + paths.complete ]) if let pathExtension = pathExtension { - let symlinkPath = path + ".\(pathExtension)" + let symlinkPath = paths.complete + ".\(pathExtension)" if fileSize(symlinkPath) == nil { - let _ = try? FileManager.default.linkItem(atPath: path, toPath: symlinkPath) + let _ = try? FileManager.default.linkItem(atPath: paths.complete, toPath: symlinkPath) } subscriber.putNext(MediaResourceData(path: symlinkPath, offset: 0, size: size, complete: true)) subscriber.putCompletion() } else { - subscriber.putNext(MediaResourceData(path: path, offset: 0, size: size, complete: true)) + subscriber.putNext(MediaResourceData(path: paths.complete, offset: 0, size: size, complete: true)) subscriber.putCompletion() } } else if fetch { if attemptSynchronously && complete { - subscriber.putNext(MediaResourceData(path: path, offset: 0, size: 0, complete: false)) + subscriber.putNext(MediaResourceData(path: paths.partial, offset: 0, size: 0, complete: false)) } self.dataQueue.async { let key = CachedMediaResourceRepresentationKey(resourceId: resource.id, representation: representation) @@ -698,7 +727,7 @@ public final class MediaBox { self.cachedRepresentationContexts[key] = context } - let index = context.dataSubscribers.add(({ data in + let index = context.dataSubscribers.add(CachedMediaResourceRepresentationSubscriber(update: { data in if !complete || data.complete { if let pathExtension = pathExtension, data.complete { let symlinkPath = data.path + ".\(pathExtension)" @@ -713,7 +742,7 @@ public final class MediaBox { if data.complete { subscriber.putCompletion() } - })) + }, onlyComplete: complete)) if let currentData = context.currentData { if !complete || currentData.complete { subscriber.putNext(currentData) @@ -722,7 +751,7 @@ public final class MediaBox { subscriber.putCompletion() } } else if !complete { - subscriber.putNext(MediaResourceData(path: path, offset: 0, size: 0, complete: false)) + subscriber.putNext(MediaResourceData(path: paths.partial, offset: 0, size: 0, complete: false)) } disposable.set(ActionDisposable { [weak context] in @@ -747,32 +776,65 @@ public final class MediaBox { } |> deliverOn(self.dataQueue) context.disposable.set(signal.start(next: { [weak self, weak context] next in + guard let strongSelf = self else { + return + } if let next = next { + var isDone = false switch next { - case let .temporaryPath(temporaryPath): - rename(temporaryPath, path) - case let .tempFile(tempFile): - rename(tempFile.path, path) - TempBox.shared.dispose(tempFile) + case let .temporaryPath(temporaryPath): + rename(temporaryPath, paths.complete) + isDone = true + case let .tempFile(tempFile): + rename(tempFile.path, paths.complete) + TempBox.shared.dispose(tempFile) + isDone = true + case .reset: + let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .readwrite) + file?.truncate(count: 0) + unlink(paths.complete) + case let .data(dataPart): + let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .append) + let dataCount = dataPart.count + dataPart.withUnsafeBytes { (bytes: UnsafePointer) -> Void in + file?.write(bytes, count: dataCount) + } + case .done: + link(paths.partial, paths.complete) + isDone = true } if let strongSelf = self, let currentContext = strongSelf.cachedRepresentationContexts[key], currentContext === context { - currentContext.disposable.dispose() - strongSelf.cachedRepresentationContexts.removeValue(forKey: key) - if let size = fileSize(path) { - let data = MediaResourceData(path: path, offset: 0, size: size, complete: true) + if isDone { + currentContext.disposable.dispose() + strongSelf.cachedRepresentationContexts.removeValue(forKey: key) + } + if let size = fileSize(paths.complete) { + let data = MediaResourceData(path: paths.complete, offset: 0, size: size, complete: isDone) currentContext.currentData = data for subscriber in currentContext.dataSubscribers.copyItems() { - subscriber(data) + if !subscriber.onlyComplete || isDone { + subscriber.update(data) + } + } + } else if let size = fileSize(paths.partial) { + let data = MediaResourceData(path: paths.partial, offset: 0, size: size, complete: isDone) + currentContext.currentData = data + for subscriber in currentContext.dataSubscribers.copyItems() { + if !subscriber.onlyComplete || isDone { + subscriber.update(data) + } } } } } else { if let strongSelf = self, let context = strongSelf.cachedRepresentationContexts[key] { - let data = MediaResourceData(path: path, offset: 0, size: 0, complete: false) + let data = MediaResourceData(path: paths.partial, offset: 0, size: 0, complete: false) context.currentData = data for subscriber in context.dataSubscribers.copyItems() { - subscriber(data) + if !subscriber.onlyComplete { + subscriber.update(data) + } } } } @@ -780,7 +842,7 @@ public final class MediaBox { } } } else { - subscriber.putNext(MediaResourceData(path: path, offset: 0, size: 0, complete: false)) + subscriber.putNext(MediaResourceData(path: paths.partial, offset: 0, size: 0, complete: false)) subscriber.putCompletion() } } @@ -929,6 +991,32 @@ public final class MediaBox { unlink(paths.partial + ".meta") self.fileContexts.removeValue(forKey: id) } + + let uniqueIds = Set(ids.map { $0.id.uniqueId }) + + var pathsToDelete: [String] = [] + + for cacheType in ["cache", "short-cache"] { + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: "\(self.basePath)/\(cacheType)"), includingPropertiesForKeys: [], options: [.skipsSubdirectoryDescendants], errorHandler: nil) { + while let item = enumerator.nextObject() { + guard let url = item as? NSURL, let path = url.path, let fileName = url.lastPathComponent else { + continue + } + + if let range = fileName.range(of: ":") { + let resourceId = String(fileName[fileName.startIndex ..< range.lowerBound]) + if uniqueIds.contains(resourceId) { + pathsToDelete.append(path) + } + } + } + } + } + + for path in pathsToDelete { + unlink(path) + } + subscriber.putCompletion() } return EmptyDisposable diff --git a/submodules/Postbox/Sources/Message.swift b/submodules/Postbox/Sources/Message.swift index 5960cc1bc4..3968afde45 100644 --- a/submodules/Postbox/Sources/Message.swift +++ b/submodules/Postbox/Sources/Message.swift @@ -525,6 +525,10 @@ public final class Message { return Message(stableId: self.stableId, stableVersion: self.stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, author: self.author, text: self.text, attributes: self.attributes, media: media, peers: self.peers, associatedMessages: self.associatedMessages, associatedMessageIds: self.associatedMessageIds) } + public func withUpdatedPeers(_ peers: SimpleDictionary) -> Message { + return Message(stableId: self.stableId, stableVersion: self.stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, author: self.author, text: self.text, attributes: self.attributes, media: self.media, peers: peers, associatedMessages: self.associatedMessages, associatedMessageIds: self.associatedMessageIds) + } + public func withUpdatedFlags(_ flags: MessageFlags) -> Message { return Message(stableId: self.stableId, stableVersion: self.stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, timestamp: self.timestamp, flags: flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, author: self.author, text: self.text, attributes: self.attributes, media: self.media, peers: self.peers, associatedMessages: self.associatedMessages, associatedMessageIds: self.associatedMessageIds) } diff --git a/submodules/Postbox/Sources/MessageHistoryFailedTable.swift b/submodules/Postbox/Sources/MessageHistoryFailedTable.swift new file mode 100644 index 0000000000..eb9c7fa676 --- /dev/null +++ b/submodules/Postbox/Sources/MessageHistoryFailedTable.swift @@ -0,0 +1,74 @@ +import Foundation + +final class MessageHistoryFailedTable: Table { + static func tableSpec(_ id: Int32) -> ValueBoxTable { + return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true) + } + + private let sharedKey = ValueBoxKey(length: 8 + 4 + 4) + + private(set) var updatedPeerIds = Set() + private(set) var updatedMessageIds = Set() + + private func key(_ id: MessageId) -> ValueBoxKey { + self.sharedKey.setInt64(0, value: id.peerId.toInt64()) + self.sharedKey.setInt32(8, value: id.namespace) + self.sharedKey.setInt32(8 + 4, value: id.id) + + return self.sharedKey + } + + private func lowerBound(peerId: PeerId) -> ValueBoxKey { + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: peerId.toInt64()) + return key + } + + private func upperBound(peerId: PeerId) -> ValueBoxKey { + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: peerId.toInt64()) + return key.successor + } + + func add(_ id: MessageId) { + self.valueBox.set(self.table, key: self.key(id), value: MemoryBuffer()) + self.updatedPeerIds.insert(id.peerId) + self.updatedMessageIds.insert(id) + } + + func remove(_ id: MessageId) { + self.valueBox.remove(self.table, key: self.key(id), secure: false) + self.updatedPeerIds.insert(id.peerId) + self.updatedMessageIds.remove(id) + } + + func get(peerId: PeerId) -> [MessageId] { + var ids:[MessageId] = [] + self.valueBox.range(self.table, start: self.lowerBound(peerId: peerId), end: self.upperBound(peerId: peerId), keys: { key in + + let peerId = PeerId(key.getInt64(0)) + let namespace = key.getInt32(8) + let id = key.getInt32(8 + 4) + ids.append(MessageId(peerId: peerId, namespace: namespace, id: id)) + + return false + }, limit: 100) + + self.updatedMessageIds = self.updatedMessageIds.union(ids) + + return ids + } + + func contains(peerId: PeerId) -> Bool { + var result = false + self.valueBox.range(self.table, start: self.lowerBound(peerId: peerId), end: self.upperBound(peerId: peerId), keys: { _ in + result = true + return false + }, limit: 1) + return result + } + + override func beforeCommit() { + self.updatedPeerIds.removeAll() + } +} diff --git a/submodules/Postbox/Sources/MessageHistoryHolesView.swift b/submodules/Postbox/Sources/MessageHistoryHolesView.swift index 540b1b8317..14c65efea0 100644 --- a/submodules/Postbox/Sources/MessageHistoryHolesView.swift +++ b/submodules/Postbox/Sources/MessageHistoryHolesView.swift @@ -4,11 +4,13 @@ public struct MessageHistoryHolesViewEntry: Equatable, Hashable { public let hole: MessageHistoryViewHole public let direction: MessageHistoryViewRelativeHoleDirection public let space: MessageHistoryHoleSpace + public let count: Int - public init(hole: MessageHistoryViewHole, direction: MessageHistoryViewRelativeHoleDirection, space: MessageHistoryHoleSpace) { + public init(hole: MessageHistoryViewHole, direction: MessageHistoryViewRelativeHoleDirection, space: MessageHistoryHoleSpace, count: Int) { self.hole = hole self.direction = direction self.space = space + self.count = count } } diff --git a/submodules/Postbox/Sources/MessageHistoryMetadataTable.swift b/submodules/Postbox/Sources/MessageHistoryMetadataTable.swift index 5fe138ec64..7032f2bce6 100644 --- a/submodules/Postbox/Sources/MessageHistoryMetadataTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryMetadataTable.swift @@ -10,6 +10,7 @@ private enum MetadataPrefix: Int8 { case GroupFeedIndexInitialized = 7 case ShouldReindexUnreadCounts = 8 case PeerHistoryInitialized = 9 + case ShouldReindexUnreadCountsState = 10 } public struct ChatListTotalUnreadCounters: PostboxCoding, Equatable { @@ -148,6 +149,21 @@ final class MessageHistoryMetadataTable: Table { } } + func setShouldReindexUnreadCountsState(value: Int32) { + var value = value + self.valueBox.set(self.table, key: self.key(MetadataPrefix.ShouldReindexUnreadCountsState), value: MemoryBuffer(memory: &value, capacity: 4, length: 4, freeWhenDone: false)) + } + + func getShouldReindexUnreadCountsState() -> Int32? { + if let value = self.valueBox.get(self.table, key: self.key(MetadataPrefix.ShouldReindexUnreadCountsState)) { + var version: Int32 = 0 + value.read(&version, offset: 0, length: 4) + return version + } else { + return nil + } + } + func setInitialized(_ peerId: PeerId) { self.initializedHistoryPeerIds.insert(peerId) self.sharedBuffer.reset() diff --git a/submodules/Postbox/Sources/MessageHistoryTable.swift b/submodules/Postbox/Sources/MessageHistoryTable.swift index 5f40bd055b..f0fb7ca809 100644 --- a/submodules/Postbox/Sources/MessageHistoryTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryTable.swift @@ -69,6 +69,7 @@ final class MessageHistoryTable: Table { let historyMetadataTable: MessageHistoryMetadataTable let globallyUniqueMessageIdsTable: MessageGloballyUniqueIdTable let unsentTable: MessageHistoryUnsentTable + let failedTable: MessageHistoryFailedTable let tagsTable: MessageHistoryTagsTable let globalTagsTable: GlobalMessageHistoryTagsTable let localTagsTable: LocalMessageHistoryTagsTable @@ -78,7 +79,7 @@ final class MessageHistoryTable: Table { let summaryTable: MessageHistoryTagsSummaryTable let pendingActionsTable: PendingMessageActionsTable - init(valueBox: ValueBox, table: ValueBoxTable, seedConfiguration: SeedConfiguration, messageHistoryIndexTable: MessageHistoryIndexTable, messageHistoryHoleIndexTable: MessageHistoryHoleIndexTable, messageMediaTable: MessageMediaTable, historyMetadataTable: MessageHistoryMetadataTable, globallyUniqueMessageIdsTable: MessageGloballyUniqueIdTable, unsentTable: MessageHistoryUnsentTable, tagsTable: MessageHistoryTagsTable, globalTagsTable: GlobalMessageHistoryTagsTable, localTagsTable: LocalMessageHistoryTagsTable, readStateTable: MessageHistoryReadStateTable, synchronizeReadStateTable: MessageHistorySynchronizeReadStateTable, textIndexTable: MessageHistoryTextIndexTable, summaryTable: MessageHistoryTagsSummaryTable, pendingActionsTable: PendingMessageActionsTable) { + init(valueBox: ValueBox, table: ValueBoxTable, seedConfiguration: SeedConfiguration, messageHistoryIndexTable: MessageHistoryIndexTable, messageHistoryHoleIndexTable: MessageHistoryHoleIndexTable, messageMediaTable: MessageMediaTable, historyMetadataTable: MessageHistoryMetadataTable, globallyUniqueMessageIdsTable: MessageGloballyUniqueIdTable, unsentTable: MessageHistoryUnsentTable, failedTable: MessageHistoryFailedTable, tagsTable: MessageHistoryTagsTable, globalTagsTable: GlobalMessageHistoryTagsTable, localTagsTable: LocalMessageHistoryTagsTable, readStateTable: MessageHistoryReadStateTable, synchronizeReadStateTable: MessageHistorySynchronizeReadStateTable, textIndexTable: MessageHistoryTextIndexTable, summaryTable: MessageHistoryTagsSummaryTable, pendingActionsTable: PendingMessageActionsTable) { self.seedConfiguration = seedConfiguration self.messageHistoryIndexTable = messageHistoryIndexTable self.messageHistoryHoleIndexTable = messageHistoryHoleIndexTable @@ -86,6 +87,7 @@ final class MessageHistoryTable: Table { self.historyMetadataTable = historyMetadataTable self.globallyUniqueMessageIdsTable = globallyUniqueMessageIdsTable self.unsentTable = unsentTable + self.failedTable = failedTable self.tagsTable = tagsTable self.globalTagsTable = globalTagsTable self.localTagsTable = localTagsTable @@ -249,6 +251,9 @@ final class MessageHistoryTable: Table { if message.flags.contains(.Unsent) && !message.flags.contains(.Failed) { self.unsentTable.add(message.id, operations: &unsentMessageOperations) } + if message.flags.contains(.Failed) { + self.failedTable.add(message.id) + } let tags = message.tags.rawValue if tags != 0 { for i in 0 ..< 32 { @@ -1194,6 +1199,9 @@ final class MessageHistoryTable: Table { if message.flags.contains(.Unsent) && !message.flags.contains(.Failed) { self.unsentTable.remove(index.id, operations: &unsentMessageOperations) } + if message.flags.contains(.Failed) { + self.failedTable.remove(message.id) + } if let globallyUniqueId = message.globallyUniqueId { self.globallyUniqueMessageIdsTable.remove(peerId: message.id.peerId, globallyUniqueId: globallyUniqueId) @@ -1372,7 +1380,7 @@ final class MessageHistoryTable: Table { } } - self.valueBox.remove(self.table, key: self.key(index), secure: false) + self.valueBox.remove(self.table, key: self.key(index), secure: true) let updatedIndex = message.index @@ -1387,7 +1395,7 @@ final class MessageHistoryTable: Table { } } - if previousMessage.globalTags != message.globalTags || (previousMessage.index != message.index && (!previousMessage.globalTags.isEmpty || !message.globalTags.isEmpty)) { + if !previousMessage.globalTags.isEmpty || !message.globalTags.isEmpty { if !previousMessage.globalTags.isEmpty { for tag in previousMessage.globalTags { self.globalTagsTable.remove(tag, index: index) @@ -1447,6 +1455,23 @@ final class MessageHistoryTable: Table { break } + if previousMessage.id != message.id { + if previousMessage.flags.contains(.Failed) { + self.failedTable.remove(previousMessage.id) + } + if message.flags.contains(.Failed) { + self.failedTable.add(message.id) + } + } else { + if previousMessage.flags.contains(.Failed) != message.flags.contains(.Failed) { + if previousMessage.flags.contains(.Failed) { + self.failedTable.remove(previousMessage.id) + } else { + self.failedTable.add(message.id) + } + } + } + if self.seedConfiguration.peerNamespacesRequiringMessageTextIndex.contains(message.id.peerId.namespace) { if previousMessage.id != message.id || previousMessage.text != message.text || previousMessage.tags != message.tags { self.textIndexTable.remove(messageId: previousMessage.id) @@ -2250,6 +2275,54 @@ final class MessageHistoryTable: Table { return Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: message.timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: forwardInfo, author: author, text: message.text, attributes: parsedAttributes, media: parsedMedia, peers: peers, associatedMessages: associatedMessages, associatedMessageIds: associatedMessageIds) } + func renderMessagePeers(_ message: Message, peerTable: PeerTable) -> Message { + var author: Peer? + var peers = SimpleDictionary() + if let authorId = message.author?.id { + author = peerTable.get(authorId) + } + + if let chatPeer = peerTable.get(message.id.peerId) { + peers[chatPeer.id] = chatPeer + + if let associatedPeerId = chatPeer.associatedPeerId { + if let peer = peerTable.get(associatedPeerId) { + peers[peer.id] = peer + } + } + } + + for media in message.media { + for peerId in media.peerIds { + if let peer = peerTable.get(peerId) { + peers[peer.id] = peer + } + } + } + + for attribute in message.attributes { + for peerId in attribute.associatedPeerIds { + if let peer = peerTable.get(peerId) { + peers[peer.id] = peer + } + } + } + + return message.withUpdatedPeers(peers) + } + + func renderAssociatedMessages(associatedMessageIds: [MessageId], peerTable: PeerTable) -> SimpleDictionary { + var associatedMessages = SimpleDictionary() + for messageId in associatedMessageIds { + if let index = self.messageHistoryIndexTable.getIndex(messageId) { + if let message = self.getMessage(index) { + associatedMessages[messageId] = self.renderMessage(message, peerTable: peerTable, addAssociatedMessages: false) + } + } + } + return associatedMessages + } + private func globalTagsIntermediateEntry(_ entry: IntermediateMessageHistoryEntry) -> IntermediateGlobalMessageTagsEntry? { return .message(entry.message) } diff --git a/submodules/Postbox/Sources/MessageHistoryTagsTable.swift b/submodules/Postbox/Sources/MessageHistoryTagsTable.swift index 0f4c7f6570..2af930855d 100644 --- a/submodules/Postbox/Sources/MessageHistoryTagsTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryTagsTable.swift @@ -132,6 +132,15 @@ class MessageHistoryTagsTable: Table { return Int(self.valueBox.count(self.table, start: lowerBoundKey, end: upperBoundKey)) } + func latestIndex(tag: MessageTags, peerId: PeerId, namespace: MessageId.Namespace) -> MessageIndex? { + var result: MessageIndex? + self.valueBox.range(self.table, start: self.lowerBound(tag: tag, peerId: peerId, namespace: namespace), end: self.upperBound(tag: tag, peerId: peerId, namespace: namespace), keys: { key in + result = extractKey(key) + return true + }, limit: 1) + return result + } + func findRandomIndex(peerId: PeerId, namespace: MessageId.Namespace, tag: MessageTags, ignoreIds: ([MessageId], Set), isMessage: (MessageIndex) -> Bool) -> MessageIndex? { var indices: [MessageIndex] = [] self.valueBox.range(self.table, start: self.lowerBound(tag: tag, peerId: peerId, namespace: namespace), end: self.upperBound(tag: tag, peerId: peerId, namespace: namespace), keys: { key in diff --git a/submodules/Postbox/Sources/MessageHistoryTextIndexTable.swift b/submodules/Postbox/Sources/MessageHistoryTextIndexTable.swift index ca8efff04c..710a4caf36 100644 --- a/submodules/Postbox/Sources/MessageHistoryTextIndexTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryTextIndexTable.swift @@ -66,7 +66,7 @@ final class MessageHistoryTextIndexTable { } func remove(messageId: MessageId) { - self.valueBox.fullTextRemove(self.table, itemId: itemId(messageId)) + self.valueBox.fullTextRemove(self.table, itemId: itemId(messageId), secure: true) } func search(peerId: PeerId?, text: String, tags: MessageTags?) -> [MessageId] { diff --git a/submodules/Postbox/Sources/MessageHistoryView.swift b/submodules/Postbox/Sources/MessageHistoryView.swift index 156842d538..615ef4082c 100644 --- a/submodules/Postbox/Sources/MessageHistoryView.swift +++ b/submodules/Postbox/Sources/MessageHistoryView.swift @@ -1,12 +1,23 @@ import Foundation -public struct MessageHistoryViewPeerHole: Equatable, Hashable { +public struct MessageHistoryViewPeerHole: Equatable, Hashable, CustomStringConvertible { public let peerId: PeerId public let namespace: MessageId.Namespace + + public var description: String { + return "peerId: \(self.peerId), namespace: \(self.namespace)" + } } -public enum MessageHistoryViewHole: Equatable, Hashable { +public enum MessageHistoryViewHole: Equatable, Hashable, CustomStringConvertible { case peer(MessageHistoryViewPeerHole) + + public var description: String { + switch self { + case let .peer(hole): + return "peer(\(hole))" + } + } } public struct MessageHistoryMessageEntry { @@ -18,109 +29,118 @@ public struct MessageHistoryMessageEntry { enum MutableMessageHistoryEntry { case IntermediateMessageEntry(IntermediateMessage, MessageHistoryEntryLocation?, MessageHistoryEntryMonthLocation?) - case MessageEntry(MessageHistoryMessageEntry) + case MessageEntry(MessageHistoryMessageEntry, reloadAssociatedMessages: Bool, reloadPeers: Bool) var index: MessageIndex { switch self { - case let .IntermediateMessageEntry(message, _, _): - return message.index - case let .MessageEntry(message): - return message.message.index + case let .IntermediateMessageEntry(message, _, _): + return message.index + case let .MessageEntry(message, _, _): + return message.message.index } } var tags: MessageTags { switch self { - case let .IntermediateMessageEntry(message, _, _): - return message.tags - case let .MessageEntry(message): - return message.message.tags + case let .IntermediateMessageEntry(message, _, _): + return message.tags + case let .MessageEntry(message, _, _): + return message.message.tags } } func updatedLocation(_ location: MessageHistoryEntryLocation?) -> MutableMessageHistoryEntry { switch self { - case let .IntermediateMessageEntry(message, _, monthLocation): - return .IntermediateMessageEntry(message, location, monthLocation) - case let .MessageEntry(message): - return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: location, monthLocation: message.monthLocation, attributes: message.attributes)) + case let .IntermediateMessageEntry(message, _, monthLocation): + return .IntermediateMessageEntry(message, location, monthLocation) + case let .MessageEntry(message, reloadAssociatedMessages, reloadPeers): + return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: location, monthLocation: message.monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers) } } func updatedMonthLocation(_ monthLocation: MessageHistoryEntryMonthLocation?) -> MutableMessageHistoryEntry { switch self { - case let .IntermediateMessageEntry(message, location, _): - return .IntermediateMessageEntry(message, location, monthLocation) - case let .MessageEntry(message): - return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: message.location, monthLocation: monthLocation, attributes: message.attributes)) + case let .IntermediateMessageEntry(message, location, _): + return .IntermediateMessageEntry(message, location, monthLocation) + case let .MessageEntry(message, reloadAssociatedMessages, reloadPeers): + return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: message.location, monthLocation: monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers) } } func offsetLocationForInsertedIndex(_ index: MessageIndex) -> MutableMessageHistoryEntry { switch self { - case let .IntermediateMessageEntry(message, location, monthLocation): - if let location = location { - if MessageIndex(id: message.id, timestamp: message.timestamp) > index { - return .IntermediateMessageEntry(message, MessageHistoryEntryLocation(index: location.index + 1, count: location.count + 1), monthLocation) - } else { - return .IntermediateMessageEntry(message, MessageHistoryEntryLocation(index: location.index, count: location.count - 1), monthLocation) - } + case let .IntermediateMessageEntry(message, location, monthLocation): + if let location = location { + if MessageIndex(id: message.id, timestamp: message.timestamp) > index { + return .IntermediateMessageEntry(message, MessageHistoryEntryLocation(index: location.index + 1, count: location.count + 1), monthLocation) } else { - return self + return .IntermediateMessageEntry(message, MessageHistoryEntryLocation(index: location.index, count: location.count - 1), monthLocation) } - case let .MessageEntry(message): - if let location = message.location { - if message.message.index > index { - return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: MessageHistoryEntryLocation(index: location.index + 1, count: location.count + 1), monthLocation: message.monthLocation, attributes: message.attributes)) - } else { - return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: MessageHistoryEntryLocation(index: location.index, count: location.count + 1), monthLocation: message.monthLocation, attributes: message.attributes)) - } + } else { + return self + } + case let .MessageEntry(message, reloadAssociatedMessages, reloadPeers): + if let location = message.location { + if message.message.index > index { + return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: MessageHistoryEntryLocation(index: location.index + 1, count: location.count + 1), monthLocation: message.monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers) } else { - return self + return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: MessageHistoryEntryLocation(index: location.index, count: location.count + 1), monthLocation: message.monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers) } + } else { + return self + } } } func offsetLocationForRemovedIndex(_ index: MessageIndex) -> MutableMessageHistoryEntry { switch self { - case let .IntermediateMessageEntry(message, location, monthLocation): - if let location = location { - if MessageIndex(id: message.id, timestamp: message.timestamp) > index { - //assert(location.index > 0) - //assert(location.count != 0) - return .IntermediateMessageEntry(message, MessageHistoryEntryLocation(index: location.index - 1, count: location.count - 1), monthLocation) - } else { - //assert(location.count != 0) - return .IntermediateMessageEntry(message, MessageHistoryEntryLocation(index: location.index, count: location.count - 1), monthLocation) - } + case let .IntermediateMessageEntry(message, location, monthLocation): + if let location = location { + if MessageIndex(id: message.id, timestamp: message.timestamp) > index { + //assert(location.index > 0) + //assert(location.count != 0) + return .IntermediateMessageEntry(message, MessageHistoryEntryLocation(index: location.index - 1, count: location.count - 1), monthLocation) } else { - return self + //assert(location.count != 0) + return .IntermediateMessageEntry(message, MessageHistoryEntryLocation(index: location.index, count: location.count - 1), monthLocation) } - case let .MessageEntry(message): - if let location = message.location { - if message.message.index > index { - //assert(location.index > 0) - //assert(location.count != 0) - return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: MessageHistoryEntryLocation(index: location.index - 1, count: location.count - 1), monthLocation: message.monthLocation, attributes: message.attributes)) - } else { - //assert(location.count != 0) - return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: MessageHistoryEntryLocation(index: location.index, count: location.count - 1), monthLocation: message.monthLocation, attributes: message.attributes)) - } + } else { + return self + } + case let .MessageEntry(message, reloadAssociatedMessages, reloadPeers): + if let location = message.location { + if message.message.index > index { + //assert(location.index > 0) + //assert(location.count != 0) + return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: MessageHistoryEntryLocation(index: location.index - 1, count: location.count - 1), monthLocation: message.monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers) } else { - return self + //assert(location.count != 0) + return .MessageEntry(MessageHistoryMessageEntry(message: message.message, location: MessageHistoryEntryLocation(index: location.index, count: location.count - 1), monthLocation: message.monthLocation, attributes: message.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers) } + } else { + return self + } } } func updatedTimestamp(_ timestamp: Int32) -> MutableMessageHistoryEntry { switch self { - case let .IntermediateMessageEntry(message, location, monthLocation): - let updatedMessage = IntermediateMessage(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, authorId: message.authorId, text: message.text, attributesData: message.attributesData, embeddedMediaData: message.embeddedMediaData, referencedMedia: message.referencedMedia) - return .IntermediateMessageEntry(updatedMessage, location, monthLocation) - case let .MessageEntry(value): - let message = value.message - let updatedMessage = Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: message.media, peers: message.peers, associatedMessages: message.associatedMessages, associatedMessageIds: message.associatedMessageIds) - return .MessageEntry(MessageHistoryMessageEntry(message: updatedMessage, location: value.location, monthLocation: value.monthLocation, attributes: value.attributes)) + case let .IntermediateMessageEntry(message, location, monthLocation): + let updatedMessage = IntermediateMessage(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, authorId: message.authorId, text: message.text, attributesData: message.attributesData, embeddedMediaData: message.embeddedMediaData, referencedMedia: message.referencedMedia) + return .IntermediateMessageEntry(updatedMessage, location, monthLocation) + case let .MessageEntry(value, reloadAssociatedMessages, reloadPeers): + let message = value.message + let updatedMessage = Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: message.media, peers: message.peers, associatedMessages: message.associatedMessages, associatedMessageIds: message.associatedMessageIds) + return .MessageEntry(MessageHistoryMessageEntry(message: updatedMessage, location: value.location, monthLocation: value.monthLocation, attributes: value.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers) + } + } + + func getAssociatedMessageIds() -> [MessageId] { + switch self { + case let .IntermediateMessageEntry(message, location, monthLocation): + return [] + case let .MessageEntry(value, _, _): + return value.message.associatedMessageIds } } } @@ -164,7 +184,7 @@ public struct MessageHistoryEntry: Comparable { self.monthLocation = monthLocation self.attributes = attributes } - + public static func ==(lhs: MessageHistoryEntry, rhs: MessageHistoryEntry) -> Bool { if lhs.message.index == rhs.message.index && lhs.message.flags == rhs.message.flags && lhs.location == rhs.location && lhs.isRead == rhs.isRead && lhs.monthLocation == rhs.monthLocation && lhs.attributes == rhs.attributes { return true @@ -183,17 +203,26 @@ enum MessageHistoryTopTaggedMessage { var id: MessageId { switch self { - case let .message(message): - return message.id - case let .intermediate(message): - return message.id + case let .message(message): + return message.id + case let .intermediate(message): + return message.id } } } -public enum MessageHistoryViewRelativeHoleDirection: Equatable, Hashable { +public enum MessageHistoryViewRelativeHoleDirection: Equatable, Hashable, CustomStringConvertible { case range(start: MessageId, end: MessageId) case aroundId(MessageId) + + public var description: String { + switch self { + case let .range(start, end): + return "range(\(start), \(end))" + case let .aroundId(id): + return "aroundId(\(id))" + } + } } public struct MessageHistoryViewOrderStatistics: OptionSet { @@ -229,6 +258,7 @@ final class MutableMessageHistoryView { let tag: MessageTags? let namespaces: MessageIdNamespaces private let orderStatistics: MessageHistoryViewOrderStatistics + private let clipHoles: Bool private let anchor: HistoryViewInputAnchor fileprivate var combinedReadStates: MessageHistoryViewReadState? @@ -242,10 +272,11 @@ final class MutableMessageHistoryView { fileprivate(set) var sampledState: HistoryViewSample - init(postbox: Postbox, orderStatistics: MessageHistoryViewOrderStatistics, peerIds: MessageHistoryViewPeerIds, anchor inputAnchor: HistoryViewInputAnchor, combinedReadStates: MessageHistoryViewReadState?, transientReadStates: MessageHistoryViewReadState?, tag: MessageTags?, namespaces: MessageIdNamespaces, count: Int, topTaggedMessages: [MessageId.Namespace: MessageHistoryTopTaggedMessage?], additionalDatas: [AdditionalMessageHistoryViewDataEntry], getMessageCountInRange: (MessageIndex, MessageIndex) -> Int32) { + init(postbox: Postbox, orderStatistics: MessageHistoryViewOrderStatistics, clipHoles: Bool, peerIds: MessageHistoryViewPeerIds, anchor inputAnchor: HistoryViewInputAnchor, combinedReadStates: MessageHistoryViewReadState?, transientReadStates: MessageHistoryViewReadState?, tag: MessageTags?, namespaces: MessageIdNamespaces, count: Int, topTaggedMessages: [MessageId.Namespace: MessageHistoryTopTaggedMessage?], additionalDatas: [AdditionalMessageHistoryViewDataEntry], getMessageCountInRange: (MessageIndex, MessageIndex) -> Int32) { self.anchor = inputAnchor self.orderStatistics = orderStatistics + self.clipHoles = clipHoles self.peerIds = peerIds self.combinedReadStates = combinedReadStates self.transientReadStates = transientReadStates @@ -259,14 +290,14 @@ final class MutableMessageHistoryView { if case let .loading(loadingState) = self.state { let sampledState = loadingState.checkAndSample(postbox: postbox) switch sampledState { - case let .ready(anchor, holes): - self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: tag, namespaces: namespaces, statistics: self.orderStatistics, halfLimit: count + 1, locations: peerIds, postbox: postbox, holes: holes)) - self.sampledState = self.state.sample(postbox: postbox) - case .loadHole: - break + case let .ready(anchor, holes): + self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: tag, namespaces: namespaces, statistics: self.orderStatistics, halfLimit: count + 1, locations: peerIds, postbox: postbox, holes: holes)) + self.sampledState = self.state.sample(postbox: postbox, clipHoles: self.clipHoles) + case .loadHole: + break } } - self.sampledState = self.state.sample(postbox: postbox) + self.sampledState = self.state.sample(postbox: postbox, clipHoles: self.clipHoles) self.render(postbox: postbox) } @@ -276,22 +307,22 @@ final class MutableMessageHistoryView { if case let .loading(loadingState) = self.state { let sampledState = loadingState.checkAndSample(postbox: postbox) switch sampledState { - case let .ready(anchor, holes): - self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, namespaces: self.namespaces, statistics: self.orderStatistics, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) - case .loadHole: - break + case let .ready(anchor, holes): + self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, namespaces: self.namespaces, statistics: self.orderStatistics, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) + case .loadHole: + break } } if case let .loading(loadingState) = self.state { let sampledState = loadingState.checkAndSample(postbox: postbox) switch sampledState { - case let .ready(anchor, holes): - self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, namespaces: self.namespaces, statistics: self.orderStatistics, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) - case .loadHole: - break + case let .ready(anchor, holes): + self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, namespaces: self.namespaces, statistics: self.orderStatistics, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) + case .loadHole: + break } } - self.sampledState = self.state.sample(postbox: postbox) + self.sampledState = self.state.sample(postbox: postbox, clipHoles: self.clipHoles) } func refreshDueToExternalTransaction(postbox: Postbox) -> Bool { @@ -301,18 +332,18 @@ final class MutableMessageHistoryView { func updatePeerIds(transaction: PostboxTransaction) { switch self.peerIds { - case let .single(peerId): - if let updatedData = transaction.currentUpdatedCachedPeerData[peerId] { - if updatedData.associatedHistoryMessageId != nil { - self.peerIds = .associated(peerId, updatedData.associatedHistoryMessageId) - } + case let .single(peerId): + if let updatedData = transaction.currentUpdatedCachedPeerData[peerId] { + if updatedData.associatedHistoryMessageId != nil { + self.peerIds = .associated(peerId, updatedData.associatedHistoryMessageId) } - case let .associated(peerId, associatedId): - if let updatedData = transaction.currentUpdatedCachedPeerData[peerId] { - if updatedData.associatedHistoryMessageId != associatedId { - self.peerIds = .associated(peerId, updatedData.associatedHistoryMessageId) - } + } + case let .associated(peerId, associatedId): + if let updatedData = transaction.currentUpdatedCachedPeerData[peerId] { + if updatedData.associatedHistoryMessageId != associatedId { + self.peerIds = .associated(peerId, updatedData.associatedHistoryMessageId) } + } } } @@ -321,27 +352,27 @@ final class MutableMessageHistoryView { var peerIdsSet = Set() switch self.peerIds { - case let .single(peerId): - peerIdsSet.insert(peerId) - if let value = transaction.currentOperationsByPeerId[peerId] { + case let .single(peerId): + peerIdsSet.insert(peerId) + if let value = transaction.currentOperationsByPeerId[peerId] { + operations.append(value) + } + case .associated: + switch self.peerIds { + case .single: + assertionFailure() + case let .associated(mainPeerId, associatedPeerId): + peerIdsSet.insert(mainPeerId) + if let associatedPeerId = associatedPeerId { + peerIdsSet.insert(associatedPeerId.peerId) + } + } + + for (peerId, value) in transaction.currentOperationsByPeerId { + if peerIdsSet.contains(peerId) { operations.append(value) } - case .associated: - switch self.peerIds { - case .single: - assertionFailure() - case let .associated(mainPeerId, associatedPeerId): - peerIdsSet.insert(mainPeerId) - if let associatedPeerId = associatedPeerId { - peerIdsSet.insert(associatedPeerId.peerId) - } - } - - for (peerId, value) in transaction.currentOperationsByPeerId { - if peerIdsSet.contains(peerId) { - operations.append(value) - } - } + } } var hasChanges = false @@ -349,160 +380,163 @@ final class MutableMessageHistoryView { let unwrappedTag: MessageTags = self.tag ?? [] switch self.state { - case let .loading(loadingState): - for (key, holeOperations) in transaction.currentPeerHoleOperations { - var matchesSpace = false - switch key.space { - case .everywhere: - matchesSpace = unwrappedTag.isEmpty - case let .tag(tag): - if let currentTag = self.tag, currentTag == tag { - matchesSpace = true - } + case let .loading(loadingState): + for (key, holeOperations) in transaction.currentPeerHoleOperations { + var matchesSpace = false + switch key.space { + case .everywhere: + matchesSpace = unwrappedTag.isEmpty + case let .tag(tag): + if let currentTag = self.tag, currentTag == tag { + matchesSpace = true } - if matchesSpace { - if peerIdsSet.contains(key.peerId) { - for operation in holeOperations { - switch operation { - case let .insert(range): - if loadingState.insertHole(space: PeerIdAndNamespace(peerId: key.peerId, namespace: key.namespace), range: range) { - hasChanges = true - } - case let .remove(range): - if loadingState.removeHole(space: PeerIdAndNamespace(peerId: key.peerId, namespace: key.namespace), range: range) { - hasChanges = true - } + } + if matchesSpace { + if peerIdsSet.contains(key.peerId) { + for operation in holeOperations { + switch operation { + case let .insert(range): + if loadingState.insertHole(space: PeerIdAndNamespace(peerId: key.peerId, namespace: key.namespace), range: range) { + hasChanges = true + } + case let .remove(range): + if loadingState.removeHole(space: PeerIdAndNamespace(peerId: key.peerId, namespace: key.namespace), range: range) { + hasChanges = true } } } } } - case let .loaded(loadedState): - for operationSet in operations { - var addCount = 0 - var removeCount = 0 - for operation in operationSet { - switch operation { - case .InsertMessage: - addCount += 1 - case .Remove: - removeCount += 1 - default: - break + } + case let .loaded(loadedState): + for operationSet in operations { + var addCount = 0 + var removeCount = 0 + for operation in operationSet { + switch operation { + case .InsertMessage: + addCount += 1 + case .Remove: + removeCount += 1 + default: + break + } + } + for operation in operationSet { + switch operation { + case let .InsertMessage(message): + if unwrappedTag.isEmpty || message.tags.contains(unwrappedTag) { + if self.namespaces.contains(message.id.namespace) { + if loadedState.add(entry: .IntermediateMessageEntry(message, nil, nil)) { + hasChanges = true + } + } } - } - if addCount == 2 && removeCount == 2 { - assert(true) - } - for operation in operationSet { - switch operation { - case let .InsertMessage(message): - if unwrappedTag.isEmpty || message.tags.contains(unwrappedTag) { - if loadedState.add(entry: .IntermediateMessageEntry(message, nil, nil)) { - hasChanges = true - } - } - case let .Remove(indicesAndTags): - for (index, _) in indicesAndTags { - if loadedState.remove(index: index) { - hasChanges = true - } - } - case let .UpdateEmbeddedMedia(index, buffer): - if loadedState.updateEmbeddedMedia(index: index, buffer: buffer) { + case let .Remove(indicesAndTags): + for (index, _) in indicesAndTags { + if self.namespaces.contains(index.id.namespace) { + if loadedState.remove(index: index) { hasChanges = true } - case let .UpdateGroupInfos(groupInfos): - if loadedState.updateGroupInfo(mapping: groupInfos) { - hasChanges = true - } - case let .UpdateReadState(peerId, combinedReadState): + } + } + case let .UpdateEmbeddedMedia(index, buffer): + if self.namespaces.contains(index.id.namespace) { + if loadedState.updateEmbeddedMedia(index: index, buffer: buffer) { hasChanges = true - if let transientReadStates = self.transientReadStates { - switch transientReadStates { - case let .peer(states): - var updatedStates = states - updatedStates[peerId] = combinedReadState - self.transientReadStates = .peer(updatedStates) - } - } - case let .UpdateTimestamp(index, timestamp): - if loadedState.updateTimestamp(postbox: postbox, index: index, timestamp: timestamp) { + } + } + case let .UpdateGroupInfos(groupInfos): + if loadedState.updateGroupInfo(mapping: groupInfos) { + hasChanges = true + } + case let .UpdateReadState(peerId, combinedReadState): + hasChanges = true + if let transientReadStates = self.transientReadStates { + switch transientReadStates { + case let .peer(states): + var updatedStates = states + updatedStates[peerId] = combinedReadState + self.transientReadStates = .peer(updatedStates) + } + } + case let .UpdateTimestamp(index, timestamp): + if loadedState.updateTimestamp(postbox: postbox, index: index, timestamp: timestamp) { + hasChanges = true + } + } + } + } + for (key, holeOperations) in transaction.currentPeerHoleOperations { + var matchesSpace = false + switch key.space { + case .everywhere: + matchesSpace = unwrappedTag.isEmpty + case let .tag(tag): + if let currentTag = self.tag, currentTag == tag { + matchesSpace = true + } + } + if matchesSpace { + if peerIdsSet.contains(key.peerId) { + for operation in holeOperations { + switch operation { + case let .insert(range): + if loadedState.insertHole(space: PeerIdAndNamespace(peerId: key.peerId, namespace: key.namespace), range: range) { hasChanges = true } - } - } - } - for (key, holeOperations) in transaction.currentPeerHoleOperations { - var matchesSpace = false - switch key.space { - case .everywhere: - matchesSpace = unwrappedTag.isEmpty - case let .tag(tag): - if let currentTag = self.tag, currentTag == tag { - matchesSpace = true - } - } - if matchesSpace { - if peerIdsSet.contains(key.peerId) { - for operation in holeOperations { - switch operation { - case let .insert(range): - if loadedState.insertHole(space: PeerIdAndNamespace(peerId: key.peerId, namespace: key.namespace), range: range) { - hasChanges = true - } - case let .remove(range): - if loadedState.removeHole(space: PeerIdAndNamespace(peerId: key.peerId, namespace: key.namespace), range: range) { - hasChanges = true - } + case let .remove(range): + if loadedState.removeHole(space: PeerIdAndNamespace(peerId: key.peerId, namespace: key.namespace), range: range) { + hasChanges = true } } } } } - if !transaction.updatedMedia.isEmpty { - if loadedState.updateMedia(updatedMedia: transaction.updatedMedia) { - hasChanges = true - } + } + if !transaction.updatedMedia.isEmpty { + if loadedState.updateMedia(updatedMedia: transaction.updatedMedia) { + hasChanges = true } + } } if hasChanges { if case let .loading(loadingState) = self.state { let sampledState = loadingState.checkAndSample(postbox: postbox) switch sampledState { - case let .ready(anchor, holes): - self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, namespaces: self.namespaces, statistics: self.orderStatistics, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) - case .loadHole: - break + case let .ready(anchor, holes): + self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, namespaces: self.namespaces, statistics: self.orderStatistics, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) + case .loadHole: + break } } - self.sampledState = self.state.sample(postbox: postbox) + self.sampledState = self.state.sample(postbox: postbox, clipHoles: self.clipHoles) } for operationSet in operations { for operation in operationSet { switch operation { - case let .InsertMessage(message): - if message.flags.contains(.TopIndexable) { - if let currentTopMessage = self.topTaggedMessages[message.id.namespace] { - if currentTopMessage == nil || currentTopMessage!.id < message.id { - self.topTaggedMessages[message.id.namespace] = MessageHistoryTopTaggedMessage.intermediate(message) - hasChanges = true - } + case let .InsertMessage(message): + if message.flags.contains(.TopIndexable) { + if let currentTopMessage = self.topTaggedMessages[message.id.namespace] { + if currentTopMessage == nil || currentTopMessage!.id < message.id { + self.topTaggedMessages[message.id.namespace] = MessageHistoryTopTaggedMessage.intermediate(message) + hasChanges = true } } - case let .Remove(indices): - if !self.topTaggedMessages.isEmpty { - for (index, _) in indices { - if let maybeCurrentTopMessage = self.topTaggedMessages[index.id.namespace], let currentTopMessage = maybeCurrentTopMessage, index.id == currentTopMessage.id { - let item: MessageHistoryTopTaggedMessage? = nil - self.topTaggedMessages[index.id.namespace] = item - } + } + case let .Remove(indices): + if !self.topTaggedMessages.isEmpty { + for (index, _) in indices { + if let maybeCurrentTopMessage = self.topTaggedMessages[index.id.namespace], let currentTopMessage = maybeCurrentTopMessage, index.id == currentTopMessage.id { + let item: MessageHistoryTopTaggedMessage? = nil + self.topTaggedMessages[index.id.namespace] = item } } - default: - break + } + default: + break } } } @@ -511,81 +545,81 @@ final class MutableMessageHistoryView { var currentCachedPeerData: CachedPeerData? for i in 0 ..< self.additionalDatas.count { switch self.additionalDatas[i] { - case let .cachedPeerData(peerId, currentData): - currentCachedPeerData = currentData - if let updatedData = transaction.currentUpdatedCachedPeerData[peerId] { - if currentData?.messageIds != updatedData.messageIds { - updatedCachedPeerDataMessages = true - } - currentCachedPeerData = updatedData - self.additionalDatas[i] = .cachedPeerData(peerId, updatedData) - hasChanges = true - } - case .cachedPeerDataMessages: - break - case let .peerChatState(peerId, _): - if transaction.currentUpdatedPeerChatStates.contains(peerId) { - self.additionalDatas[i] = .peerChatState(peerId, postbox.peerChatStateTable.get(peerId) as? PeerChatState) - hasChanges = true - } - case .totalUnreadState: - break - case .peerNotificationSettings: - break - case let .cacheEntry(entryId, _): - if transaction.updatedCacheEntryKeys.contains(entryId) { - self.additionalDatas[i] = .cacheEntry(entryId, postbox.retrieveItemCacheEntry(id: entryId)) - hasChanges = true - } - case .preferencesEntry: - break - case let .peerIsContact(peerId, value): - if let replacedPeerIds = transaction.replaceContactPeerIds { - let updatedValue: Bool - if let contactPeer = postbox.peerTable.get(peerId), let associatedPeerId = contactPeer.associatedPeerId { - updatedValue = replacedPeerIds.contains(associatedPeerId) - } else { - updatedValue = replacedPeerIds.contains(peerId) - } - - if value != updatedValue { - self.additionalDatas[i] = .peerIsContact(peerId, value) - hasChanges = true - } - } - case let .peer(peerId, _): - if let peer = transaction.currentUpdatedPeers[peerId] { - self.additionalDatas[i] = .peer(peerId, peer) + case let .cachedPeerData(peerId, currentData): + currentCachedPeerData = currentData + if let updatedData = transaction.currentUpdatedCachedPeerData[peerId] { + if currentData?.messageIds != updatedData.messageIds { + updatedCachedPeerDataMessages = true + } + currentCachedPeerData = updatedData + self.additionalDatas[i] = .cachedPeerData(peerId, updatedData) + hasChanges = true + } + case .cachedPeerDataMessages: + break + case let .peerChatState(peerId, _): + if transaction.currentUpdatedPeerChatStates.contains(peerId) { + self.additionalDatas[i] = .peerChatState(peerId, postbox.peerChatStateTable.get(peerId) as? PeerChatState) + hasChanges = true + } + case .totalUnreadState: + break + case .peerNotificationSettings: + break + case let .cacheEntry(entryId, _): + if transaction.updatedCacheEntryKeys.contains(entryId) { + self.additionalDatas[i] = .cacheEntry(entryId, postbox.retrieveItemCacheEntry(id: entryId)) + hasChanges = true + } + case .preferencesEntry: + break + case let .peerIsContact(peerId, value): + if let replacedPeerIds = transaction.replaceContactPeerIds { + let updatedValue: Bool + if let contactPeer = postbox.peerTable.get(peerId), let associatedPeerId = contactPeer.associatedPeerId { + updatedValue = replacedPeerIds.contains(associatedPeerId) + } else { + updatedValue = replacedPeerIds.contains(peerId) + } + + if value != updatedValue { + self.additionalDatas[i] = .peerIsContact(peerId, value) hasChanges = true } + } + case let .peer(peerId, _): + if let peer = transaction.currentUpdatedPeers[peerId] { + self.additionalDatas[i] = .peer(peerId, peer) + hasChanges = true + } } } if let cachedData = currentCachedPeerData, !cachedData.messageIds.isEmpty { for i in 0 ..< self.additionalDatas.count { switch self.additionalDatas[i] { - case .cachedPeerDataMessages(_, _): - outer: for operationSet in operations { - for operation in operationSet { - switch operation { - case let .InsertMessage(message): - if cachedData.messageIds.contains(message.id) { - updatedCachedPeerDataMessages = true - break outer - } - case let .Remove(indicesWithTags): - for (index, _) in indicesWithTags { - if cachedData.messageIds.contains(index.id) { - updatedCachedPeerDataMessages = true - break outer - } - } - default: - break + case .cachedPeerDataMessages(_, _): + outer: for operationSet in operations { + for operation in operationSet { + switch operation { + case let .InsertMessage(message): + if cachedData.messageIds.contains(message.id) { + updatedCachedPeerDataMessages = true + break outer } + case let .Remove(indicesWithTags): + for (index, _) in indicesWithTags { + if cachedData.messageIds.contains(index.id) { + updatedCachedPeerDataMessages = true + break outer + } + } + default: + break } } - default: - break + } + default: + break } } } @@ -594,18 +628,18 @@ final class MutableMessageHistoryView { hasChanges = true for i in 0 ..< self.additionalDatas.count { switch self.additionalDatas[i] { - case let .cachedPeerDataMessages(peerId, _): - var messages: [MessageId: Message] = [:] - if let cachedData = currentCachedPeerData { - for id in cachedData.messageIds { - if let message = postbox.getMessage(id) { - messages[id] = message - } + case let .cachedPeerDataMessages(peerId, _): + var messages: [MessageId: Message] = [:] + if let cachedData = currentCachedPeerData { + for id in cachedData.messageIds { + if let message = postbox.getMessage(id) { + messages[id] = message } } - self.additionalDatas[i] = .cachedPeerDataMessages(peerId, messages) - default: - break + } + self.additionalDatas[i] = .cachedPeerDataMessages(peerId, messages) + default: + break } } } @@ -613,13 +647,13 @@ final class MutableMessageHistoryView { if !transaction.currentPeerHoleOperations.isEmpty { var peerIdsSet: [PeerId] = [] switch peerIds { - case let .single(peerId): - peerIdsSet.append(peerId) - case let .associated(peerId, associatedId): - peerIdsSet.append(peerId) - if let associatedId = associatedId { - peerIdsSet.append(associatedId.peerId) - } + case let .single(peerId): + peerIdsSet.append(peerId) + case let .associated(peerId, associatedId): + peerIdsSet.append(peerId) + if let associatedId = associatedId { + peerIdsSet.append(associatedId.peerId) + } } let space: MessageHistoryHoleSpace = self.tag.flatMap(MessageHistoryHoleSpace.tag) ?? .everywhere for key in transaction.currentPeerHoleOperations.keys { @@ -645,27 +679,27 @@ final class MutableMessageHistoryView { } } - func firstHole() -> (MessageHistoryViewHole, MessageHistoryViewRelativeHoleDirection)? { + func firstHole() -> (MessageHistoryViewHole, MessageHistoryViewRelativeHoleDirection, Int)? { switch self.sampledState { - case let .loading(loadingSample): - switch loadingSample { - case .ready: - return nil - case let .loadHole(peerId, namespace, _, id): - return (.peer(MessageHistoryViewPeerHole(peerId: peerId, namespace: namespace)), .aroundId(MessageId(peerId: peerId, namespace: namespace, id: id))) - } - case let .loaded(loadedSample): - if let hole = loadedSample.hole { - let direction: MessageHistoryViewRelativeHoleDirection - if let endId = hole.endId { - direction = .range(start: MessageId(peerId: hole.peerId, namespace: hole.namespace, id: hole.startId), end: MessageId(peerId: hole.peerId, namespace: hole.namespace, id: endId)) - } else { - direction = .aroundId(MessageId(peerId: hole.peerId, namespace: hole.namespace, id: hole.startId)) - } - return (.peer(MessageHistoryViewPeerHole(peerId: hole.peerId, namespace: hole.namespace)), direction) + case let .loading(loadingSample): + switch loadingSample { + case .ready: + return nil + case let .loadHole(peerId, namespace, _, id): + return (.peer(MessageHistoryViewPeerHole(peerId: peerId, namespace: namespace)), .aroundId(MessageId(peerId: peerId, namespace: namespace, id: id)), self.fillCount * 2) + } + case let .loaded(loadedSample): + if let hole = loadedSample.hole { + let direction: MessageHistoryViewRelativeHoleDirection + if let endId = hole.endId { + direction = .range(start: MessageId(peerId: hole.peerId, namespace: hole.namespace, id: hole.startId), end: MessageId(peerId: hole.peerId, namespace: hole.namespace, id: endId)) } else { - return nil + direction = .aroundId(MessageId(peerId: hole.peerId, namespace: hole.namespace, id: hole.startId)) } + return (.peer(MessageHistoryViewPeerHole(peerId: hole.peerId, namespace: hole.namespace)), direction, self.fillCount * 2) + } else { + return nil + } } } } @@ -690,88 +724,88 @@ public final class MessageHistoryView { self.namespaces = mutableView.namespaces var entries: [MessageHistoryEntry] switch mutableView.sampledState { - case .loading: - self.isLoading = true + case .loading: + self.isLoading = true + self.anchorIndex = .upperBound + entries = [] + self.holeEarlier = true + self.holeLater = true + self.earlierId = nil + self.laterId = nil + case let .loaded(state): + var isLoading = false + switch state.anchor { + case .lowerBound: + self.anchorIndex = .lowerBound + case .upperBound: self.anchorIndex = .upperBound - entries = [] - self.holeEarlier = true - self.holeLater = true - self.earlierId = nil - self.laterId = nil - case let .loaded(state): - var isLoading = false - switch state.anchor { - case .lowerBound: - self.anchorIndex = .lowerBound - case .upperBound: - self.anchorIndex = .upperBound - case let .index(index): - self.anchorIndex = .message(index) - } - self.holeEarlier = state.holesToLower - self.holeLater = state.holesToHigher - if state.entries.isEmpty && state.hole != nil { - isLoading = true - } - entries = [] - if let transientReadStates = mutableView.transientReadStates, case let .peer(states) = transientReadStates { - for entry in state.entries { - if mutableView.namespaces.contains(entry.message.id.namespace) { - let read: Bool - if entry.message.flags.contains(.Incoming) { - read = false - } else if let readState = states[entry.message.id.peerId] { - read = readState.isOutgoingMessageIndexRead(entry.message.index) - } else { - read = false - } - entries.append(MessageHistoryEntry(message: entry.message, isRead: read, location: entry.location, monthLocation: entry.monthLocation, attributes: entry.attributes)) + case let .index(index): + self.anchorIndex = .message(index) + } + self.holeEarlier = state.holesToLower + self.holeLater = state.holesToHigher + if state.entries.isEmpty && state.hole != nil { + isLoading = true + } + entries = [] + if let transientReadStates = mutableView.transientReadStates, case let .peer(states) = transientReadStates { + for entry in state.entries { + if mutableView.namespaces.contains(entry.message.id.namespace) { + let read: Bool + if entry.message.flags.contains(.Incoming) { + read = false + } else if let readState = states[entry.message.id.peerId] { + read = readState.isOutgoingMessageIndexRead(entry.message.index) + } else { + read = false } + entries.append(MessageHistoryEntry(message: entry.message, isRead: read, location: entry.location, monthLocation: entry.monthLocation, attributes: entry.attributes)) } + } + } else { + for entry in state.entries { + if mutableView.namespaces.contains(entry.message.id.namespace) { + entries.append(MessageHistoryEntry(message: entry.message, isRead: false, location: entry.location, monthLocation: entry.monthLocation, attributes: entry.attributes)) + } + } + } + assert(Set(entries.map({ $0.message.stableId })).count == entries.count) + if !entries.isEmpty { + let anchorIndex = binaryIndexOrLower(entries, state.anchor) + let lowerOrEqualThanAnchorCount = anchorIndex + 1 + let higherThanAnchorCount = entries.count - anchorIndex - 1 + + if higherThanAnchorCount > mutableView.fillCount { + self.laterId = entries[entries.count - 1].index + entries.removeLast() } else { - for entry in state.entries { - if mutableView.namespaces.contains(entry.message.id.namespace) { - entries.append(MessageHistoryEntry(message: entry.message, isRead: false, location: entry.location, monthLocation: entry.monthLocation, attributes: entry.attributes)) - } - } + self.laterId = nil } - assert(Set(entries.map({ $0.message.stableId })).count == entries.count) - if !entries.isEmpty { - let anchorIndex = binaryIndexOrLower(entries, state.anchor) - let lowerOrEqualThanAnchorCount = anchorIndex + 1 - let higherThanAnchorCount = entries.count - anchorIndex - 1 - - if higherThanAnchorCount > mutableView.fillCount { - self.laterId = entries[entries.count - 1].index - entries.removeLast() - } else { - self.laterId = nil - } - - if lowerOrEqualThanAnchorCount > mutableView.fillCount { - self.earlierId = entries[0].index - entries.removeFirst() - } else { - self.earlierId = nil - } + + if lowerOrEqualThanAnchorCount > mutableView.fillCount { + self.earlierId = entries[0].index + entries.removeFirst() } else { self.earlierId = nil - self.laterId = nil - if state.holesToLower || state.holesToHigher { - isLoading = true - } } - self.isLoading = isLoading + } else { + self.earlierId = nil + self.laterId = nil + if state.holesToLower || state.holesToHigher { + isLoading = true + } + } + self.isLoading = isLoading } var topTaggedMessages: [Message] = [] for (_, message) in mutableView.topTaggedMessages { if let message = message { switch message { - case let .message(message): - topTaggedMessages.append(message) - default: - assertionFailure("unexpected intermediate tagged message entry in MessageHistoryView.init()") + case let .message(message): + topTaggedMessages.append(message) + default: + assertionFailure("unexpected intermediate tagged message entry in MessageHistoryView.init()") } } } @@ -782,61 +816,61 @@ public final class MessageHistoryView { if let combinedReadStates = mutableView.combinedReadStates { switch combinedReadStates { - case let .peer(states): - var hasUnread = false - for (_, readState) in states { - if readState.count > 0 { - hasUnread = true - break - } + case let .peer(states): + var hasUnread = false + for (_, readState) in states { + if readState.count > 0 { + hasUnread = true + break } - - var maxIndex: MessageIndex? - - if hasUnread { - var peerIds = Set() - for entry in entries { - peerIds.insert(entry.index.id.peerId) - } - for peerId in peerIds { - if let combinedReadState = states[peerId] { - for (namespace, state) in combinedReadState.states { - var maxNamespaceIndex: MessageIndex? - var index = entries.count - 1 - for entry in entries.reversed() { - if entry.index.id.peerId == peerId && entry.index.id.namespace == namespace && state.isIncomingMessageIndexRead(entry.index) { - maxNamespaceIndex = entry.index + } + + var maxIndex: MessageIndex? + + if hasUnread { + var peerIds = Set() + for entry in entries { + peerIds.insert(entry.index.id.peerId) + } + for peerId in peerIds { + if let combinedReadState = states[peerId] { + for (namespace, state) in combinedReadState.states { + var maxNamespaceIndex: MessageIndex? + var index = entries.count - 1 + for entry in entries.reversed() { + if entry.index.id.peerId == peerId && entry.index.id.namespace == namespace && state.isIncomingMessageIndexRead(entry.index) { + maxNamespaceIndex = entry.index + break + } + index -= 1 + } + if maxNamespaceIndex == nil && index == -1 && entries.count != 0 { + index = 0 + for entry in entries { + if entry.index.id.peerId == peerId && entry.index.id.namespace == namespace { + maxNamespaceIndex = entry.index.predecessor() break } - index -= 1 + index += 1 } - if maxNamespaceIndex == nil && index == -1 && entries.count != 0 { - index = 0 - for entry in entries { - if entry.index.id.peerId == peerId && entry.index.id.namespace == namespace { - maxNamespaceIndex = entry.index.predecessor() - break - } - index += 1 + } + if let _ = maxNamespaceIndex , index + 1 < entries.count { + for i in index + 1 ..< entries.count { + if entries[i].message.flags.intersection(.IsIncomingMask).isEmpty { + maxNamespaceIndex = entries[i].message.index + } else { + break } } - if let _ = maxNamespaceIndex , index + 1 < entries.count { - for i in index + 1 ..< entries.count { - if entries[i].message.flags.intersection(.IsIncomingMask).isEmpty { - maxNamespaceIndex = entries[i].message.index - } else { - break - } - } - } - if let maxNamespaceIndex = maxNamespaceIndex , maxIndex == nil || maxIndex! < maxNamespaceIndex { - maxIndex = maxNamespaceIndex - } + } + if let maxNamespaceIndex = maxNamespaceIndex , maxIndex == nil || maxIndex! < maxNamespaceIndex { + maxIndex = maxNamespaceIndex } } } } - self.maxReadIndex = maxIndex + } + self.maxReadIndex = maxIndex } } else { self.maxReadIndex = nil diff --git a/submodules/Postbox/Sources/MessageHistoryViewState.swift b/submodules/Postbox/Sources/MessageHistoryViewState.swift index c00bb54ef6..731a0d4263 100644 --- a/submodules/Postbox/Sources/MessageHistoryViewState.swift +++ b/submodules/Postbox/Sources/MessageHistoryViewState.swift @@ -1,8 +1,13 @@ import Foundation -struct PeerIdAndNamespace: Hashable { - let peerId: PeerId - let namespace: MessageId.Namespace +public struct PeerIdAndNamespace: Hashable { + public let peerId: PeerId + public let namespace: MessageId.Namespace + + public init(peerId: PeerId, namespace: MessageId.Namespace) { + self.peerId = peerId + self.namespace = namespace + } } private func canContainHoles(_ peerIdAndNamespace: PeerIdAndNamespace, seedConfiguration: SeedConfiguration) -> Bool { @@ -321,17 +326,6 @@ private func sampleHoleRanges(orderedEntriesBySpace: [PeerIdAndNamespace: Ordere } } - for item in items.lowerOrAtAnchor { - if item.index.id.id == 76891 { - assert(true) - } - } - for item in items.higherThanAnchor { - if item.index.id.id == 76891 { - assert(true) - } - } - var lowerOrAtAnchorHole: (distanceFromAnchor: Int, hole: SampledHistoryViewHole)? for i in (-1 ..< items.lowerOrAtAnchor.count).reversed() { @@ -500,103 +494,6 @@ private func sampleHoleRanges(orderedEntriesBySpace: [PeerIdAndNamespace: Ordere sampledHole = (chosenHole.distanceFromAnchor, chosenHole.hole) } } - - /*let anchorIndex = binaryIndexOrLower(items.entries, anchor) - let anchorStartingMessageId: MessageId.Id - if anchorIndex == -1 { - anchorStartingMessageId = 1 - } else { - anchorStartingMessageId = items.entries[anchorIndex].index.id.id - } - - let startingLowerDirectionIndex = anchorIndex - let startingHigherDirectionIndex = anchorIndex + 1 - - var lowerDirectionIndex = startingLowerDirectionIndex - var higherDirectionIndex = startingHigherDirectionIndex - while lowerDirectionIndex >= 0 || higherDirectionIndex < items.entries.count { - if lowerDirectionIndex >= 0 { - let itemIndex = items.entries[lowerDirectionIndex].index - var itemBoundaryMessageId: MessageId.Id = itemIndex.id.id - if lowerDirectionIndex == 0 && itemBoundaryMessageId == bounds.lower.id.id { - itemBoundaryMessageId = 1 - } - let previousBoundaryIndex: MessageIndex - if lowerDirectionIndex == startingLowerDirectionIndex { - previousBoundaryIndex = itemIndex - } else { - previousBoundaryIndex = items.entries[lowerDirectionIndex + 1].index - } - let toLowerRange: ClosedRange = min(Int(anchorStartingMessageId), Int(itemBoundaryMessageId)) ... max(Int(anchorStartingMessageId), Int(itemBoundaryMessageId)) - if indices.intersects(integersIn: toLowerRange) { - var itemClipIndex: MessageIndex - if indices.contains(Int(previousBoundaryIndex.id.id)) { - itemClipIndex = previousBoundaryIndex - } else { - itemClipIndex = previousBoundaryIndex.predecessor() - } - clipRanges.append(MessageIndex.absoluteLowerBound() ... itemClipIndex) - var replaceHole = false - if let (currentItemIndex, _) = sampledHole { - if let currentItemIndex = currentItemIndex, abs(lowerDirectionIndex - anchorIndex) < abs(currentItemIndex - anchorIndex) { - replaceHole = true - } - } else { - replaceHole = true - } - - if replaceHole { - if let idInHole = indices.integerLessThanOrEqualTo(toLowerRange.upperBound) { - sampledHole = (lowerDirectionIndex, SampledHistoryViewHole(peerId: space.peerId, namespace: space.namespace, tag: tag, indices: indices, startId: MessageId.Id(idInHole), endId: 1)) - } else { - assertionFailure() - } - } - lowerDirectionIndex = -1 - } - } - lowerDirectionIndex -= 1 - - if higherDirectionIndex < items.entries.count { - let itemIndex = items.entries[higherDirectionIndex].index - var itemBoundaryMessageId: MessageId.Id = itemIndex.id.id - if higherDirectionIndex == items.entries.count - 1 && itemBoundaryMessageId == bounds.upper.id.id { - itemBoundaryMessageId = Int32.max - 1 - } - let previousBoundaryIndex: MessageIndex - if higherDirectionIndex == startingHigherDirectionIndex { - previousBoundaryIndex = itemIndex - } else { - previousBoundaryIndex = items.entries[higherDirectionIndex - 1].index - } - let toHigherRange: ClosedRange = min(Int(anchorStartingMessageId), Int(itemBoundaryMessageId)) ... max(Int(anchorStartingMessageId), Int(itemBoundaryMessageId)) - if indices.intersects(integersIn: toHigherRange) { - var itemClipIndex: MessageIndex - if indices.contains(Int(previousBoundaryIndex.id.id)) { - itemClipIndex = previousBoundaryIndex - } else { - itemClipIndex = previousBoundaryIndex.successor() - } - clipRanges.append(itemClipIndex ... MessageIndex.absoluteUpperBound()) - var replaceHole = false - if let (currentItemIndex, _) = sampledHole { - if let currentItemIndex = currentItemIndex, abs(higherDirectionIndex - anchorIndex) < abs(currentItemIndex - anchorIndex) { - replaceHole = true - } - } else { - replaceHole = true - } - - if replaceHole { - if let idInHole = indices.integerGreaterThanOrEqualTo(toHigherRange.lowerBound) { - sampledHole = (higherDirectionIndex, SampledHistoryViewHole(peerId: space.peerId, namespace: space.namespace, tag: tag, indices: indices, startId: MessageId.Id(idInHole), endId: Int32.max - 1)) - } - } - higherDirectionIndex = items.entries.count - } - } - higherDirectionIndex += 1 - }*/ } return (clipRanges, sampledHole?.hole) } @@ -633,8 +530,132 @@ struct HistoryViewHoles { } struct OrderedHistoryViewEntries { - var lowerOrAtAnchor: [MutableMessageHistoryEntry] - var higherThanAnchor: [MutableMessageHistoryEntry] + private(set) var lowerOrAtAnchor: [MutableMessageHistoryEntry] + private(set) var higherThanAnchor: [MutableMessageHistoryEntry] + + private(set) var reverseAssociatedIndices: [MessageId: [MessageIndex]] = [:] + + fileprivate init(lowerOrAtAnchor: [MutableMessageHistoryEntry], higherThanAnchor: [MutableMessageHistoryEntry]) { + self.lowerOrAtAnchor = lowerOrAtAnchor + self.higherThanAnchor = higherThanAnchor + + for entry in lowerOrAtAnchor { + for id in entry.getAssociatedMessageIds() { + if self.reverseAssociatedIndices[id] == nil { + self.reverseAssociatedIndices[id] = [entry.index] + } else { + self.reverseAssociatedIndices[id]!.append(entry.index) + } + } + } + for entry in higherThanAnchor { + for id in entry.getAssociatedMessageIds() { + if self.reverseAssociatedIndices[id] == nil { + self.reverseAssociatedIndices[id] = [entry.index] + } else { + self.reverseAssociatedIndices[id]!.append(entry.index) + } + } + } + } + + mutating func setLowerOrAtAnchorAtArrayIndex(_ index: Int, to value: MutableMessageHistoryEntry) { + let previousIndex = self.lowerOrAtAnchor[index].index + let updatedIndex = value.index + let previousAssociatedIds = self.lowerOrAtAnchor[index].getAssociatedMessageIds() + let updatedAssociatedIds = value.getAssociatedMessageIds() + + self.lowerOrAtAnchor[index] = value + + if previousAssociatedIds != updatedAssociatedIds { + for id in previousAssociatedIds { + self.reverseAssociatedIndices[id]?.removeAll(where: { $0 == previousIndex }) + if let isEmpty = self.reverseAssociatedIndices[id]?.isEmpty, isEmpty { + self.reverseAssociatedIndices.removeValue(forKey: id) + } + } + for id in updatedAssociatedIds { + if self.reverseAssociatedIndices[id] == nil { + self.reverseAssociatedIndices[id] = [updatedIndex] + } else { + self.reverseAssociatedIndices[id]!.append(updatedIndex) + } + } + } + } + + mutating func setHigherThanAnchorAtArrayIndex(_ index: Int, to value: MutableMessageHistoryEntry) { + let previousIndex = self.higherThanAnchor[index].index + let updatedIndex = value.index + let previousAssociatedIds = self.higherThanAnchor[index].getAssociatedMessageIds() + let updatedAssociatedIds = value.getAssociatedMessageIds() + + self.higherThanAnchor[index] = value + + if previousAssociatedIds != updatedAssociatedIds { + for id in previousAssociatedIds { + self.reverseAssociatedIndices[id]?.removeAll(where: { $0 == previousIndex }) + if let isEmpty = self.reverseAssociatedIndices[id]?.isEmpty, isEmpty { + self.reverseAssociatedIndices.removeValue(forKey: id) + } + } + for id in updatedAssociatedIds { + if self.reverseAssociatedIndices[id] == nil { + self.reverseAssociatedIndices[id] = [updatedIndex] + } else { + self.reverseAssociatedIndices[id]!.append(updatedIndex) + } + } + } + } + + mutating func insertLowerOrAtAnchorAtArrayIndex(_ index: Int, value: MutableMessageHistoryEntry) { + self.lowerOrAtAnchor.insert(value, at: index) + + for id in value.getAssociatedMessageIds() { + if self.reverseAssociatedIndices[id] == nil { + self.reverseAssociatedIndices[id] = [value.index] + } else { + self.reverseAssociatedIndices[id]!.append(value.index) + } + } + } + + mutating func insertHigherThanAnchorAtArrayIndex(_ index: Int, value: MutableMessageHistoryEntry) { + self.higherThanAnchor.insert(value, at: index) + + for id in value.getAssociatedMessageIds() { + if self.reverseAssociatedIndices[id] == nil { + self.reverseAssociatedIndices[id] = [value.index] + } else { + self.reverseAssociatedIndices[id]!.append(value.index) + } + } + } + + mutating func removeLowerOrAtAnchorAtArrayIndex(_ index: Int) { + let previousIndex = self.lowerOrAtAnchor[index].index + for id in self.lowerOrAtAnchor[index].getAssociatedMessageIds() { + self.reverseAssociatedIndices[id]?.removeAll(where: { $0 == previousIndex }) + if let isEmpty = self.reverseAssociatedIndices[id]?.isEmpty, isEmpty { + self.reverseAssociatedIndices.removeValue(forKey: id) + } + } + + self.lowerOrAtAnchor.remove(at: index) + } + + mutating func removeHigherThanAnchorAtArrayIndex(_ index: Int) { + let previousIndex = self.higherThanAnchor[index].index + for id in self.higherThanAnchor[index].getAssociatedMessageIds() { + self.reverseAssociatedIndices[id]?.removeAll(where: { $0 == previousIndex }) + if let isEmpty = self.reverseAssociatedIndices[id]?.isEmpty, isEmpty { + self.reverseAssociatedIndices.removeValue(forKey: id) + } + } + + self.higherThanAnchor.remove(at: index) + } mutating func fixMonotony() { if self.lowerOrAtAnchor.count > 1 { @@ -688,6 +709,10 @@ struct OrderedHistoryViewEntries { } } + func indicesForAssociatedMessageId(_ id: MessageId) -> [MessageIndex]? { + return self.reverseAssociatedIndices[id] + } + var first: MutableMessageHistoryEntry? { return self.lowerOrAtAnchor.first ?? self.higherThanAnchor.first } @@ -696,13 +721,13 @@ struct OrderedHistoryViewEntries { var anyUpdated = false for i in 0 ..< self.lowerOrAtAnchor.count { if let updated = f(self.lowerOrAtAnchor[i]) { - self.lowerOrAtAnchor[i] = updated + self.setLowerOrAtAnchorAtArrayIndex(i, to: updated) anyUpdated = true } } for i in 0 ..< self.higherThanAnchor.count { if let updated = f(self.higherThanAnchor[i]) { - self.higherThanAnchor[i] = updated + self.setHigherThanAnchorAtArrayIndex(i, to: updated) anyUpdated = true } } @@ -712,12 +737,12 @@ struct OrderedHistoryViewEntries { mutating func update(index: MessageIndex, _ f: (MutableMessageHistoryEntry) -> MutableMessageHistoryEntry?) -> Bool { if let entryIndex = binarySearch(self.lowerOrAtAnchor, extract: { $0.index }, searchItem: index) { if let updated = f(self.lowerOrAtAnchor[entryIndex]) { - self.lowerOrAtAnchor[entryIndex] = updated + self.setLowerOrAtAnchorAtArrayIndex(entryIndex, to: updated) return true } } else if let entryIndex = binarySearch(self.higherThanAnchor, extract: { $0.index }, searchItem: index) { if let updated = f(self.higherThanAnchor[entryIndex]) { - self.higherThanAnchor[entryIndex] = updated + self.setHigherThanAnchorAtArrayIndex(entryIndex, to: updated) return true } } @@ -726,10 +751,10 @@ struct OrderedHistoryViewEntries { mutating func remove(index: MessageIndex) -> Bool { if let entryIndex = binarySearch(self.lowerOrAtAnchor, extract: { $0.index }, searchItem: index) { - self.lowerOrAtAnchor.remove(at: entryIndex) + self.removeLowerOrAtAnchorAtArrayIndex(entryIndex) return true } else if let entryIndex = binarySearch(self.higherThanAnchor, extract: { $0.index }, searchItem: index) { - self.higherThanAnchor.remove(at: entryIndex) + self.removeHigherThanAnchorAtArrayIndex(entryIndex) return true } else { return false @@ -854,10 +879,10 @@ final class HistoryViewLoadedState { let currentLocation = nextLocation nextLocation = nextLocation.successor switch entry { - case let .IntermediateMessageEntry(message, _, monthLocation): - return .IntermediateMessageEntry(message, currentLocation, monthLocation) - case let .MessageEntry(entry): - return .MessageEntry(MessageHistoryMessageEntry(message: entry.message, location: currentLocation, monthLocation: entry.monthLocation, attributes: entry.attributes)) + case let .IntermediateMessageEntry(message, _, monthLocation): + return .IntermediateMessageEntry(message, currentLocation, monthLocation) + case let .MessageEntry(entry, reloadAssociatedMessages, reloadPeers): + return .MessageEntry(MessageHistoryMessageEntry(message: entry.message, location: currentLocation, monthLocation: entry.monthLocation, attributes: entry.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers) } } } @@ -880,8 +905,8 @@ final class HistoryViewLoadedState { switch entry { case let .IntermediateMessageEntry(message, location, _): return .IntermediateMessageEntry(message, location, MessageHistoryEntryMonthLocation(indexInMonth: Int32(currentIndexInMonth))) - case let .MessageEntry(entry): - return .MessageEntry(MessageHistoryMessageEntry(message: entry.message, location: entry.location, monthLocation: MessageHistoryEntryMonthLocation(indexInMonth: Int32(currentIndexInMonth)), attributes: entry.attributes)) + case let .MessageEntry(entry, reloadAssociatedMessages, reloadPeers): + return .MessageEntry(MessageHistoryMessageEntry(message: entry.message, location: entry.location, monthLocation: MessageHistoryEntryMonthLocation(indexInMonth: Int32(currentIndexInMonth)), attributes: entry.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers) } } } @@ -940,8 +965,8 @@ final class HistoryViewLoadedState { switch entry { case let .IntermediateMessageEntry(message, location, monthLocation): return .IntermediateMessageEntry(message.withUpdatedGroupInfo(groupInfo), location, monthLocation) - case let .MessageEntry(messageEntry): - return .MessageEntry(MessageHistoryMessageEntry(message: messageEntry.message.withUpdatedGroupInfo(groupInfo), location: messageEntry.location, monthLocation: messageEntry.monthLocation, attributes: messageEntry.attributes)) + case let .MessageEntry(messageEntry, reloadAssociatedMessages, reloadPeers): + return .MessageEntry(MessageHistoryMessageEntry(message: messageEntry.message.withUpdatedGroupInfo(groupInfo), location: messageEntry.location, monthLocation: messageEntry.monthLocation, attributes: messageEntry.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers) } } return nil @@ -963,8 +988,8 @@ final class HistoryViewLoadedState { switch entry { case let .IntermediateMessageEntry(message, location, monthLocation): return .IntermediateMessageEntry(message.withUpdatedEmbeddedMedia(buffer), location, monthLocation) - case let .MessageEntry(messageEntry): - return .MessageEntry(MessageHistoryMessageEntry(message: messageEntry.message, location: messageEntry.location, monthLocation: messageEntry.monthLocation, attributes: messageEntry.attributes)) + case let .MessageEntry(messageEntry, reloadAssociatedMessages, reloadPeers): + return .MessageEntry(MessageHistoryMessageEntry(message: messageEntry.message, location: messageEntry.location, monthLocation: messageEntry.monthLocation, attributes: messageEntry.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers) } }) } @@ -974,8 +999,9 @@ final class HistoryViewLoadedState { for space in self.orderedEntriesBySpace.keys { let spaceUpdated = self.orderedEntriesBySpace[space]!.mutableScan({ entry in switch entry { - case let .MessageEntry(value): + case let .MessageEntry(value, reloadAssociatedMessages, reloadPeers): let message = value.message + var reloadPeers = reloadPeers var rebuild = false for media in message.media { @@ -990,6 +1016,9 @@ final class HistoryViewLoadedState { for media in message.media { if let mediaId = media.id, let updated = updatedMedia[mediaId] { if let updated = updated { + if media.peerIds != updated.peerIds { + reloadPeers = true + } messageMedia.append(updated) } } else { @@ -997,7 +1026,7 @@ final class HistoryViewLoadedState { } } let updatedMessage = Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: message.timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: messageMedia, peers: message.peers, associatedMessages: message.associatedMessages, associatedMessageIds: message.associatedMessageIds) - return .MessageEntry(MessageHistoryMessageEntry(message: updatedMessage, location: value.location, monthLocation: value.monthLocation, attributes: value.attributes)) + return .MessageEntry(MessageHistoryMessageEntry(message: updatedMessage, location: value.location, monthLocation: value.monthLocation, attributes: value.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers) } case .IntermediateMessageEntry: break @@ -1019,24 +1048,20 @@ final class HistoryViewLoadedState { } var updated = false - /*for i in 0 ..< self.orderedEntriesBySpace[space]!.entries.count { - switch self.orderedEntriesBySpace[space]!.entries[i] { - case .IntermediateMessageEntry: - break - case let .MessageEntry(currentEntry): - if !currentEntry.message.associatedMessageIds.isEmpty && currentEntry.message.associatedMessageIds.contains(entry.index.id) { - var associatedMessages = currentEntry.message.associatedMessages - switch entry { - case let .IntermediateMessageEntry(message, _, _): - associatedMessages[entry.index.id] = postbox.messageHistoryTable.renderMessage(message, peerTable: postbox.peerTable) - case let .MessageEntry(message): - associatedMessages[entry.index.id] = message.message - } - self.orderedEntriesBySpace[space]!.entries[i] = .MessageEntry(MessageHistoryMessageEntry(message: currentEntry.message.withUpdatedAssociatedMessages(associatedMessages), location: currentEntry.location, monthLocation: currentEntry.monthLocation, attributes: currentEntry.attributes)) + + if let associatedIndices = self.orderedEntriesBySpace[space]!.indicesForAssociatedMessageId(entry.index.id) { + for associatedIndex in associatedIndices { + self.orderedEntriesBySpace[space]!.update(index: associatedIndex, { current in + switch current { + case .IntermediateMessageEntry: + return current + case let .MessageEntry(messageEntry, _, reloadPeers): updated = true + return .MessageEntry(messageEntry, reloadAssociatedMessages: true, reloadPeers: reloadPeers) } + }) } - }*/ + } if self.anchor.isEqualOrGreater(than: entry.index) { let insertionIndex = binaryInsertionIndex(self.orderedEntriesBySpace[space]!.lowerOrAtAnchor, extract: { $0.index }, searchItem: entry.index) @@ -1044,7 +1069,7 @@ final class HistoryViewLoadedState { if insertionIndex < self.orderedEntriesBySpace[space]!.lowerOrAtAnchor.count { if self.orderedEntriesBySpace[space]!.lowerOrAtAnchor[insertionIndex].index == entry.index { assertionFailure("Inserting an existing index is not allowed") - self.orderedEntriesBySpace[space]!.lowerOrAtAnchor[insertionIndex] = entry + self.orderedEntriesBySpace[space]!.setLowerOrAtAnchorAtArrayIndex(insertionIndex, to: entry) return true } } @@ -1052,9 +1077,9 @@ final class HistoryViewLoadedState { if insertionIndex == 0 && self.orderedEntriesBySpace[space]!.lowerOrAtAnchor.count >= self.halfLimit { return updated } - self.orderedEntriesBySpace[space]!.lowerOrAtAnchor.insert(entry, at: insertionIndex) + self.orderedEntriesBySpace[space]!.insertLowerOrAtAnchorAtArrayIndex(insertionIndex, value: entry) if self.orderedEntriesBySpace[space]!.lowerOrAtAnchor.count > self.halfLimit { - self.orderedEntriesBySpace[space]!.lowerOrAtAnchor.removeFirst() + self.orderedEntriesBySpace[space]!.removeLowerOrAtAnchorAtArrayIndex(0) } return true } else { @@ -1063,7 +1088,7 @@ final class HistoryViewLoadedState { if insertionIndex < self.orderedEntriesBySpace[space]!.higherThanAnchor.count { if self.orderedEntriesBySpace[space]!.higherThanAnchor[insertionIndex].index == entry.index { assertionFailure("Inserting an existing index is not allowed") - self.orderedEntriesBySpace[space]!.higherThanAnchor[insertionIndex] = entry + self.orderedEntriesBySpace[space]!.setHigherThanAnchorAtArrayIndex(insertionIndex, to: entry) return true } } @@ -1071,9 +1096,9 @@ final class HistoryViewLoadedState { if insertionIndex == self.orderedEntriesBySpace[space]!.higherThanAnchor.count && self.orderedEntriesBySpace[space]!.higherThanAnchor.count >= self.halfLimit { return updated } - self.orderedEntriesBySpace[space]!.higherThanAnchor.insert(entry, at: insertionIndex) + self.orderedEntriesBySpace[space]!.insertHigherThanAnchorAtArrayIndex(insertionIndex, value: entry) if self.orderedEntriesBySpace[space]!.higherThanAnchor.count > self.halfLimit { - self.orderedEntriesBySpace[space]!.higherThanAnchor.removeLast() + self.orderedEntriesBySpace[space]!.removeHigherThanAnchorAtArrayIndex(self.orderedEntriesBySpace[space]!.higherThanAnchor.count - 1) } return true } @@ -1087,17 +1112,24 @@ final class HistoryViewLoadedState { var updated = false - /*for i in 0 ..< self.orderedEntriesBySpace[space]!.entries.count { - switch self.orderedEntriesBySpace[space]!.entries[i] { - case .IntermediateMessageEntry: - break - case let .MessageEntry(entry): - if let associatedMessages = entry.message.associatedMessages.filteredOut(keysIn: [index.id]) { - self.orderedEntriesBySpace[space]!.entries[i] = .MessageEntry(MessageHistoryMessageEntry(message: entry.message.withUpdatedAssociatedMessages(associatedMessages), location: entry.location, monthLocation: entry.monthLocation, attributes: entry.attributes)) + if let associatedIndices = self.orderedEntriesBySpace[space]!.indicesForAssociatedMessageId(index.id) { + for associatedIndex in associatedIndices { + self.orderedEntriesBySpace[space]!.update(index: associatedIndex, { current in + switch current { + case .IntermediateMessageEntry: + return current + case let .MessageEntry(messageEntry, reloadAssociatedMessages, reloadPeers): updated = true + + if let associatedMessages = messageEntry.message.associatedMessages.filteredOut(keysIn: [index.id]) { + return .MessageEntry(MessageHistoryMessageEntry(message: messageEntry.message.withUpdatedAssociatedMessages(associatedMessages), location: messageEntry.location, monthLocation: messageEntry.monthLocation, attributes: messageEntry.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers) + } else { + return current + } } + }) } - }*/ + } if self.orderedEntriesBySpace[space]!.remove(index: index) { self.spacesWithRemovals.insert(space) @@ -1107,7 +1139,7 @@ final class HistoryViewLoadedState { return updated } - func completeAndSample(postbox: Postbox) -> HistoryViewLoadedSample { + func completeAndSample(postbox: Postbox, clipHoles: Bool) -> HistoryViewLoadedSample { if !self.spacesWithRemovals.isEmpty { for space in self.spacesWithRemovals { self.fillSpace(space: space, postbox: postbox) @@ -1138,7 +1170,7 @@ final class HistoryViewLoadedState { entry = self.orderedEntriesBySpace[space]!.higherThanAnchor[index] } - if !clipRanges.isEmpty { + if clipHoles && !clipRanges.isEmpty { let entryIndex = entry.index for range in clipRanges { if range.contains(entryIndex) { @@ -1154,8 +1186,27 @@ final class HistoryViewLoadedState { } switch entry { - case let .MessageEntry(value): - result.append(value) + case let .MessageEntry(value, reloadAssociatedMessages, reloadPeers): + var updatedMessage = value.message + if reloadAssociatedMessages { + let associatedMessages = postbox.messageHistoryTable.renderAssociatedMessages(associatedMessageIds: value.message.associatedMessageIds, peerTable: postbox.peerTable) + updatedMessage = value.message.withUpdatedAssociatedMessages(associatedMessages) + } + if reloadPeers { + updatedMessage = postbox.messageHistoryTable.renderMessagePeers(updatedMessage, peerTable: postbox.peerTable) + } + + if value.message !== updatedMessage { + let updatedValue = MessageHistoryMessageEntry(message: updatedMessage, location: value.location, monthLocation: value.monthLocation, attributes: value.attributes) + if directionIndex == 0 { + self.orderedEntriesBySpace[space]!.setLowerOrAtAnchorAtArrayIndex(index, to: .MessageEntry(updatedValue, reloadAssociatedMessages: false, reloadPeers: false)) + } else { + self.orderedEntriesBySpace[space]!.setHigherThanAnchorAtArrayIndex(index, to: .MessageEntry(updatedValue, reloadAssociatedMessages: false, reloadPeers: false)) + } + result.append(updatedValue) + } else { + result.append(value) + } case let .IntermediateMessageEntry(message, location, monthLocation): let renderedMessage = postbox.messageHistoryTable.renderMessage(message, peerTable: postbox.peerTable) var authorIsContact = false @@ -1164,9 +1215,9 @@ final class HistoryViewLoadedState { } let entry = MessageHistoryMessageEntry(message: renderedMessage, location: location, monthLocation: monthLocation, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: authorIsContact)) if directionIndex == 0 { - self.orderedEntriesBySpace[space]!.lowerOrAtAnchor[index] = .MessageEntry(entry) + self.orderedEntriesBySpace[space]!.setLowerOrAtAnchorAtArrayIndex(index, to: .MessageEntry(entry, reloadAssociatedMessages: false, reloadPeers: false)) } else { - self.orderedEntriesBySpace[space]!.higherThanAnchor[index] = .MessageEntry(entry) + self.orderedEntriesBySpace[space]!.setHigherThanAnchorAtArrayIndex(index, to: .MessageEntry(entry, reloadAssociatedMessages: false, reloadPeers: false)) } result.append(entry) } @@ -1327,12 +1378,12 @@ enum HistoryViewState { } } - func sample(postbox: Postbox) -> HistoryViewSample { + func sample(postbox: Postbox, clipHoles: Bool) -> HistoryViewSample { switch self { - case let .loading(loadingState): - return .loading(loadingState.checkAndSample(postbox: postbox)) - case let .loaded(loadedState): - return .loaded(loadedState.completeAndSample(postbox: postbox)) + case let .loading(loadingState): + return .loading(loadingState.checkAndSample(postbox: postbox)) + case let .loaded(loadedState): + return .loaded(loadedState.completeAndSample(postbox: postbox, clipHoles: clipHoles)) } } } diff --git a/submodules/Postbox/Sources/MessageOfInterestHolesView.swift b/submodules/Postbox/Sources/MessageOfInterestHolesView.swift index b07fe3c366..caa7f03362 100644 --- a/submodules/Postbox/Sources/MessageOfInterestHolesView.swift +++ b/submodules/Postbox/Sources/MessageOfInterestHolesView.swift @@ -15,9 +15,13 @@ public struct HolesViewMedia: Comparable { } } -public struct MessageOfInterestHole: Hashable, Equatable { +public struct MessageOfInterestHole: Hashable, Equatable, CustomStringConvertible { public let hole: MessageHistoryViewHole public let direction: MessageHistoryViewRelativeHoleDirection + + public var description: String { + return "hole: \(self.hole), direction: \(self.direction)" + } } public enum MessageOfInterestViewLocation: Hashable { @@ -29,6 +33,7 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView { private let count: Int private var anchor: HistoryViewInputAnchor private var wrappedView: MutableMessageHistoryView + private var peerIds: MessageHistoryViewPeerIds fileprivate var closestHole: MessageOfInterestHole? fileprivate var closestLaterMedia: [HolesViewMedia] = [] @@ -37,28 +42,31 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView { self.location = location self.count = count - var peerId: PeerId + let mainPeerId: PeerId + let peerIds: MessageHistoryViewPeerIds switch self.location { - case let .peer(id): - peerId = id + case let .peer(id): + mainPeerId = id + peerIds = postbox.peerIdsForLocation(.peer(id), tagMask: nil) } + self.peerIds = peerIds var anchor: HistoryViewInputAnchor = .upperBound - if let combinedState = postbox.readStateTable.getCombinedState(peerId), let state = combinedState.states.first, state.1.count != 0 { + if let combinedState = postbox.readStateTable.getCombinedState(mainPeerId), let state = combinedState.states.first, state.1.count != 0 { switch state.1 { - case let .idBased(maxIncomingReadId, _, _, _, _): - anchor = .message(MessageId(peerId: peerId, namespace: state.0, id: maxIncomingReadId)) - case let .indexBased(maxIncomingReadIndex, _, _, _): - anchor = .index(maxIncomingReadIndex) + case let .idBased(maxIncomingReadId, _, _, _, _): + anchor = .message(MessageId(peerId: mainPeerId, namespace: state.0, id: maxIncomingReadId)) + case let .indexBased(maxIncomingReadIndex, _, _, _): + anchor = .index(maxIncomingReadIndex) } } self.anchor = anchor - self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], peerIds: .single(peerId), anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0}) + self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, peerIds: peerIds, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0}) let _ = self.updateFromView() } private func updateFromView() -> Bool { let closestHole: MessageOfInterestHole? - if let (hole, direction) = self.wrappedView.firstHole() { + if let (hole, direction, _) = self.wrappedView.firstHole() { closestHole = MessageOfInterestHole(hole: hole, direction: direction) } else { closestHole = nil @@ -66,26 +74,26 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView { var closestLaterMedia: [HolesViewMedia] = [] switch self.wrappedView.sampledState { - case .loading: - break - case let .loaded(sample): - switch sample.anchor { - case .index: - let anchorIndex = binaryIndexOrLower(sample.entries, sample.anchor) - loop: for i in max(0, anchorIndex) ..< sample.entries.count { - let message = sample.entries[i].message - if !message.media.isEmpty, let peer = message.peers[message.id.peerId] { - for media in message.media { - closestLaterMedia.append(HolesViewMedia(media: media, peer: peer, authorIsContact: sample.entries[i].attributes.authorIsContact, index: message.index)) - } - } - if closestLaterMedia.count >= 3 { - break loop - } + case .loading: + break + case let .loaded(sample): + switch sample.anchor { + case .index: + let anchorIndex = binaryIndexOrLower(sample.entries, sample.anchor) + loop: for i in max(0, anchorIndex) ..< sample.entries.count { + let message = sample.entries[i].message + if !message.media.isEmpty, let peer = message.peers[message.id.peerId] { + for media in message.media { + closestLaterMedia.append(HolesViewMedia(media: media, peer: peer, authorIsContact: sample.entries[i].attributes.authorIsContact, index: message.index)) } - case .lowerBound, .upperBound: - break + } + if closestLaterMedia.count >= 3 { + break loop + } } + case .lowerBound, .upperBound: + break + } } if self.closestHole != closestHole || self.closestLaterMedia != closestLaterMedia { @@ -105,21 +113,56 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView { } var anchor: HistoryViewInputAnchor = self.anchor if transaction.alteredInitialPeerCombinedReadStates[peerId] != nil { + var updatedAnchor: HistoryViewInputAnchor = .upperBound if let combinedState = postbox.readStateTable.getCombinedState(peerId), let state = combinedState.states.first, state.1.count != 0 { switch state.1 { - case let .idBased(maxIncomingReadId, _, _, _, _): - anchor = .message(MessageId(peerId: peerId, namespace: state.0, id: maxIncomingReadId)) - case let .indexBased(maxIncomingReadIndex, _, _, _): - anchor = .index(maxIncomingReadIndex) + case let .idBased(maxIncomingReadId, _, _, _, _): + anchor = .message(MessageId(peerId: peerId, namespace: state.0, id: maxIncomingReadId)) + case let .indexBased(maxIncomingReadIndex, _, _, _): + anchor = .index(maxIncomingReadIndex) } } + anchor = updatedAnchor } if self.anchor != anchor { self.anchor = anchor - self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], peerIds: .single(peerId), anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0}) + let peerIds: MessageHistoryViewPeerIds + switch self.location { + case let .peer(id): + peerIds = postbox.peerIdsForLocation(.peer(id), tagMask: nil) + } + self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, peerIds: peerIds, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0}) return self.updateFromView() } else if self.wrappedView.replay(postbox: postbox, transaction: transaction) { + var reloadView = false + if !transaction.currentPeerHoleOperations.isEmpty { + var allPeerIds: [PeerId] + switch peerIds { + case let .single(peerId): + allPeerIds = [peerId] + case let .associated(peerId, attachedMessageId): + allPeerIds = [peerId] + if let attachedMessageId = attachedMessageId { + allPeerIds.append(attachedMessageId.peerId) + } + } + for (key, _) in transaction.currentPeerHoleOperations { + if allPeerIds.contains(key.peerId) { + reloadView = true + break + } + } + } + if reloadView { + let peerIds: MessageHistoryViewPeerIds + switch self.location { + case let .peer(id): + peerIds = postbox.peerIdsForLocation(.peer(id), tagMask: nil) + } + self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], clipHoles: true, peerIds: peerIds, anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0}) + } + return self.updateFromView() } else { return false diff --git a/submodules/Postbox/Sources/Peer.swift b/submodules/Postbox/Sources/Peer.swift index 69c279993a..0f0cba4b37 100644 --- a/submodules/Postbox/Sources/Peer.swift +++ b/submodules/Postbox/Sources/Peer.swift @@ -132,6 +132,22 @@ public func arePeerDictionariesEqual(_ lhs: SimpleDictionary, _ rh return true } +public func arePeerDictionariesEqual(_ lhs: [PeerId: Peer], _ rhs: [PeerId: Peer]) -> Bool { + if lhs.count != rhs.count { + return false + } + for (id, lhsPeer) in lhs { + if let rhsPeer = rhs[id] { + if !lhsPeer.isEqual(rhsPeer) { + return false + } + } else { + return false + } + } + return true +} + public struct PeerSummaryCounterTags: OptionSet, Sequence, Hashable { public var rawValue: Int32 diff --git a/submodules/Postbox/Sources/PeerNotificationSettingsTable.swift b/submodules/Postbox/Sources/PeerNotificationSettingsTable.swift index 2d45e0e054..7fea4ea0d4 100644 --- a/submodules/Postbox/Sources/PeerNotificationSettingsTable.swift +++ b/submodules/Postbox/Sources/PeerNotificationSettingsTable.swift @@ -225,7 +225,7 @@ final class PeerNotificationSettingsTable: Table { return (added, removed) } - func resetAll(to settings: PeerNotificationSettings, updatedSettings: inout Set, updatedTimestamps: inout [PeerId: PeerNotificationSettingsBehaviorTimestamp]) -> [PeerId] { + func resetAll(to settings: PeerNotificationSettings, updatedSettings: inout Set, updatedTimestamps: inout [PeerId: PeerNotificationSettingsBehaviorTimestamp]) -> [PeerId: PeerNotificationSettings?] { let lowerBound = ValueBoxKey(length: 8) lowerBound.setInt64(0, value: 0) let upperBound = ValueBoxKey(length: 8) @@ -236,17 +236,17 @@ final class PeerNotificationSettingsTable: Table { return true }, limit: 0) - var updatedPeerIds: [PeerId] = [] + var updatedPeers: [PeerId: PeerNotificationSettings?] = [:] for peerId in peerIds { let entry = self.getEntry(peerId) if let current = entry.current, !current.isEqual(to: settings) || entry.pending != nil { let _ = self.setCurrent(id: peerId, settings: settings, updatedTimestamps: &updatedTimestamps) let _ = self.setPending(id: peerId, settings: nil, updatedSettings: &updatedSettings) - updatedPeerIds.append(peerId) + updatedPeers[peerId] = entry.effective } } - return updatedPeerIds + return updatedPeers } override func beforeCommit() { diff --git a/submodules/Postbox/Sources/PeerNotificationSettingsView.swift b/submodules/Postbox/Sources/PeerNotificationSettingsView.swift index c22d0b4a93..22a0825d02 100644 --- a/submodules/Postbox/Sources/PeerNotificationSettingsView.swift +++ b/submodules/Postbox/Sources/PeerNotificationSettingsView.swift @@ -26,7 +26,7 @@ final class MutablePeerNotificationSettingsView: MutablePostboxView { if let peer = postbox.peerTable.get(peerId), let associatedPeerId = peer.associatedPeerId { notificationPeerId = associatedPeerId } - if let settings = transaction.currentUpdatedPeerNotificationSettings[notificationPeerId] { + if let (_, settings) = transaction.currentUpdatedPeerNotificationSettings[notificationPeerId] { self.notificationSettings[peerId] = settings updated = true } diff --git a/submodules/Postbox/Sources/PeerView.swift b/submodules/Postbox/Sources/PeerView.swift index fd389a7fa5..0c5d610e2f 100644 --- a/submodules/Postbox/Sources/PeerView.swift +++ b/submodules/Postbox/Sources/PeerView.swift @@ -214,12 +214,12 @@ final class MutablePeerView: MutablePostboxView { if let peer = self.peers[self.peerId] { if let associatedPeerId = peer.associatedPeerId { - if let notificationSettings = updatedNotificationSettings[associatedPeerId] { + if let (_, notificationSettings) = updatedNotificationSettings[associatedPeerId] { self.notificationSettings = notificationSettings updated = true } } else { - if let notificationSettings = updatedNotificationSettings[peer.id] { + if let (_, notificationSettings) = updatedNotificationSettings[peer.id] { self.notificationSettings = notificationSettings updated = true } diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 1c8abe8ff9..31548f7f4f 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -107,6 +107,14 @@ public final class Transaction { return [] } } + public func failedMessageIds(for peerId: PeerId) -> [MessageId] { + assert(!self.disposed) + if let postbox = self.postbox { + return postbox.failedMessageIds(for: peerId) + } else { + return [] + } + } public func deleteMessagesWithGlobalIds(_ ids: [Int32], forEachMedia: (Media) -> Void) { assert(!self.disposed) @@ -647,6 +655,11 @@ public final class Transaction { return self.postbox?.retrieveItemCacheEntry(id: id) } + public func clearItemCacheCollection(collectionId: ItemCacheCollectionId) { + assert(!self.disposed) + self.postbox?.clearItemCacheCollection(collectionId: collectionId) + } + public func operationLogGetNextEntryLocalIndex(peerId: PeerId, tag: PeerOperationLogTag) -> Int32 { assert(!self.disposed) if let postbox = self.postbox { @@ -919,6 +932,11 @@ public final class Transaction { assert(!self.disposed) self.postbox?.addHolesEverywhere(peerNamespaces: peerNamespaces, holeNamespace: holeNamespace) } + + public func reindexUnreadCounters() { + assert(!self.disposed) + self.postbox?.reindexUnreadCounters() + } } public enum PostboxResult { @@ -1059,7 +1077,7 @@ public final class Postbox { private var currentPeerHoleOperations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:] private var currentUpdatedPeers: [PeerId: Peer] = [:] - private var currentUpdatedPeerNotificationSettings: [PeerId: PeerNotificationSettings] = [:] + private var currentUpdatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)] = [:] private var currentUpdatedPeerNotificationBehaviorTimestamps: [PeerId: PeerNotificationSettingsBehaviorTimestamp] = [:] private var currentUpdatedCachedPeerData: [PeerId: CachedPeerData] = [:] private var currentUpdatedPeerPresences: [PeerId: PeerPresence] = [:] @@ -1129,6 +1147,7 @@ public final class Postbox { let additionalChatListItemsTable: AdditionalChatListItemsTable let messageHistoryMetadataTable: MessageHistoryMetadataTable let messageHistoryUnsentTable: MessageHistoryUnsentTable + let messageHistoryFailedTable: MessageHistoryFailedTable let messageHistoryTagsTable: MessageHistoryTagsTable let globalMessageHistoryTagsTable: GlobalMessageHistoryTagsTable let localMessageHistoryTagsTable: LocalMessageHistoryTagsTable @@ -1206,6 +1225,7 @@ public final class Postbox { self.messageHistoryMetadataTable = MessageHistoryMetadataTable(valueBox: self.valueBox, table: MessageHistoryMetadataTable.tableSpec(10)) self.messageHistoryHoleIndexTable = MessageHistoryHoleIndexTable(valueBox: self.valueBox, table: MessageHistoryHoleIndexTable.tableSpec(56), metadataTable: self.messageHistoryMetadataTable, seedConfiguration: self.seedConfiguration) self.messageHistoryUnsentTable = MessageHistoryUnsentTable(valueBox: self.valueBox, table: MessageHistoryUnsentTable.tableSpec(11)) + self.messageHistoryFailedTable = MessageHistoryFailedTable(valueBox: self.valueBox, table: MessageHistoryFailedTable.tableSpec(49)) self.invalidatedMessageHistoryTagsSummaryTable = InvalidatedMessageHistoryTagsSummaryTable(valueBox: self.valueBox, table: InvalidatedMessageHistoryTagsSummaryTable.tableSpec(47)) self.messageHistoryTagsSummaryTable = MessageHistoryTagsSummaryTable(valueBox: self.valueBox, table: MessageHistoryTagsSummaryTable.tableSpec(44), invalidateTable: self.invalidatedMessageHistoryTagsSummaryTable) self.pendingMessageActionsMetadataTable = PendingMessageActionsMetadataTable(valueBox: self.valueBox, table: PendingMessageActionsMetadataTable.tableSpec(45)) @@ -1222,7 +1242,7 @@ public final class Postbox { self.timestampBasedMessageAttributesTable = TimestampBasedMessageAttributesTable(valueBox: self.valueBox, table: TimestampBasedMessageAttributesTable.tableSpec(34), indexTable: self.timestampBasedMessageAttributesIndexTable) self.textIndexTable = MessageHistoryTextIndexTable(valueBox: self.valueBox, table: MessageHistoryTextIndexTable.tableSpec(41)) self.additionalChatListItemsTable = AdditionalChatListItemsTable(valueBox: self.valueBox, table: AdditionalChatListItemsTable.tableSpec(55)) - self.messageHistoryTable = MessageHistoryTable(valueBox: self.valueBox, table: MessageHistoryTable.tableSpec(7), seedConfiguration: seedConfiguration, messageHistoryIndexTable: self.messageHistoryIndexTable, messageHistoryHoleIndexTable: self.messageHistoryHoleIndexTable, messageMediaTable: self.mediaTable, historyMetadataTable: self.messageHistoryMetadataTable, globallyUniqueMessageIdsTable: self.globallyUniqueMessageIdsTable, unsentTable: self.messageHistoryUnsentTable, tagsTable: self.messageHistoryTagsTable, globalTagsTable: self.globalMessageHistoryTagsTable, localTagsTable: self.localMessageHistoryTagsTable, readStateTable: self.readStateTable, synchronizeReadStateTable: self.synchronizeReadStateTable, textIndexTable: self.textIndexTable, summaryTable: self.messageHistoryTagsSummaryTable, pendingActionsTable: self.pendingMessageActionsTable) + self.messageHistoryTable = MessageHistoryTable(valueBox: self.valueBox, table: MessageHistoryTable.tableSpec(7), seedConfiguration: seedConfiguration, messageHistoryIndexTable: self.messageHistoryIndexTable, messageHistoryHoleIndexTable: self.messageHistoryHoleIndexTable, messageMediaTable: self.mediaTable, historyMetadataTable: self.messageHistoryMetadataTable, globallyUniqueMessageIdsTable: self.globallyUniqueMessageIdsTable, unsentTable: self.messageHistoryUnsentTable, failedTable: self.messageHistoryFailedTable, tagsTable: self.messageHistoryTagsTable, globalTagsTable: self.globalMessageHistoryTagsTable, localTagsTable: self.localMessageHistoryTagsTable, readStateTable: self.readStateTable, synchronizeReadStateTable: self.synchronizeReadStateTable, textIndexTable: self.textIndexTable, summaryTable: self.messageHistoryTagsSummaryTable, pendingActionsTable: self.pendingMessageActionsTable) self.peerChatStateTable = PeerChatStateTable(valueBox: self.valueBox, table: PeerChatStateTable.tableSpec(13)) self.peerNameTokenIndexTable = ReverseIndexReferenceTable(valueBox: self.valueBox, table: ReverseIndexReferenceTable.tableSpec(26)) self.peerNameIndexTable = PeerNameIndexTable(valueBox: self.valueBox, table: PeerNameIndexTable.tableSpec(27), peerTable: self.peerTable, peerNameTokenIndexTable: self.peerNameTokenIndexTable) @@ -1262,6 +1282,7 @@ public final class Postbox { tables.append(self.globallyUniqueMessageIdsTable) tables.append(self.messageHistoryMetadataTable) tables.append(self.messageHistoryUnsentTable) + tables.append(self.messageHistoryFailedTable) tables.append(self.messageHistoryTagsTable) tables.append(self.globalMessageHistoryTagsTable) tables.append(self.localMessageHistoryTagsTable) @@ -1341,6 +1362,15 @@ public final class Postbox { print("(Postbox initialization took \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") let _ = self.transaction({ transaction -> Void in + let reindexUnreadVersion: Int32 = 2 + if self.messageHistoryMetadataTable.getShouldReindexUnreadCountsState() != reindexUnreadVersion { + self.messageHistoryMetadataTable.setShouldReindexUnreadCounts(value: true) + self.messageHistoryMetadataTable.setShouldReindexUnreadCountsState(value: reindexUnreadVersion) + } + #if DEBUG + self.messageHistoryMetadataTable.setShouldReindexUnreadCounts(value: true) + #endif + if self.messageHistoryMetadataTable.shouldReindexUnreadCounts() { self.groupMessageStatsTable.removeAll() let startTime = CFAbsoluteTimeGetCurrent() @@ -1623,8 +1653,29 @@ public final class Postbox { self.synchronizeGroupMessageStatsTable.set(groupId: groupId, namespace: namespace, needsValidation: false, operations: &self.currentUpdatedGroupSummarySynchronizeOperations) } - func fetchAroundChatEntries(groupId: PeerGroupId, index: ChatListIndex, count: Int) -> (entries: [MutableChatListEntry], earlier: MutableChatListEntry?, later: MutableChatListEntry?) { - let (intermediateEntries, intermediateLower, intermediateUpper) = self.chatListTable.entriesAround(groupId: groupId, index: index, messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count) + private func mappedChatListFilterPredicate(_ predicate: @escaping (Peer, PeerNotificationSettings?, Bool) -> Bool) -> (ChatListIntermediateEntry) -> Bool { + return { entry in + switch entry { + case let .message(index, _, _): + if let peer = self.peerTable.get(index.messageIndex.id.peerId) { + let isUnread = self.readStateTable.getCombinedState(index.messageIndex.id.peerId)?.isUnread ?? false + if predicate(peer, self.peerNotificationSettingsTable.getEffective(index.messageIndex.id.peerId), isUnread) { + return true + } else { + return false + } + } else { + return false + } + case .hole: + return true + } + } + } + + func fetchAroundChatEntries(groupId: PeerGroupId, index: ChatListIndex, count: Int, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)?) -> (entries: [MutableChatListEntry], earlier: MutableChatListEntry?, later: MutableChatListEntry?) { + let mappedPredicate = filterPredicate.flatMap(self.mappedChatListFilterPredicate) + let (intermediateEntries, intermediateLower, intermediateUpper) = self.chatListTable.entriesAround(groupId: groupId, index: index, messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate) let entries: [MutableChatListEntry] = intermediateEntries.map { entry in return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable) } @@ -1638,16 +1689,18 @@ public final class Postbox { return (entries, lower, upper) } - func fetchEarlierChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int) -> [MutableChatListEntry] { - let intermediateEntries = self.chatListTable.earlierEntries(groupId: groupId, index: index.flatMap({ ($0, true) }), messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count) + func fetchEarlierChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)?) -> [MutableChatListEntry] { + let mappedPredicate = filterPredicate.flatMap(self.mappedChatListFilterPredicate) + let intermediateEntries = self.chatListTable.earlierEntries(groupId: groupId, index: index.flatMap({ ($0, true) }), messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate) let entries: [MutableChatListEntry] = intermediateEntries.map { entry in return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable) } return entries } - func fetchLaterChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int) -> [MutableChatListEntry] { - let intermediateEntries = self.chatListTable.laterEntries(groupId: groupId, index: index.flatMap({ ($0, true) }), messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count) + func fetchLaterChatEntries(groupId: PeerGroupId, index: ChatListIndex?, count: Int, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)?) -> [MutableChatListEntry] { + let mappedPredicate = filterPredicate.flatMap(self.mappedChatListFilterPredicate) + let intermediateEntries = self.chatListTable.laterEntries(groupId: groupId, index: index.flatMap({ ($0, true) }), messageHistoryTable: self.messageHistoryTable, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable, count: count, predicate: mappedPredicate) let entries: [MutableChatListEntry] = intermediateEntries.map { entry in return MutableChatListEntry(entry, cachedDataTable: self.cachedPeerDataTable, readStateTable: self.readStateTable, messageHistoryTable: self.messageHistoryTable) } @@ -1689,7 +1742,7 @@ public final class Postbox { let transactionParticipationInTotalUnreadCountUpdates = self.peerNotificationSettingsTable.transactionParticipationInTotalUnreadCountUpdates(postbox: self) self.chatListIndexTable.commitWithTransaction(postbox: self, alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates, updatedPeers: updatedPeers, transactionParticipationInTotalUnreadCountUpdates: transactionParticipationInTotalUnreadCountUpdates, updatedRootUnreadState: &self.currentUpdatedTotalUnreadState, updatedGroupTotalUnreadSummaries: &self.currentUpdatedGroupTotalUnreadSummaries, currentUpdatedGroupSummarySynchronizeOperations: &self.currentUpdatedGroupSummarySynchronizeOperations) - let transaction = PostboxTransaction(currentUpdatedState: self.currentUpdatedState, currentPeerHoleOperations: self.currentPeerHoleOperations, currentOperationsByPeerId: self.currentOperationsByPeerId, chatListOperations: self.currentChatListOperations, currentUpdatedChatListInclusions: self.currentUpdatedChatListInclusions, currentUpdatedPeers: self.currentUpdatedPeers, currentUpdatedPeerNotificationSettings: self.currentUpdatedPeerNotificationSettings, currentUpdatedPeerNotificationBehaviorTimestamps: self.currentUpdatedPeerNotificationBehaviorTimestamps, currentUpdatedCachedPeerData: self.currentUpdatedCachedPeerData, currentUpdatedPeerPresences: currentUpdatedPeerPresences, currentUpdatedPeerChatListEmbeddedStates: self.currentUpdatedPeerChatListEmbeddedStates, currentUpdatedTotalUnreadState: self.currentUpdatedTotalUnreadState, currentUpdatedTotalUnreadSummaries: self.currentUpdatedGroupTotalUnreadSummaries, alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates, currentPeerMergedOperationLogOperations: self.currentPeerMergedOperationLogOperations, currentTimestampBasedMessageAttributesOperations: self.currentTimestampBasedMessageAttributesOperations, unsentMessageOperations: self.currentUnsentOperations, updatedSynchronizePeerReadStateOperations: self.currentUpdatedSynchronizeReadStateOperations, currentUpdatedGroupSummarySynchronizeOperations: self.currentUpdatedGroupSummarySynchronizeOperations, currentPreferencesOperations: self.currentPreferencesOperations, currentOrderedItemListOperations: self.currentOrderedItemListOperations, currentItemCollectionItemsOperations: self.currentItemCollectionItemsOperations, currentItemCollectionInfosOperations: self.currentItemCollectionInfosOperations, currentUpdatedPeerChatStates: self.currentUpdatedPeerChatStates, currentGlobalTagsOperations: self.currentGlobalTagsOperations, currentLocalTagsOperations: self.currentLocalTagsOperations, updatedMedia: self.currentUpdatedMedia, replaceRemoteContactCount: self.currentReplaceRemoteContactCount, replaceContactPeerIds: self.currentReplacedContactPeerIds, currentPendingMessageActionsOperations: self.currentPendingMessageActionsOperations, currentUpdatedMessageActionsSummaries: self.currentUpdatedMessageActionsSummaries, currentUpdatedMessageTagSummaries: self.currentUpdatedMessageTagSummaries, currentInvalidateMessageTagSummaries: self.currentInvalidateMessageTagSummaries, currentUpdatedPendingPeerNotificationSettings: self.currentUpdatedPendingPeerNotificationSettings, replacedAdditionalChatListItems: self.currentReplacedAdditionalChatListItems, updatedNoticeEntryKeys: self.currentUpdatedNoticeEntryKeys, updatedCacheEntryKeys: self.currentUpdatedCacheEntryKeys, currentUpdatedMasterClientId: currentUpdatedMasterClientId) + let transaction = PostboxTransaction(currentUpdatedState: self.currentUpdatedState, currentPeerHoleOperations: self.currentPeerHoleOperations, currentOperationsByPeerId: self.currentOperationsByPeerId, chatListOperations: self.currentChatListOperations, currentUpdatedChatListInclusions: self.currentUpdatedChatListInclusions, currentUpdatedPeers: self.currentUpdatedPeers, currentUpdatedPeerNotificationSettings: self.currentUpdatedPeerNotificationSettings, currentUpdatedPeerNotificationBehaviorTimestamps: self.currentUpdatedPeerNotificationBehaviorTimestamps, currentUpdatedCachedPeerData: self.currentUpdatedCachedPeerData, currentUpdatedPeerPresences: currentUpdatedPeerPresences, currentUpdatedPeerChatListEmbeddedStates: self.currentUpdatedPeerChatListEmbeddedStates, currentUpdatedTotalUnreadState: self.currentUpdatedTotalUnreadState, currentUpdatedTotalUnreadSummaries: self.currentUpdatedGroupTotalUnreadSummaries, alteredInitialPeerCombinedReadStates: alteredInitialPeerCombinedReadStates, currentPeerMergedOperationLogOperations: self.currentPeerMergedOperationLogOperations, currentTimestampBasedMessageAttributesOperations: self.currentTimestampBasedMessageAttributesOperations, unsentMessageOperations: self.currentUnsentOperations, updatedSynchronizePeerReadStateOperations: self.currentUpdatedSynchronizeReadStateOperations, currentUpdatedGroupSummarySynchronizeOperations: self.currentUpdatedGroupSummarySynchronizeOperations, currentPreferencesOperations: self.currentPreferencesOperations, currentOrderedItemListOperations: self.currentOrderedItemListOperations, currentItemCollectionItemsOperations: self.currentItemCollectionItemsOperations, currentItemCollectionInfosOperations: self.currentItemCollectionInfosOperations, currentUpdatedPeerChatStates: self.currentUpdatedPeerChatStates, currentGlobalTagsOperations: self.currentGlobalTagsOperations, currentLocalTagsOperations: self.currentLocalTagsOperations, updatedMedia: self.currentUpdatedMedia, replaceRemoteContactCount: self.currentReplaceRemoteContactCount, replaceContactPeerIds: self.currentReplacedContactPeerIds, currentPendingMessageActionsOperations: self.currentPendingMessageActionsOperations, currentUpdatedMessageActionsSummaries: self.currentUpdatedMessageActionsSummaries, currentUpdatedMessageTagSummaries: self.currentUpdatedMessageTagSummaries, currentInvalidateMessageTagSummaries: self.currentInvalidateMessageTagSummaries, currentUpdatedPendingPeerNotificationSettings: self.currentUpdatedPendingPeerNotificationSettings, replacedAdditionalChatListItems: self.currentReplacedAdditionalChatListItems, updatedNoticeEntryKeys: self.currentUpdatedNoticeEntryKeys, updatedCacheEntryKeys: self.currentUpdatedCacheEntryKeys, currentUpdatedMasterClientId: currentUpdatedMasterClientId, updatedFailedMessagePeerIds: self.messageHistoryFailedTable.updatedPeerIds, updatedFailedMessageIds: self.messageHistoryFailedTable.updatedMessageIds) var updatedTransactionState: Int64? var updatedMasterClientId: Int64? if !transaction.isEmpty { @@ -1758,6 +1811,9 @@ public final class Postbox { } return result } + fileprivate func failedMessageIds(for peerId: PeerId) -> [MessageId] { + return self.messageHistoryFailedTable.get(peerId: peerId) + } fileprivate func messageIdForGloballyUniqueMessageId(peerId: PeerId, id: Int64) -> MessageId? { return self.globallyUniqueMessageIdsTable.get(peerId: peerId, globallyUniqueId: id) @@ -1861,21 +1917,33 @@ public final class Postbox { fileprivate func updateCurrentPeerNotificationSettings(_ notificationSettings: [PeerId: PeerNotificationSettings]) { for (peerId, settings) in notificationSettings { + let previous: PeerNotificationSettings? + if let (value, _) = self.currentUpdatedPeerNotificationSettings[peerId] { + previous = value + } else { + previous = self.peerNotificationSettingsTable.getEffective(peerId) + } if let updated = self.peerNotificationSettingsTable.setCurrent(id: peerId, settings: settings, updatedTimestamps: &self.currentUpdatedPeerNotificationBehaviorTimestamps) { - self.currentUpdatedPeerNotificationSettings[peerId] = updated + self.currentUpdatedPeerNotificationSettings[peerId] = (previous, updated) } } } fileprivate func updatePendingPeerNotificationSettings(peerId: PeerId, settings: PeerNotificationSettings?) { + let previous: PeerNotificationSettings? + if let (value, _) = self.currentUpdatedPeerNotificationSettings[peerId] { + previous = value + } else { + previous = self.peerNotificationSettingsTable.getEffective(peerId) + } if let updated = self.peerNotificationSettingsTable.setPending(id: peerId, settings: settings, updatedSettings: &self.currentUpdatedPendingPeerNotificationSettings) { - self.currentUpdatedPeerNotificationSettings[peerId] = updated + self.currentUpdatedPeerNotificationSettings[peerId] = (previous, updated) } } fileprivate func resetAllPeerNotificationSettings(_ notificationSettings: PeerNotificationSettings) { - for peerId in self.peerNotificationSettingsTable.resetAll(to: notificationSettings, updatedSettings: &self.currentUpdatedPendingPeerNotificationSettings, updatedTimestamps: &self.currentUpdatedPeerNotificationBehaviorTimestamps) { - self.currentUpdatedPeerNotificationSettings[peerId] = notificationSettings + for (peerId, previous) in self.peerNotificationSettingsTable.resetAll(to: notificationSettings, updatedSettings: &self.currentUpdatedPendingPeerNotificationSettings, updatedTimestamps: &self.currentUpdatedPeerNotificationBehaviorTimestamps) { + self.currentUpdatedPeerNotificationSettings[peerId] = (previous, notificationSettings) } } @@ -2040,6 +2108,10 @@ public final class Postbox { return self.itemCacheTable.retrieve(id: id, metaTable: self.itemCacheMetaTable) } + func clearItemCacheCollection(collectionId: ItemCacheCollectionId) { + return self.itemCacheTable.removeAll(collectionId: collectionId) + } + fileprivate func removeItemCacheEntry(id: ItemCacheEntryId) { self.itemCacheTable.remove(id: id, metaTable: self.itemCacheMetaTable) } @@ -2220,7 +2292,7 @@ public final class Postbox { } } - private func peerIdsForLocation(_ chatLocation: ChatLocation, tagMask: MessageTags?) -> MessageHistoryViewPeerIds { + func peerIdsForLocation(_ chatLocation: ChatLocation, tagMask: MessageTags?) -> MessageHistoryViewPeerIds { var peerIds: MessageHistoryViewPeerIds switch chatLocation { case let .peer(peerId): @@ -2232,7 +2304,7 @@ public final class Postbox { return peerIds } - public func aroundMessageOfInterestHistoryViewForChatLocation(_ chatLocation: ChatLocation, count: Int, topTaggedMessageIdNamespaces: Set, tagMask: MessageTags?, namespaces: MessageIdNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData]) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + public func aroundMessageOfInterestHistoryViewForChatLocation(_ chatLocation: ChatLocation, count: Int, clipHoles: Bool = true, topTaggedMessageIdNamespaces: Set, tagMask: MessageTags?, namespaces: MessageIdNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData]) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { return self.transactionSignal(userInteractive: true, { subscriber, transaction in let peerIds = self.peerIdsForLocation(chatLocation, tagMask: tagMask) @@ -2278,26 +2350,26 @@ public final class Postbox { } } } - return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, count: count, anchor: anchor, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tagMask: tagMask, namespaces: namespaces, orderStatistics: orderStatistics, additionalData: additionalData) + return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, count: count, clipHoles: clipHoles, anchor: anchor, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tagMask: tagMask, namespaces: namespaces, orderStatistics: orderStatistics, additionalData: additionalData) }) } - public func aroundIdMessageHistoryViewForLocation(_ chatLocation: ChatLocation, count: Int, messageId: MessageId, topTaggedMessageIdNamespaces: Set, tagMask: MessageTags?, namespaces: MessageIdNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + public func aroundIdMessageHistoryViewForLocation(_ chatLocation: ChatLocation, count: Int, clipHoles: Bool = true, messageId: MessageId, topTaggedMessageIdNamespaces: Set, tagMask: MessageTags?, namespaces: MessageIdNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { return self.transactionSignal { subscriber, transaction in let peerIds = self.peerIdsForLocation(chatLocation, tagMask: tagMask) - return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, count: count, anchor: .message(messageId), fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tagMask: tagMask, namespaces: namespaces, orderStatistics: orderStatistics, additionalData: additionalData) + return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, count: count, clipHoles: clipHoles, anchor: .message(messageId), fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tagMask: tagMask, namespaces: namespaces, orderStatistics: orderStatistics, additionalData: additionalData) } } - public func aroundMessageHistoryViewForLocation(_ chatLocation: ChatLocation, anchor: HistoryViewInputAnchor, count: Int, fixedCombinedReadStates: MessageHistoryViewReadState?, topTaggedMessageIdNamespaces: Set, tagMask: MessageTags?, namespaces: MessageIdNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + public func aroundMessageHistoryViewForLocation(_ chatLocation: ChatLocation, anchor: HistoryViewInputAnchor, count: Int, clipHoles: Bool = true, fixedCombinedReadStates: MessageHistoryViewReadState?, topTaggedMessageIdNamespaces: Set, tagMask: MessageTags?, namespaces: MessageIdNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { return self.transactionSignal { subscriber, transaction in let peerIds = self.peerIdsForLocation(chatLocation, tagMask: tagMask) - return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, count: count, anchor: anchor, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tagMask: tagMask, namespaces: namespaces, orderStatistics: orderStatistics, additionalData: additionalData) + return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, count: count, clipHoles: clipHoles, anchor: anchor, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tagMask: tagMask, namespaces: namespaces, orderStatistics: orderStatistics, additionalData: additionalData) } } - private func syncAroundMessageHistoryViewForPeerId(subscriber: Subscriber<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError>, peerIds: MessageHistoryViewPeerIds, count: Int, anchor: HistoryViewInputAnchor, fixedCombinedReadStates: MessageHistoryViewReadState?, topTaggedMessageIdNamespaces: Set, tagMask: MessageTags?, namespaces: MessageIdNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData]) -> Disposable { + private func syncAroundMessageHistoryViewForPeerId(subscriber: Subscriber<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError>, peerIds: MessageHistoryViewPeerIds, count: Int, clipHoles: Bool, anchor: HistoryViewInputAnchor, fixedCombinedReadStates: MessageHistoryViewReadState?, topTaggedMessageIdNamespaces: Set, tagMask: MessageTags?, namespaces: MessageIdNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData]) -> Disposable { var topTaggedMessages: [MessageId.Namespace: MessageHistoryTopTaggedMessage?] = [:] var mainPeerId: PeerId? switch peerIds { @@ -2386,7 +2458,7 @@ public final class Postbox { readStates = transientReadStates } - let mutableView = MutableMessageHistoryView(postbox: self, orderStatistics: orderStatistics, peerIds: peerIds, anchor: anchor, combinedReadStates: readStates, transientReadStates: transientReadStates, tag: tagMask, namespaces: namespaces, count: count, topTaggedMessages: topTaggedMessages, additionalDatas: additionalDataEntries, getMessageCountInRange: { lowerBound, upperBound in + let mutableView = MutableMessageHistoryView(postbox: self, orderStatistics: orderStatistics, clipHoles: clipHoles, peerIds: peerIds, anchor: anchor, combinedReadStates: readStates, transientReadStates: transientReadStates, tag: tagMask, namespaces: namespaces, count: count, topTaggedMessages: topTaggedMessages, additionalDatas: additionalDataEntries, getMessageCountInRange: { lowerBound, upperBound in if let tagMask = tagMask { return Int32(self.messageHistoryTable.getMessageCountInRange(peerId: lowerBound.id.peerId, namespace: lowerBound.id.namespace, tag: tagMask, lowerBound: lowerBound, upperBound: upperBound)) } else { @@ -2474,15 +2546,13 @@ public final class Postbox { |> switchToLatest } - public func tailChatListView(groupId: PeerGroupId, count: Int, summaryComponents: ChatListEntrySummaryComponents) -> Signal<(ChatListView, ViewUpdateType), NoError> { - return self.aroundChatListView(groupId: groupId, index: ChatListIndex.absoluteUpperBound, count: count, summaryComponents: summaryComponents, userInteractive: true) + public func tailChatListView(groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)? = nil, count: Int, summaryComponents: ChatListEntrySummaryComponents) -> Signal<(ChatListView, ViewUpdateType), NoError> { + return self.aroundChatListView(groupId: groupId, filterPredicate: filterPredicate, index: ChatListIndex.absoluteUpperBound, count: count, summaryComponents: summaryComponents, userInteractive: true) } - public func aroundChatListView(groupId: PeerGroupId, index: ChatListIndex, count: Int, summaryComponents: ChatListEntrySummaryComponents, userInteractive: Bool = false) -> Signal<(ChatListView, ViewUpdateType), NoError> { + public func aroundChatListView(groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)? = nil, index: ChatListIndex, count: Int, summaryComponents: ChatListEntrySummaryComponents, userInteractive: Bool = false) -> Signal<(ChatListView, ViewUpdateType), NoError> { return self.transactionSignal(userInteractive: userInteractive, { subscriber, transaction in - let (entries, earlier, later) = self.fetchAroundChatEntries(groupId: groupId, index: index, count: count) - - let mutableView = MutableChatListView(postbox: self, groupId: groupId, earlier: earlier, entries: entries, later: later, count: count, summaryComponents: summaryComponents) + let mutableView = MutableChatListView(postbox: self, groupId: groupId, filterPredicate: filterPredicate, aroundIndex: index, count: count, summaryComponents: summaryComponents) mutableView.render(postbox: self, renderMessage: self.renderIntermediateMessage, getPeer: { id in return self.peerTable.get(id) }, getPeerNotificationSettings: { self.peerNotificationSettingsTable.getEffective($0) }, getPeerPresence: { self.peerPresenceTable.get($0) }) @@ -3158,4 +3228,38 @@ public final class Postbox { } } } + + fileprivate func reindexUnreadCounters() { + self.groupMessageStatsTable.removeAll() + let startTime = CFAbsoluteTimeGetCurrent() + let (rootState, summaries) = self.chatListIndexTable.debugReindexUnreadCounts(postbox: self) + + self.messageHistoryMetadataTable.setChatListTotalUnreadState(rootState) + self.currentUpdatedTotalUnreadState = rootState + for (groupId, summary) in summaries { + self.groupMessageStatsTable.set(groupId: groupId, summary: summary) + self.currentUpdatedGroupTotalUnreadSummaries[groupId] = summary + } + } + + public func failedMessageIdsView(peerId: PeerId) -> Signal { + return self.transactionSignal { subscriber, transaction in + let view = MutableFailedMessageIdsView(peerId: peerId, ids: self.failedMessageIds(for: peerId)) + let (index, signal) = self.viewTracker.addFailedMessageIdsView(view) + subscriber.putNext(view.immutableView()) + let disposable = signal.start(next: { next in + subscriber.putNext(next) + }) + + return ActionDisposable { [weak self] in + disposable.dispose() + if let strongSelf = self { + strongSelf.queue.async { + strongSelf.viewTracker.removeFailedMessageIdsView(index) + } + } + } + } + } + } diff --git a/submodules/Postbox/Sources/PostboxTransaction.swift b/submodules/Postbox/Sources/PostboxTransaction.swift index 52401de1aa..207ed2a45a 100644 --- a/submodules/Postbox/Sources/PostboxTransaction.swift +++ b/submodules/Postbox/Sources/PostboxTransaction.swift @@ -7,7 +7,7 @@ final class PostboxTransaction { let chatListOperations: [PeerGroupId: [ChatListOperation]] let currentUpdatedChatListInclusions: [PeerId: PeerChatListInclusion] let currentUpdatedPeers: [PeerId: Peer] - let currentUpdatedPeerNotificationSettings: [PeerId: PeerNotificationSettings] + let currentUpdatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)] let currentUpdatedPeerNotificationBehaviorTimestamps: [PeerId: PeerNotificationSettingsBehaviorTimestamp] let currentUpdatedCachedPeerData: [PeerId: CachedPeerData] let currentUpdatedPeerPresences: [PeerId: PeerPresence] @@ -40,6 +40,10 @@ final class PostboxTransaction { let replacedAdditionalChatListItems: [PeerId]? let updatedNoticeEntryKeys: Set let updatedCacheEntryKeys: Set + let updatedFailedMessagePeerIds: Set + let updatedFailedMessageIds: Set + + var isEmpty: Bool { if currentUpdatedState != nil { @@ -159,10 +163,16 @@ final class PostboxTransaction { if !updatedCacheEntryKeys.isEmpty { return false } + if !updatedFailedMessagePeerIds.isEmpty { + return false + } + if !updatedFailedMessageIds.isEmpty { + return false + } return true } - init(currentUpdatedState: PostboxCoding?, currentPeerHoleOperations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:], currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], chatListOperations: [PeerGroupId: [ChatListOperation]], currentUpdatedChatListInclusions: [PeerId: PeerChatListInclusion], currentUpdatedPeers: [PeerId: Peer], currentUpdatedPeerNotificationSettings: [PeerId: PeerNotificationSettings], currentUpdatedPeerNotificationBehaviorTimestamps: [PeerId: PeerNotificationSettingsBehaviorTimestamp], currentUpdatedCachedPeerData: [PeerId: CachedPeerData], currentUpdatedPeerPresences: [PeerId: PeerPresence], currentUpdatedPeerChatListEmbeddedStates: [PeerId: PeerChatListEmbeddedInterfaceState?], currentUpdatedTotalUnreadState: ChatListTotalUnreadState?, currentUpdatedTotalUnreadSummaries: [PeerGroupId: PeerGroupUnreadCountersCombinedSummary], alteredInitialPeerCombinedReadStates: [PeerId: CombinedPeerReadState], currentPeerMergedOperationLogOperations: [PeerMergedOperationLogOperation], currentTimestampBasedMessageAttributesOperations: [TimestampBasedMessageAttributesOperation], unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], updatedSynchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?], currentUpdatedGroupSummarySynchronizeOperations: [PeerGroupAndNamespace: Bool], currentPreferencesOperations: [PreferencesOperation], currentOrderedItemListOperations: [Int32: [OrderedItemListOperation]], currentItemCollectionItemsOperations: [ItemCollectionId: [ItemCollectionItemsOperation]], currentItemCollectionInfosOperations: [ItemCollectionInfosOperation], currentUpdatedPeerChatStates: Set, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set, replacedAdditionalChatListItems: [PeerId]?, updatedNoticeEntryKeys: Set, updatedCacheEntryKeys: Set, currentUpdatedMasterClientId: Int64?) { + init(currentUpdatedState: PostboxCoding?, currentPeerHoleOperations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:], currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], chatListOperations: [PeerGroupId: [ChatListOperation]], currentUpdatedChatListInclusions: [PeerId: PeerChatListInclusion], currentUpdatedPeers: [PeerId: Peer], currentUpdatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)], currentUpdatedPeerNotificationBehaviorTimestamps: [PeerId: PeerNotificationSettingsBehaviorTimestamp], currentUpdatedCachedPeerData: [PeerId: CachedPeerData], currentUpdatedPeerPresences: [PeerId: PeerPresence], currentUpdatedPeerChatListEmbeddedStates: [PeerId: PeerChatListEmbeddedInterfaceState?], currentUpdatedTotalUnreadState: ChatListTotalUnreadState?, currentUpdatedTotalUnreadSummaries: [PeerGroupId: PeerGroupUnreadCountersCombinedSummary], alteredInitialPeerCombinedReadStates: [PeerId: CombinedPeerReadState], currentPeerMergedOperationLogOperations: [PeerMergedOperationLogOperation], currentTimestampBasedMessageAttributesOperations: [TimestampBasedMessageAttributesOperation], unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], updatedSynchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?], currentUpdatedGroupSummarySynchronizeOperations: [PeerGroupAndNamespace: Bool], currentPreferencesOperations: [PreferencesOperation], currentOrderedItemListOperations: [Int32: [OrderedItemListOperation]], currentItemCollectionItemsOperations: [ItemCollectionId: [ItemCollectionItemsOperation]], currentItemCollectionInfosOperations: [ItemCollectionInfosOperation], currentUpdatedPeerChatStates: Set, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set, replacedAdditionalChatListItems: [PeerId]?, updatedNoticeEntryKeys: Set, updatedCacheEntryKeys: Set, currentUpdatedMasterClientId: Int64?, updatedFailedMessagePeerIds: Set, updatedFailedMessageIds: Set) { self.currentUpdatedState = currentUpdatedState self.currentPeerHoleOperations = currentPeerHoleOperations self.currentOperationsByPeerId = currentOperationsByPeerId @@ -201,5 +211,7 @@ final class PostboxTransaction { self.replacedAdditionalChatListItems = replacedAdditionalChatListItems self.updatedNoticeEntryKeys = updatedNoticeEntryKeys self.updatedCacheEntryKeys = updatedCacheEntryKeys + self.updatedFailedMessagePeerIds = updatedFailedMessagePeerIds + self.updatedFailedMessageIds = updatedFailedMessageIds } } diff --git a/submodules/Postbox/Sources/SqliteValueBox.swift b/submodules/Postbox/Sources/SqliteValueBox.swift index 3c9777f952..70b3e5661d 100644 --- a/submodules/Postbox/Sources/SqliteValueBox.swift +++ b/submodules/Postbox/Sources/SqliteValueBox.swift @@ -1498,6 +1498,38 @@ public final class SqliteValueBox: ValueBox { withExtendedLifetime(end, {}) } + public func filteredRange(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, values: (ValueBoxKey, ReadBuffer) -> ValueBoxFilterResult, limit: Int) { + var currentStart = start + var acceptedCount = 0 + while acceptedCount < limit { + var hadStop = false + var lastKey: ValueBoxKey? + self.range(table, start: currentStart, end: end, values: { key, value in + lastKey = key + let result = values(key, value) + switch result { + case .accept: + acceptedCount += 1 + return true + case .skip: + return true + case .stop: + hadStop = true + return false + } + return true + }, limit: limit) + if let lastKey = lastKey { + currentStart = lastKey + } else { + break + } + if hadStop { + break + } + } + } + public func range(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, keys: (ValueBoxKey) -> Bool, limit: Int) { precondition(self.queue.isCurrent()) if let _ = self.tables[table.id] { @@ -1764,8 +1796,14 @@ public final class SqliteValueBox: ValueBox { statement.reset() } - public func fullTextRemove(_ table: ValueBoxFullTextTable, itemId: String) { + public func fullTextRemove(_ table: ValueBoxFullTextTable, itemId: String, secure: Bool) { if let _ = self.fullTextTables[table.id] { + if secure != self.secureDeleteEnabled { + self.secureDeleteEnabled = secure + let result = database.execute("PRAGMA secure_delete=\(secure ? 1 : 0)") + precondition(result) + } + guard let itemIdData = itemId.data(using: .utf8) else { return } diff --git a/submodules/Postbox/Sources/ValueBox.swift b/submodules/Postbox/Sources/ValueBox.swift index 5e1d8eefab..08396740e6 100644 --- a/submodules/Postbox/Sources/ValueBox.swift +++ b/submodules/Postbox/Sources/ValueBox.swift @@ -57,6 +57,12 @@ public struct ValueBoxEncryptionParameters { } } +public enum ValueBoxFilterResult { + case accept + case skip + case stop +} + public protocol ValueBox { func begin() func commit() @@ -66,6 +72,7 @@ public protocol ValueBox { func endStats() func range(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, values: (ValueBoxKey, ReadBuffer) -> Bool, limit: Int) + func filteredRange(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, values: (ValueBoxKey, ReadBuffer) -> ValueBoxFilterResult, limit: Int) func range(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey, keys: (ValueBoxKey) -> Bool, limit: Int) func scan(_ table: ValueBoxTable, values: (ValueBoxKey, ReadBuffer) -> Bool) func scan(_ table: ValueBoxTable, keys: (ValueBoxKey) -> Bool) @@ -82,7 +89,7 @@ public protocol ValueBox { func removeRange(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey) func fullTextSet(_ table: ValueBoxFullTextTable, collectionId: String, itemId: String, contents: String, tags: String) func fullTextMatch(_ table: ValueBoxFullTextTable, collectionId: String?, query: String, tags: String?, values: (String, String) -> Bool) - func fullTextRemove(_ table: ValueBoxFullTextTable, itemId: String) + func fullTextRemove(_ table: ValueBoxFullTextTable, itemId: String, secure: Bool) func removeAllFromTable(_ table: ValueBoxTable) func removeTable(_ table: ValueBoxTable) func renameTable(_ table: ValueBoxTable, to toTable: ValueBoxTable) diff --git a/submodules/Postbox/Sources/ValueBoxKey.swift b/submodules/Postbox/Sources/ValueBoxKey.swift index b309f6589b..9c2d75487a 100644 --- a/submodules/Postbox/Sources/ValueBoxKey.swift +++ b/submodules/Postbox/Sources/ValueBoxKey.swift @@ -38,6 +38,13 @@ public struct ValueBoxKey: Equatable, Hashable, CustomStringConvertible, Compara memcpy(self.memory, buffer.memory, buffer.length) } + public func setData(_ offset: Int, value: Data) { + let valueLength = value.count + value.withUnsafeBytes { (bytes: UnsafePointer) -> Void in + memcpy(self.memory + offset, bytes, valueLength) + } + } + public func setInt32(_ offset: Int, value: Int32) { var bigEndianValue = Int32(bigEndian: value) memcpy(self.memory + offset, &bigEndianValue, 4) diff --git a/submodules/Postbox/Sources/ViewTracker.swift b/submodules/Postbox/Sources/ViewTracker.swift index bd232da91f..d85a3f8a50 100644 --- a/submodules/Postbox/Sources/ViewTracker.swift +++ b/submodules/Postbox/Sources/ViewTracker.swift @@ -55,6 +55,10 @@ final class ViewTracker { private var multiplePeersViews = Bag<(MutableMultiplePeersView, ValuePipe)>() private var itemCollectionsViews = Bag<(MutableItemCollectionsView, ValuePipe)>() + + private var failedMessageIdsViews = Bag<(MutableFailedMessageIdsView, ValuePipe)>() + + init(queue: Queue, renderMessage: @escaping (IntermediateMessage) -> Message, getPeer: @escaping (PeerId) -> Peer?, getPeerNotificationSettings: @escaping (PeerId) -> PeerNotificationSettings?, getCachedPeerData: @escaping (PeerId) -> CachedPeerData?, getPeerPresence: @escaping (PeerId) -> PeerPresence?, getTotalUnreadState: @escaping () -> ChatListTotalUnreadState, getPeerReadState: @escaping (PeerId) -> CombinedPeerReadState?, operationLogGetOperations: @escaping (PeerOperationLogTag, Int32, Int) -> [PeerMergedOperationLogEntry], operationLogGetTailIndex: @escaping (PeerOperationLogTag) -> Int32?, getTimestampBasedMessageAttributesHead: @escaping (UInt16) -> TimestampBasedMessageAttributesEntry?, getPreferencesEntry: @escaping (ValueBoxKey) -> PreferencesEntry?, unsentMessageIds: [MessageId], synchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation]) { self.queue = queue self.renderMessage = renderMessage @@ -349,7 +353,8 @@ final class ViewTracker { } } - if !transaction.chatListOperations.isEmpty || !transaction.currentUpdatedPeerNotificationSettings.isEmpty || !transaction.currentUpdatedPeers.isEmpty || !transaction.currentInvalidateMessageTagSummaries.isEmpty || !transaction.currentUpdatedMessageTagSummaries.isEmpty || !transaction.currentOperationsByPeerId.isEmpty || transaction.replacedAdditionalChatListItems != nil || !transaction.currentUpdatedPeerPresences.isEmpty { + if !transaction.chatListOperations.isEmpty || !transaction.currentUpdatedPeerNotificationSettings.isEmpty || !transaction.currentUpdatedPeers.isEmpty || !transaction.currentInvalidateMessageTagSummaries.isEmpty || !transaction.currentUpdatedMessageTagSummaries.isEmpty || !transaction.currentOperationsByPeerId.isEmpty || transaction.replacedAdditionalChatListItems != nil || !transaction.currentUpdatedPeerPresences.isEmpty || + !transaction.updatedFailedMessagePeerIds.isEmpty { for (mutableView, pipe) in self.chatListViews.copyItems() { let context = MutableChatListViewReplayContext() if mutableView.replay(postbox: postbox, operations: transaction.chatListOperations, updatedPeerNotificationSettings: transaction.currentUpdatedPeerNotificationSettings, updatedPeers: transaction.currentUpdatedPeers, updatedPeerPresences: transaction.currentUpdatedPeerPresences, transaction: transaction, context: context) { @@ -437,6 +442,12 @@ final class ViewTracker { pipe.putNext(mutableView.immutableView()) } } + + for (view, pipe) in self.failedMessageIdsViews.copyItems() { + if view.replay(postbox: postbox, transaction: transaction) { + pipe.putNext(view.immutableView()) + } + } } private func updateTrackedChatListHoles() { @@ -458,14 +469,14 @@ final class ViewTracker { private func updateTrackedHoles() { var firstHolesAndTags = Set() for (view, _) in self.messageHistoryViews.copyItems() { - if let (hole, direction) = view.firstHole() { + if let (hole, direction, count) = view.firstHole() { let space: MessageHistoryHoleSpace if let tag = view.tag { space = .tag(tag) } else { space = .everywhere } - firstHolesAndTags.insert(MessageHistoryHolesViewEntry(hole: hole, direction: direction, space: space)) + firstHolesAndTags.insert(MessageHistoryHolesViewEntry(hole: hole, direction: direction, space: space, count: count)) } } @@ -583,4 +594,17 @@ final class ViewTracker { return disposable } } + + func addFailedMessageIdsView(_ view: MutableFailedMessageIdsView) -> (Bag<(MutableFailedMessageIdsView, ValuePipe)>.Index, Signal) { + let record = (view, ValuePipe()) + let index = self.failedMessageIdsViews.add(record) + + return (index, record.1.signal()) + } + + func removeFailedMessageIdsView(_ index: Bag<(MutableFailedMessageIdsView, ValuePipe)>.Index) { + self.failedMessageIdsViews.remove(index) + } + + } diff --git a/submodules/Postbox/Sources/Views.swift b/submodules/Postbox/Sources/Views.swift index 86e870ac6b..c593c746e7 100644 --- a/submodules/Postbox/Sources/Views.swift +++ b/submodules/Postbox/Sources/Views.swift @@ -27,279 +27,301 @@ public enum PostboxViewKey: Hashable { case peerNotificationSettingsBehaviorTimestampView case peerChatInclusion(PeerId) case basicPeer(PeerId) + case allChatListHoles(PeerGroupId) + case historyTagInfo(peerId: PeerId, tag: MessageTags) public var hashValue: Int { switch self { - case .itemCollectionInfos: - return 0 - case .itemCollectionIds: - return 1 - case let .peerChatState(peerId): - return peerId.hashValue - case let .itemCollectionInfo(id): - return id.hashValue - case let .orderedItemList(id): - return id.hashValue - case .preferences: - return 3 - case .globalMessageTags: - return 4 - case let .peer(peerId, _): - return peerId.hashValue - case let .pendingMessageActions(type): - return type.hashValue - case let .invalidatedMessageHistoryTagSummaries(tagMask, namespace): - return tagMask.rawValue.hashValue ^ namespace.hashValue - case let .pendingMessageActionsSummary(type, peerId, namespace): - return type.hashValue ^ peerId.hashValue ^ namespace.hashValue - case let .historyTagSummaryView(tag, peerId, namespace): - return tag.rawValue.hashValue ^ peerId.hashValue ^ namespace.hashValue - case let .cachedPeerData(peerId): - return peerId.hashValue - case .unreadCounts: - return 5 - case .peerNotificationSettings: - return 6 - case .pendingPeerNotificationSettings: - return 7 - case let .messageOfInterestHole(location, namespace, count): - return 8 &+ 31 &* location.hashValue &+ 31 &* namespace.hashValue &+ 31 &* count.hashValue - case let .localMessageTag(tag): - return tag.hashValue - case .messages: - return 10 - case .additionalChatListItems: - return 11 - case let .cachedItem(id): - return id.hashValue - case .peerPresences: - return 13 - case .synchronizeGroupMessageStats: - return 14 - case .peerNotificationSettingsBehaviorTimestampView: - return 15 - case let .peerChatInclusion(peerId): - return peerId.hashValue - case let .basicPeer(peerId): - return peerId.hashValue + case .itemCollectionInfos: + return 0 + case .itemCollectionIds: + return 1 + case let .peerChatState(peerId): + return peerId.hashValue + case let .itemCollectionInfo(id): + return id.hashValue + case let .orderedItemList(id): + return id.hashValue + case .preferences: + return 3 + case .globalMessageTags: + return 4 + case let .peer(peerId, _): + return peerId.hashValue + case let .pendingMessageActions(type): + return type.hashValue + case let .invalidatedMessageHistoryTagSummaries(tagMask, namespace): + return tagMask.rawValue.hashValue ^ namespace.hashValue + case let .pendingMessageActionsSummary(type, peerId, namespace): + return type.hashValue ^ peerId.hashValue ^ namespace.hashValue + case let .historyTagSummaryView(tag, peerId, namespace): + return tag.rawValue.hashValue ^ peerId.hashValue ^ namespace.hashValue + case let .cachedPeerData(peerId): + return peerId.hashValue + case .unreadCounts: + return 5 + case .peerNotificationSettings: + return 6 + case .pendingPeerNotificationSettings: + return 7 + case let .messageOfInterestHole(location, namespace, count): + return 8 &+ 31 &* location.hashValue &+ 31 &* namespace.hashValue &+ 31 &* count.hashValue + case let .localMessageTag(tag): + return tag.hashValue + case .messages: + return 10 + case .additionalChatListItems: + return 11 + case let .cachedItem(id): + return id.hashValue + case .peerPresences: + return 13 + case .synchronizeGroupMessageStats: + return 14 + case .peerNotificationSettingsBehaviorTimestampView: + return 15 + case let .peerChatInclusion(peerId): + return peerId.hashValue + case let .basicPeer(peerId): + return peerId.hashValue + case let .allChatListHoles(groupId): + return groupId.hashValue + case let .historyTagInfo(peerId, tag): + return peerId.hashValue ^ tag.hashValue } } public static func ==(lhs: PostboxViewKey, rhs: PostboxViewKey) -> Bool { switch lhs { - case let .itemCollectionInfos(lhsNamespaces): - if case let .itemCollectionInfos(rhsNamespaces) = rhs, lhsNamespaces == rhsNamespaces { - return true - } else { - return false - } - case let .itemCollectionIds(lhsNamespaces): - if case let .itemCollectionIds(rhsNamespaces) = rhs, lhsNamespaces == rhsNamespaces { - return true - } else { - return false - } - case let .itemCollectionInfo(id): - if case .itemCollectionInfo(id) = rhs { - return true - } else { - return false - } - case let .peerChatState(peerId): - if case .peerChatState(peerId) = rhs { - return true - } else { - return false - } - case let .orderedItemList(id): - if case .orderedItemList(id) = rhs { - return true - } else { - return false - } - case let .preferences(lhsKeys): - if case let .preferences(rhsKeys) = rhs, lhsKeys == rhsKeys { - return true - } else { - return false - } - case let .globalMessageTags(globalTag, position, count, _): - if case .globalMessageTags(globalTag, position, count, _) = rhs { - return true - } else { - return false - } - case let .peer(peerId, components): - if case .peer(peerId, components) = rhs { - return true - } else { - return false - } - case let .pendingMessageActions(type): - if case .pendingMessageActions(type) = rhs { - return true - } else { - return false - } - case .invalidatedMessageHistoryTagSummaries: - if case .invalidatedMessageHistoryTagSummaries = rhs { - return true - } else { - return false - } - case let .pendingMessageActionsSummary(type, peerId, namespace): - if case .pendingMessageActionsSummary(type, peerId, namespace) = rhs { - return true - } else { - return false - } - case let .historyTagSummaryView(tag, peerId, namespace): - if case .historyTagSummaryView(tag, peerId, namespace) = rhs { - return true - } else { - return false - } - case let .cachedPeerData(peerId): - if case .cachedPeerData(peerId) = rhs { - return true - } else { - return false - } - case let .unreadCounts(lhsItems): - if case let .unreadCounts(rhsItems) = rhs, lhsItems == rhsItems { - return true - } else { - return false - } - case let .peerNotificationSettings(peerIds): - if case .peerNotificationSettings(peerIds) = rhs { - return true - } else { - return false - } - case .pendingPeerNotificationSettings: - if case .pendingPeerNotificationSettings = rhs { - return true - } else { - return false - } - case let .messageOfInterestHole(peerId, namespace, count): - if case .messageOfInterestHole(peerId, namespace, count) = rhs { - return true - } else { - return false - } - case let .localMessageTag(tag): - if case .localMessageTag(tag) = rhs { - return true - } else { - return false - } - case let .messages(ids): - if case .messages(ids) = rhs { - return true - } else { - return false - } - case .additionalChatListItems: - if case .additionalChatListItems = rhs { - return true - } else { - return false - } - case let .cachedItem(id): - if case .cachedItem(id) = rhs { - return true - } else { - return false - } - case let .peerPresences(ids): - if case .peerPresences(ids) = rhs { - return true - } else { - return false - } - case .synchronizeGroupMessageStats: - if case .synchronizeGroupMessageStats = rhs { - return true - } else { - return false - } - case .peerNotificationSettingsBehaviorTimestampView: - if case .peerNotificationSettingsBehaviorTimestampView = rhs { - return true - } else { - return false - } - case let .peerChatInclusion(id): - if case .peerChatInclusion(id) = rhs { - return true - } else { - return false - } - case let .basicPeer(id): - if case .basicPeer(id) = rhs { - return true - } else { - return false - } + case let .itemCollectionInfos(lhsNamespaces): + if case let .itemCollectionInfos(rhsNamespaces) = rhs, lhsNamespaces == rhsNamespaces { + return true + } else { + return false + } + case let .itemCollectionIds(lhsNamespaces): + if case let .itemCollectionIds(rhsNamespaces) = rhs, lhsNamespaces == rhsNamespaces { + return true + } else { + return false + } + case let .itemCollectionInfo(id): + if case .itemCollectionInfo(id) = rhs { + return true + } else { + return false + } + case let .peerChatState(peerId): + if case .peerChatState(peerId) = rhs { + return true + } else { + return false + } + case let .orderedItemList(id): + if case .orderedItemList(id) = rhs { + return true + } else { + return false + } + case let .preferences(lhsKeys): + if case let .preferences(rhsKeys) = rhs, lhsKeys == rhsKeys { + return true + } else { + return false + } + case let .globalMessageTags(globalTag, position, count, _): + if case .globalMessageTags(globalTag, position, count, _) = rhs { + return true + } else { + return false + } + case let .peer(peerId, components): + if case .peer(peerId, components) = rhs { + return true + } else { + return false + } + case let .pendingMessageActions(type): + if case .pendingMessageActions(type) = rhs { + return true + } else { + return false + } + case .invalidatedMessageHistoryTagSummaries: + if case .invalidatedMessageHistoryTagSummaries = rhs { + return true + } else { + return false + } + case let .pendingMessageActionsSummary(type, peerId, namespace): + if case .pendingMessageActionsSummary(type, peerId, namespace) = rhs { + return true + } else { + return false + } + case let .historyTagSummaryView(tag, peerId, namespace): + if case .historyTagSummaryView(tag, peerId, namespace) = rhs { + return true + } else { + return false + } + case let .cachedPeerData(peerId): + if case .cachedPeerData(peerId) = rhs { + return true + } else { + return false + } + case let .unreadCounts(lhsItems): + if case let .unreadCounts(rhsItems) = rhs, lhsItems == rhsItems { + return true + } else { + return false + } + case let .peerNotificationSettings(peerIds): + if case .peerNotificationSettings(peerIds) = rhs { + return true + } else { + return false + } + case .pendingPeerNotificationSettings: + if case .pendingPeerNotificationSettings = rhs { + return true + } else { + return false + } + case let .messageOfInterestHole(peerId, namespace, count): + if case .messageOfInterestHole(peerId, namespace, count) = rhs { + return true + } else { + return false + } + case let .localMessageTag(tag): + if case .localMessageTag(tag) = rhs { + return true + } else { + return false + } + case let .messages(ids): + if case .messages(ids) = rhs { + return true + } else { + return false + } + case .additionalChatListItems: + if case .additionalChatListItems = rhs { + return true + } else { + return false + } + case let .cachedItem(id): + if case .cachedItem(id) = rhs { + return true + } else { + return false + } + case let .peerPresences(ids): + if case .peerPresences(ids) = rhs { + return true + } else { + return false + } + case .synchronizeGroupMessageStats: + if case .synchronizeGroupMessageStats = rhs { + return true + } else { + return false + } + case .peerNotificationSettingsBehaviorTimestampView: + if case .peerNotificationSettingsBehaviorTimestampView = rhs { + return true + } else { + return false + } + case let .peerChatInclusion(id): + if case .peerChatInclusion(id) = rhs { + return true + } else { + return false + } + case let .basicPeer(id): + if case .basicPeer(id) = rhs { + return true + } else { + return false + } + case let .allChatListHoles(groupId): + if case .allChatListHoles(groupId) = rhs { + return true + } else { + return false + } + case let .historyTagInfo(peerId, tag): + if case .historyTagInfo(peerId, tag) = rhs { + return true + } else { + return false + } } } } func postboxViewForKey(postbox: Postbox, key: PostboxViewKey) -> MutablePostboxView { switch key { - case let .itemCollectionInfos(namespaces): - return MutableItemCollectionInfosView(postbox: postbox, namespaces: namespaces) - case let .itemCollectionIds(namespaces): - return MutableItemCollectionIdsView(postbox: postbox, namespaces: namespaces) - case let .itemCollectionInfo(id): - return MutableItemCollectionInfoView(postbox: postbox, id: id) - case let .peerChatState(peerId): - return MutablePeerChatStateView(postbox: postbox, peerId: peerId) - case let .orderedItemList(id): - return MutableOrderedItemListView(postbox: postbox, collectionId: id) - case let .preferences(keys): - return MutablePreferencesView(postbox: postbox, keys: keys) - case let .globalMessageTags(globalTag, position, count, groupingPredicate): - return MutableGlobalMessageTagsView(postbox: postbox, globalTag: globalTag, position: position, count: count, groupingPredicate: groupingPredicate) - case let .peer(peerId, components): - return MutablePeerView(postbox: postbox, peerId: peerId, components: components) - case let .pendingMessageActions(type): - return MutablePendingMessageActionsView(postbox: postbox, type: type) - case let .invalidatedMessageHistoryTagSummaries(tagMask, namespace): - return MutableInvalidatedMessageHistoryTagSummariesView(postbox: postbox, tagMask: tagMask, namespace: namespace) - case let .pendingMessageActionsSummary(type, peerId, namespace): - return MutablePendingMessageActionsSummaryView(postbox: postbox, type: type, peerId: peerId, namespace: namespace) - case let .historyTagSummaryView(tag, peerId, namespace): - return MutableMessageHistoryTagSummaryView(postbox: postbox, tag: tag, peerId: peerId, namespace: namespace) - case let .cachedPeerData(peerId): - return MutableCachedPeerDataView(postbox: postbox, peerId: peerId) - case let .unreadCounts(items): - return MutableUnreadMessageCountsView(postbox: postbox, items: items) - case let .peerNotificationSettings(peerIds): - return MutablePeerNotificationSettingsView(postbox: postbox, peerIds: peerIds) - case .pendingPeerNotificationSettings: - return MutablePendingPeerNotificationSettingsView(postbox: postbox) - case let .messageOfInterestHole(location, namespace, count): - return MutableMessageOfInterestHolesView(postbox: postbox, location: location, namespace: namespace, count: count) - case let .localMessageTag(tag): - return MutableLocalMessageTagsView(postbox: postbox, tag: tag) - case let .messages(ids): - return MutableMessagesView(postbox: postbox, ids: ids) - case .additionalChatListItems: - return MutableAdditionalChatListItemsView(postbox: postbox) - case let .cachedItem(id): - return MutableCachedItemView(postbox: postbox, id: id) - case let .peerPresences(ids): - return MutablePeerPresencesView(postbox: postbox, ids: ids) - case .synchronizeGroupMessageStats: - return MutableSynchronizeGroupMessageStatsView(postbox: postbox) - case .peerNotificationSettingsBehaviorTimestampView: - return MutablePeerNotificationSettingsBehaviorTimestampView(postbox: postbox) - case let .peerChatInclusion(peerId): - return MutablePeerChatInclusionView(postbox: postbox, peerId: peerId) - case let .basicPeer(peerId): - return MutableBasicPeerView(postbox: postbox, peerId: peerId) + case let .itemCollectionInfos(namespaces): + return MutableItemCollectionInfosView(postbox: postbox, namespaces: namespaces) + case let .itemCollectionIds(namespaces): + return MutableItemCollectionIdsView(postbox: postbox, namespaces: namespaces) + case let .itemCollectionInfo(id): + return MutableItemCollectionInfoView(postbox: postbox, id: id) + case let .peerChatState(peerId): + return MutablePeerChatStateView(postbox: postbox, peerId: peerId) + case let .orderedItemList(id): + return MutableOrderedItemListView(postbox: postbox, collectionId: id) + case let .preferences(keys): + return MutablePreferencesView(postbox: postbox, keys: keys) + case let .globalMessageTags(globalTag, position, count, groupingPredicate): + return MutableGlobalMessageTagsView(postbox: postbox, globalTag: globalTag, position: position, count: count, groupingPredicate: groupingPredicate) + case let .peer(peerId, components): + return MutablePeerView(postbox: postbox, peerId: peerId, components: components) + case let .pendingMessageActions(type): + return MutablePendingMessageActionsView(postbox: postbox, type: type) + case let .invalidatedMessageHistoryTagSummaries(tagMask, namespace): + return MutableInvalidatedMessageHistoryTagSummariesView(postbox: postbox, tagMask: tagMask, namespace: namespace) + case let .pendingMessageActionsSummary(type, peerId, namespace): + return MutablePendingMessageActionsSummaryView(postbox: postbox, type: type, peerId: peerId, namespace: namespace) + case let .historyTagSummaryView(tag, peerId, namespace): + return MutableMessageHistoryTagSummaryView(postbox: postbox, tag: tag, peerId: peerId, namespace: namespace) + case let .cachedPeerData(peerId): + return MutableCachedPeerDataView(postbox: postbox, peerId: peerId) + case let .unreadCounts(items): + return MutableUnreadMessageCountsView(postbox: postbox, items: items) + case let .peerNotificationSettings(peerIds): + return MutablePeerNotificationSettingsView(postbox: postbox, peerIds: peerIds) + case .pendingPeerNotificationSettings: + return MutablePendingPeerNotificationSettingsView(postbox: postbox) + case let .messageOfInterestHole(location, namespace, count): + return MutableMessageOfInterestHolesView(postbox: postbox, location: location, namespace: namespace, count: count) + case let .localMessageTag(tag): + return MutableLocalMessageTagsView(postbox: postbox, tag: tag) + case let .messages(ids): + return MutableMessagesView(postbox: postbox, ids: ids) + case .additionalChatListItems: + return MutableAdditionalChatListItemsView(postbox: postbox) + case let .cachedItem(id): + return MutableCachedItemView(postbox: postbox, id: id) + case let .peerPresences(ids): + return MutablePeerPresencesView(postbox: postbox, ids: ids) + case .synchronizeGroupMessageStats: + return MutableSynchronizeGroupMessageStatsView(postbox: postbox) + case .peerNotificationSettingsBehaviorTimestampView: + return MutablePeerNotificationSettingsBehaviorTimestampView(postbox: postbox) + case let .peerChatInclusion(peerId): + return MutablePeerChatInclusionView(postbox: postbox, peerId: peerId) + case let .basicPeer(peerId): + return MutableBasicPeerView(postbox: postbox, peerId: peerId) + case let .allChatListHoles(groupId): + return MutableAllChatListHolesView(postbox: postbox, groupId: groupId) + case let .historyTagInfo(peerId, tag): + return MutableHistoryTagInfoView(postbox: postbox, peerId: peerId, tag: tag) } } diff --git a/submodules/PresentationDataUtils/Sources/AlertTheme.swift b/submodules/PresentationDataUtils/Sources/AlertTheme.swift index e9245c8313..9a0c25b108 100644 --- a/submodules/PresentationDataUtils/Sources/AlertTheme.swift +++ b/submodules/PresentationDataUtils/Sources/AlertTheme.swift @@ -5,9 +5,9 @@ import AccountContext import SwiftSignalKit public func textAlertController(context: AccountContext, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true) -> AlertController { - return textAlertController(alertContext: AlertControllerContext(theme: AlertControllerTheme(presentationTheme: context.sharedContext.currentPresentationData.with({ $0 }).theme), themeSignal: context.sharedContext.presentationData |> map { presentationData in AlertControllerTheme(presentationTheme: presentationData.theme) }), title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset) + return textAlertController(alertContext: AlertControllerContext(theme: AlertControllerTheme(presentationData: context.sharedContext.currentPresentationData.with { $0 }), themeSignal: context.sharedContext.presentationData |> map { presentationData in AlertControllerTheme(presentationData: presentationData) }), title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset) } public func richTextAlertController(context: AccountContext, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, dismissAutomatically: Bool = true) -> AlertController { - return richTextAlertController(alertContext: AlertControllerContext(theme: AlertControllerTheme(presentationTheme: context.sharedContext.currentPresentationData.with({ $0 }).theme), themeSignal: context.sharedContext.presentationData |> map { presentationData in AlertControllerTheme(presentationTheme: presentationData.theme) }), title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, dismissAutomatically: dismissAutomatically) + return richTextAlertController(alertContext: AlertControllerContext(theme: AlertControllerTheme(presentationData: context.sharedContext.currentPresentationData.with { $0 }), themeSignal: context.sharedContext.presentationData |> map { presentationData in AlertControllerTheme(presentationData: presentationData) }), title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, dismissAutomatically: dismissAutomatically) } diff --git a/submodules/PresentationDataUtils/Sources/ItemListController.swift b/submodules/PresentationDataUtils/Sources/ItemListController.swift index 7887853851..e563d060a2 100644 --- a/submodules/PresentationDataUtils/Sources/ItemListController.swift +++ b/submodules/PresentationDataUtils/Sources/ItemListController.swift @@ -13,6 +13,6 @@ public extension ItemListController { convenience init(sharedContext: SharedAccountContext, state: Signal<(ItemListControllerState, (ItemListNodeState, ItemGenerationArguments)), NoError>, tabBarItem: Signal? = nil) { let presentationData = sharedContext.currentPresentationData.with { $0 } - self.init(theme: presentationData.theme, strings: presentationData.strings, updatedPresentationData: sharedContext.presentationData |> map { ($0.theme, $0.strings) }, state: state, tabBarItem: tabBarItem) + self.init(presentationData: ItemListPresentationData(presentationData), updatedPresentationData: sharedContext.presentationData |> map(ItemListPresentationData.init(_:)), state: state, tabBarItem: tabBarItem) } } diff --git a/submodules/PresentationDataUtils/Sources/SpecialTabBarIcons.swift b/submodules/PresentationDataUtils/Sources/SpecialTabBarIcons.swift new file mode 100644 index 0000000000..0fa053ebd0 --- /dev/null +++ b/submodules/PresentationDataUtils/Sources/SpecialTabBarIcons.swift @@ -0,0 +1,5 @@ +import Foundation + +public func useSpecialTabBarIcons() -> Bool { + return (Date(timeIntervalSince1970: 1581638400)...Date(timeIntervalSince1970: 1581724799)).contains(Date()) +} diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 427e712b6e..d7a15edd46 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -377,14 +377,12 @@ public final class ReactionContextNode: ASDisplayNode { } } - public func animateOutToReaction(value: String, targetNode: ASImageNode, hideNode: Bool, completion: @escaping () -> Void) { + public func animateOutToReaction(value: String, targetNode: ASDisplayNode, hideNode: Bool, completion: @escaping () -> Void) { for itemNode in self.itemNodes { switch itemNode.reaction { case let .reaction(itemValue, _, _): if itemValue == value { - if let snapshotView = itemNode.view.snapshotContentTree(keepTransform: true) { - let targetSnapshotView = UIImageView() - targetSnapshotView.image = targetNode.image + if let snapshotView = itemNode.view.snapshotContentTree(keepTransform: true), let targetSnapshotView = targetNode.view.snapshotContentTree() { targetSnapshotView.frame = self.view.convert(targetNode.bounds, from: targetNode.view) itemNode.isHidden = true self.view.addSubview(targetSnapshotView) diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index eb64faf4c6..3243132a1e 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -77,7 +77,7 @@ final class ReactionNode: ASDisplayNode { self.animationNode.automaticallyLoadFirstFrame = loadFirstFrame self.animationNode.playToCompletionOnStop = true - var intrinsicSize = CGSize(width: maximizedReactionSize + 18.0, height: maximizedReactionSize + 18.0) + var intrinsicSize = CGSize(width: maximizedReactionSize + 14.0, height: maximizedReactionSize + 14.0) self.imageNode = ASImageNode() switch reaction { @@ -195,9 +195,8 @@ final class ReactionSelectionNode: ASDisplayNode { private let hapticFeedback = HapticFeedback() private var shadowBlur: CGFloat = 8.0 - private var minimizedReactionSize: CGFloat = 30.0 - private var maximizedReactionSize: CGFloat = 60.0 - private var smallCircleSize: CGFloat = 8.0 + private var minimizedReactionSize: CGFloat = 28.0 + private var smallCircleSize: CGFloat = 14.0 private var isRightAligned: Bool = false @@ -246,43 +245,20 @@ final class ReactionSelectionNode: ASDisplayNode { isRightAligned = true } - if isInitial && self.reactionNodes.isEmpty { - let availableContentWidth = constrainedSize.width //max(100.0, initialAnchorX) - var minimizedReactionSize = (availableContentWidth - self.maximizedReactionSize) / (CGFloat(self.reactions.count - 1) + CGFloat(self.reactions.count + 1) * 0.2) - minimizedReactionSize = max(16.0, floor(minimizedReactionSize)) - minimizedReactionSize = min(30.0, minimizedReactionSize) - - self.minimizedReactionSize = minimizedReactionSize - self.shadowBlur = floor(minimizedReactionSize * 0.26) - self.smallCircleSize = 8.0 - - let backgroundHeight = floor(minimizedReactionSize * 1.4) - - self.backgroundNode.image = generateBubbleImage(foreground: .white, diameter: backgroundHeight, shadowBlur: self.shadowBlur) - self.backgroundShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: backgroundHeight, shadowBlur: self.shadowBlur) - for i in 0 ..< self.bubbleNodes.count { - self.bubbleNodes[i].0.image = generateBubbleImage(foreground: .white, diameter: CGFloat(i + 1) * self.smallCircleSize, shadowBlur: self.shadowBlur) - self.bubbleNodes[i].1.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: CGFloat(i + 1) * self.smallCircleSize, shadowBlur: self.shadowBlur) - } - - self.reactionNodes = self.reactions.map { reaction -> ReactionNode in - return ReactionNode(account: self.account, theme: self.theme, reaction: reaction, maximizedReactionSize: self.maximizedReactionSize, loadFirstFrame: true) - } - self.reactionNodes.forEach(self.addSubnode(_:)) - } - - let backgroundHeight: CGFloat = floor(self.minimizedReactionSize * 1.4) - - let reactionSpacing: CGFloat = floor(self.minimizedReactionSize * 0.2) - let minimizedReactionVerticalInset: CGFloat = floor((backgroundHeight - minimizedReactionSize) / 2.0) - - let contentWidth: CGFloat = CGFloat(self.reactionNodes.count - 1) * (minimizedReactionSize) + maximizedReactionSize + CGFloat(self.reactionNodes.count + 1) * reactionSpacing + let reactionSideInset: CGFloat = 10.0 + var reactionSpacing: CGFloat = 6.0 + let minReactionSpacing: CGFloat = 2.0 + let minimizedReactionSize = self.minimizedReactionSize + let contentWidth: CGFloat = CGFloat(self.reactions.count) * (minimizedReactionSize) + CGFloat(self.reactions.count - 1) * reactionSpacing + reactionSideInset * 2.0 + let spaceForMaximizedReaction = CGFloat(self.reactions.count - 1) * reactionSpacing - CGFloat(self.reactions.count - 1) * minReactionSpacing + let maximizedReactionSize: CGFloat = minimizedReactionSize + spaceForMaximizedReaction + let backgroundHeight: CGFloat = floor(self.minimizedReactionSize * 1.8) var backgroundFrame = CGRect(origin: CGPoint(x: -shadowBlur, y: -shadowBlur), size: CGSize(width: contentWidth + shadowBlur * 2.0, height: backgroundHeight + shadowBlur * 2.0)) - if isRightAligned { - backgroundFrame = backgroundFrame.offsetBy(dx: initialAnchorX - contentWidth + backgroundHeight / 2.0, dy: startingPoint.y - backgroundHeight - 16.0) + if constrainedSize.width > 500.0 { + backgroundFrame = backgroundFrame.offsetBy(dx: constrainedSize.width - contentWidth - 44.0, dy: startingPoint.y - backgroundHeight - 12.0) } else { - backgroundFrame = backgroundFrame.offsetBy(dx: initialAnchorX - backgroundHeight / 2.0, dy: startingPoint.y - backgroundHeight - 16.0) + backgroundFrame = backgroundFrame.offsetBy(dx: floor((constrainedSize.width - contentWidth) / 2.0), dy: startingPoint.y - backgroundHeight - 12.0) } backgroundFrame.origin.x = max(0.0, backgroundFrame.minX) backgroundFrame.origin.x = min(constrainedSize.width - backgroundFrame.width, backgroundFrame.minX) @@ -295,14 +271,44 @@ final class ReactionSelectionNode: ASDisplayNode { if let reaction = self.reactions.last, case .reply = reaction { maximizedIndex = self.reactions.count - 1 } - if backgroundFrame.insetBy(dx: -10.0, dy: -10.0).contains(touchPoint) { + if backgroundFrame.insetBy(dx: -10.0, dy: -10.0).offsetBy(dx: 0.0, dy: 10.0).contains(touchPoint) { maximizedIndex = Int(((touchPoint.x - anchorMinX) / (anchorMaxX - anchorMinX)) * CGFloat(self.reactionNodes.count)) maximizedIndex = max(0, min(self.reactionNodes.count - 1, maximizedIndex)) } - if maximizedIndex == -1 { + + let interReactionSpacing: CGFloat + if maximizedIndex != -1 { + interReactionSpacing = minReactionSpacing + } else { + interReactionSpacing = reactionSpacing + } + + if isInitial && self.reactionNodes.isEmpty { + let availableContentWidth = constrainedSize.width //max(100.0, initialAnchorX) + + self.shadowBlur = floor(minimizedReactionSize * 0.26) + self.smallCircleSize = 14.0 + + self.backgroundNode.image = generateBubbleImage(foreground: .white, diameter: backgroundHeight, shadowBlur: self.shadowBlur) + self.backgroundShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: backgroundHeight, shadowBlur: self.shadowBlur) + for i in 0 ..< self.bubbleNodes.count { + self.bubbleNodes[i].0.image = generateBubbleImage(foreground: .white, diameter: CGFloat(i + 1) * self.smallCircleSize, shadowBlur: self.shadowBlur) + self.bubbleNodes[i].1.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: CGFloat(i + 1) * self.smallCircleSize, shadowBlur: self.shadowBlur) + } + + self.reactionNodes = self.reactions.map { reaction -> ReactionNode in + return ReactionNode(account: self.account, theme: self.theme, reaction: reaction, maximizedReactionSize: maximizedReactionSize - 12.0, loadFirstFrame: true) + } + self.reactionNodes.forEach(self.addSubnode(_:)) + } + + let minimizedReactionVerticalInset: CGFloat = floor((backgroundHeight - minimizedReactionSize) / 2.0) + + + /*if maximizedIndex == -1 { backgroundFrame.size.width -= maximizedReactionSize - minimizedReactionSize backgroundFrame.origin.x += maximizedReactionSize - minimizedReactionSize - } + }*/ self.isRightAligned = isRightAligned @@ -315,8 +321,8 @@ final class ReactionSelectionNode: ASDisplayNode { backgroundTransition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) backgroundTransition.updateFrame(node: self.backgroundShadowNode, frame: backgroundFrame) - var reactionX: CGFloat = backgroundFrame.minX + shadowBlur + reactionSpacing - if offsetFromStart > backgroundFrame.maxX - shadowBlur || offsetFromStart < backgroundFrame.minX { + var reactionX: CGFloat = backgroundFrame.minX + shadowBlur + reactionSideInset + if maximizedIndex != -1 { self.hasSelectedNode = false } else { self.hasSelectedNode = true @@ -353,14 +359,14 @@ final class ReactionSelectionNode: ASDisplayNode { var reactionFrame = CGRect(origin: CGPoint(x: reactionX, y: backgroundFrame.maxY - shadowBlur - minimizedReactionVerticalInset - reactionSize), size: CGSize(width: reactionSize, height: reactionSize)) if isMaximized { - reactionFrame.origin.x -= 9.0 - reactionFrame.size.width += 18.0 + reactionFrame.origin.x -= 7.0 + reactionFrame.size.width += 14.0 } - self.reactionNodes[i].updateLayout(size: reactionFrame.size, scale: reactionFrame.size.width / (maximizedReactionSize + 18.0), transition: transition, displayText: isMaximized) + self.reactionNodes[i].updateLayout(size: reactionFrame.size, scale: reactionFrame.size.width / (maximizedReactionSize + 14.0), transition: transition, displayText: isMaximized) transition.updateFrame(node: self.reactionNodes[i], frame: reactionFrame, beginWithCurrentState: true) - reactionX += reactionSize + reactionSpacing + reactionX += reactionSize + interReactionSpacing } let mainBubbleFrame = CGRect(origin: CGPoint(x: anchorX - self.smallCircleSize - shadowBlur, y: backgroundFrame.maxY - shadowBlur - self.smallCircleSize - shadowBlur), size: CGSize(width: self.smallCircleSize * 2.0 + shadowBlur * 2.0, height: self.smallCircleSize * 2.0 + shadowBlur * 2.0)) @@ -381,9 +387,9 @@ final class ReactionSelectionNode: ASDisplayNode { let backgroundOffset: CGPoint if self.isRightAligned { - backgroundOffset = CGPoint(x: (self.backgroundNode.frame.width - shadowBlur) / 2.0 - 42.0, y: (self.backgroundNode.frame.height - shadowBlur) / 2.0) + backgroundOffset = CGPoint(x: (self.backgroundNode.frame.width - shadowBlur) / 2.0 - 42.0, y: 10.0) } else { - backgroundOffset = CGPoint(x: -(self.backgroundNode.frame.width - shadowBlur) / 2.0 + 42.0, y: (self.backgroundNode.frame.height - shadowBlur) / 2.0) + backgroundOffset = CGPoint(x: -(self.backgroundNode.frame.width - shadowBlur) / 2.0 + 42.0, y: 10.0) } let damping: CGFloat = 100.0 @@ -391,14 +397,15 @@ final class ReactionSelectionNode: ASDisplayNode { let animationOffset: Double = 1.0 - Double(i) / Double(self.reactionNodes.count - 1) var nodeOffset: CGPoint if self.isRightAligned { - nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.maxX - shadowBlur) / 2.0 - 42.0, y: self.reactionNodes[i].frame.minY - self.backgroundNode.frame.maxY - shadowBlur) + nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.maxX - shadowBlur) / 2.0 - 42.0, y: 10.0) } else { - nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.minX + shadowBlur) / 2.0 - 42.0, y: self.reactionNodes[i].frame.minY - self.backgroundNode.frame.maxY - shadowBlur) + nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.minX + shadowBlur) / 2.0 - 42.0, y: 10.0) } - nodeOffset.x = -nodeOffset.x + nodeOffset.x = 0.0 nodeOffset.y = 30.0 - self.reactionNodes[i].layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5 + animationOffset * 0.28, initialVelocity: 0.0, damping: damping) - self.reactionNodes[i].layer.animateSpring(from: NSValue(cgPoint: nodeOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, initialVelocity: 0.0, damping: damping, additive: true) + self.reactionNodes[i].layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04, delay: animationOffset * 0.1) + self.reactionNodes[i].layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, delay: animationOffset * 0.1, initialVelocity: 0.0, damping: damping) + //self.reactionNodes[i].layer.animateSpring(from: NSValue(cgPoint: nodeOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, delay: animationOffset * 0.1, initialVelocity: 0.0, damping: damping, additive: true) } self.backgroundNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, damping: damping) @@ -407,7 +414,7 @@ final class ReactionSelectionNode: ASDisplayNode { self.backgroundShadowNode.layer.animateSpring(from: NSValue(cgPoint: backgroundOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, initialVelocity: 0.0, damping: damping, additive: true) } - func animateOut(into targetNode: ASImageNode?, hideTarget: Bool, completion: @escaping () -> Void) { + func animateOut(into targetNode: ASDisplayNode?, hideTarget: Bool, completion: @escaping () -> Void) { self.hapticFeedback.prepareTap() var completedContainer = false @@ -422,9 +429,8 @@ final class ReactionSelectionNode: ASDisplayNode { if let targetNode = targetNode { for i in 0 ..< self.reactionNodes.count { if let isMaximized = self.reactionNodes[i].isMaximized, isMaximized { - if let snapshotView = self.reactionNodes[i].view.snapshotContentTree() { - let targetSnapshotView = UIImageView() - targetSnapshotView.image = targetNode.image + targetNode.recursivelyEnsureDisplaySynchronously(true) + if let snapshotView = self.reactionNodes[i].view.snapshotContentTree(), let targetSnapshotView = targetNode.view.snapshotContentTree() { targetSnapshotView.frame = self.view.convert(targetNode.bounds, from: targetNode.view) self.reactionNodes[i].isHidden = true self.view.addSubview(targetSnapshotView) @@ -483,7 +489,7 @@ final class ReactionSelectionNode: ASDisplayNode { } } - self.backgroundNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + //self.backgroundNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) self.backgroundShadowNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.backgroundShadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift index 66a7128362..e02bf48f5f 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift @@ -47,7 +47,7 @@ public final class ReactionSelectionParentNode: ASDisplayNode { return nil } - func dismissReactions(into targetNode: ASImageNode?, hideTarget: Bool) { + func dismissReactions(into targetNode: ASDisplayNode?, hideTarget: Bool) { if let currentNode = self.currentNode { currentNode.animateOut(into: targetNode, hideTarget: hideTarget, completion: { [weak currentNode] in currentNode?.removeFromSupernode() diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift b/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift index f82c3df64f..1380aad26f 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift @@ -16,6 +16,7 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { public var availableReactions: (() -> [ReactionGestureItem])? public var getReactionContainer: (() -> ReactionSelectionParentNode?)? public var getAnchorPoint: (() -> CGPoint?)? + public var shouldElevateAnchorPoint: (() -> Bool)? public var began: (() -> Void)? public var updateOffset: ((CGFloat, Bool) -> Void)? public var completed: ((ReactionGestureItem?) -> Void)? @@ -99,7 +100,9 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { self.f() } } - let activationTimer = Timer(timeInterval: 0.1, target: TimerTarget { [weak self] in + let elevate = self.shouldElevateAnchorPoint?() ?? false + + let activationTimer = Timer(timeInterval: elevate ? 0.15 : 0.01, target: TimerTarget { [weak self] in guard let strongSelf = self else { return } @@ -108,7 +111,9 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { let location = strongSelf.currentLocation if !strongSelf.currentReactions.isEmpty, let reactionContainer = strongSelf.getReactionContainer?(), let localAnchorPoint = strongSelf.getAnchorPoint?() { strongSelf.currentContainer = reactionContainer - let reactionContainerLocation = reactionContainer.view.convert(localAnchorPoint, from: strongSelf.view) + //let reactionContainerLocation = reactionContainer.view.convert(localAnchorPoint, from: strongSelf.view) + let elevate = strongSelf.shouldElevateAnchorPoint?() ?? false + let reactionContainerLocation = reactionContainer.view.convert(location, from: nil).offsetBy(dx: 0.0, dy: elevate ? -44.0 : 22.0) let reactionContainerTouchPoint = reactionContainer.view.convert(location, from: nil) strongSelf.currentAnchorPoint = reactionContainerLocation strongSelf.currentAnchorStartPoint = location @@ -173,7 +178,7 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { } } - public func complete(into targetNode: ASImageNode?, hideTarget: Bool) { + public func complete(into targetNode: ASDisplayNode?, hideTarget: Bool) { if self.isAwaitingCompletion { self.currentContainer?.dismissReactions(into: targetNode, hideTarget: hideTarget) self.state = .ended diff --git a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift index b0d0b97d6b..8dc1fed2ed 100644 --- a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift +++ b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift @@ -148,6 +148,12 @@ public func combineLatest(queue: Que }, initialValues: [:], queue: queue) } +public func combineLatest(queue: Queue? = nil, _ s1: Signal, _ s2: Signal, _ s3: Signal, _ s4: Signal, _ s5: Signal, _ s6: Signal, _ s7: Signal, _ s8: Signal, _ s9: Signal, _ s10: Signal, _ s11: Signal) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11), E> { + return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11)], combine: { values in + return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11) + }, initialValues: [:], queue: queue) +} + public func combineLatest(queue: Queue? = nil, _ signals: [Signal]) -> Signal<[T], E> { if signals.count == 0 { return single([T](), E.self) diff --git a/submodules/SaveToCameraRoll/BUCK b/submodules/SaveToCameraRoll/BUCK index d594ecc651..b75130a540 100644 --- a/submodules/SaveToCameraRoll/BUCK +++ b/submodules/SaveToCameraRoll/BUCK @@ -18,6 +18,8 @@ static_library( "$SDKROOT/System/Library/Frameworks/Foundation.framework", "$SDKROOT/System/Library/Frameworks/UIKit.framework", "$SDKROOT/System/Library/Frameworks/MobileCoreServices.framework", - "$SDKROOT/System/Library/Frameworks/Photos.framework", + ], + weak_frameworks = [ + "Photos", ], ) diff --git a/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift b/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift index a5c381ade1..6580e6ee46 100644 --- a/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift +++ b/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift @@ -10,12 +10,12 @@ import MobileCoreServices import DeviceAccess import AccountContext -private enum SaveToCameraRollState { +public enum FetchMediaDataState { case progress(Float) case data(MediaResourceData) } -private func fetchMediaData(context: AccountContext, postbox: Postbox, mediaReference: AnyMediaReference) -> Signal<(SaveToCameraRollState, Bool), NoError> { +public func fetchMediaData(context: AccountContext, postbox: Postbox, mediaReference: AnyMediaReference) -> Signal<(FetchMediaDataState, Bool), NoError> { var resource: MediaResource? var isImage = true var fileExtension: String? @@ -46,7 +46,7 @@ private func fetchMediaData(context: AccountContext, postbox: Postbox, mediaRefe } if let resource = resource { - let fetchedData: Signal = Signal { subscriber in + let fetchedData: Signal = Signal { subscriber in let fetched = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: mediaReference.resourceReference(resource)).start() let status = postbox.mediaBox.resourceStatus(resource).start(next: { status in switch status { diff --git a/submodules/ScreenCaptureDetection/Sources/ScreenCaptureDetection.swift b/submodules/ScreenCaptureDetection/Sources/ScreenCaptureDetection.swift index 0c35661804..26f6bfba5b 100644 --- a/submodules/ScreenCaptureDetection/Sources/ScreenCaptureDetection.swift +++ b/submodules/ScreenCaptureDetection/Sources/ScreenCaptureDetection.swift @@ -75,3 +75,50 @@ public func screenCaptureEvents() -> Signal { } |> runOn(Queue.mainQueue()) } + +public final class ScreenCaptureDetectionManager { + private var observer: NSObjectProtocol? + private var screenRecordingDisposable: Disposable? + private var screenRecordingCheckTimer: SwiftSignalKit.Timer? + + public init(check: @escaping () -> Bool) { + self.observer = NotificationCenter.default.addObserver(forName: UIApplication.userDidTakeScreenshotNotification, object: nil, queue: .main, using: { [weak self] _ in + guard let strongSelf = self else { + return + } + check() + }) + + self.screenRecordingDisposable = screenRecordingActive().start(next: { [weak self] value in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + if value { + if strongSelf.screenRecordingCheckTimer == nil { + strongSelf.screenRecordingCheckTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { + guard let strongSelf = self else { + return + } + if check() { + strongSelf.screenRecordingCheckTimer?.invalidate() + strongSelf.screenRecordingCheckTimer = nil + } + }, queue: Queue.mainQueue()) + strongSelf.screenRecordingCheckTimer?.start() + } + } else if strongSelf.screenRecordingCheckTimer != nil { + strongSelf.screenRecordingCheckTimer?.invalidate() + strongSelf.screenRecordingCheckTimer = nil + } + } + }) + } + + deinit { + NotificationCenter.default.removeObserver(self.observer) + self.screenRecordingDisposable?.dispose() + self.screenRecordingCheckTimer?.invalidate() + self.screenRecordingCheckTimer = nil + } +} diff --git a/submodules/SearchBarNode/Sources/SearchBarNode.swift b/submodules/SearchBarNode/Sources/SearchBarNode.swift index 7c381d91a9..16b8215467 100644 --- a/submodules/SearchBarNode/Sources/SearchBarNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarNode.swift @@ -87,12 +87,13 @@ private class SearchBarTextField: UITextField { } var rect = bounds.insetBy(dx: 4.0, dy: 4.0) - let prefixSize = self.prefixLabel.measure(bounds.size) + let prefixSize = self.prefixLabel.measure(CGSize(width: floor(bounds.size.width * 0.7), height: bounds.size.height)) if !prefixSize.width.isZero { let prefixOffset = prefixSize.width rect.origin.x += prefixOffset rect.size.width -= prefixOffset } + rect.size.width = max(rect.size.width, 10.0) return rect } @@ -117,7 +118,7 @@ private class SearchBarTextField: UITextField { let labelSize = self.placeholderLabel.measure(textRect.size) self.placeholderLabel.frame = CGRect(origin: CGPoint(x: textRect.minX, y: textRect.minY + textOffset), size: labelSize) - let prefixSize = self.prefixLabel.measure(bounds.size) + let prefixSize = self.prefixLabel.measure(CGSize(width: floor(bounds.size.width * 0.7), height: bounds.size.height)) let prefixBounds = bounds.insetBy(dx: 4.0, dy: 4.0) self.prefixLabel.frame = CGRect(origin: CGPoint(x: prefixBounds.minX, y: prefixBounds.minY + textOffset), size: prefixSize) } diff --git a/submodules/SearchUI/Sources/SearchDisplayController.swift b/submodules/SearchUI/Sources/SearchDisplayController.swift index fa86bdf81f..d3c6254589 100644 --- a/submodules/SearchUI/Sources/SearchDisplayController.swift +++ b/submodules/SearchUI/Sources/SearchDisplayController.swift @@ -22,7 +22,7 @@ public final class SearchDisplayController { private var isSearchingDisposable: Disposable? - public init(presentationData: PresentationData, mode: SearchDisplayControllerMode = .navigation, contentNode: SearchDisplayControllerContentNode, cancel: @escaping () -> Void) { + public init(presentationData: PresentationData, mode: SearchDisplayControllerMode = .navigation, placeholder: String? = nil, contentNode: SearchDisplayControllerContentNode, cancel: @escaping () -> Void) { self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: false), strings: presentationData.strings, fieldStyle: .modern) self.mode = mode self.contentNode = contentNode @@ -48,6 +48,9 @@ public final class SearchDisplayController { self?.searchBar.prefixString = prefix self?.searchBar.text = query } + if let placeholder = placeholder { + self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: presentationData.theme.rootController.navigationSearchBar.inputPlaceholderTextColor) + } self.contentNode.setPlaceholder = { [weak self] string in guard string != self?.searchBar.placeholderString?.string else { return @@ -99,7 +102,7 @@ public final class SearchDisplayController { self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarFrame.maxY, transition: transition) } - public func activate(insertSubnode: (ASDisplayNode, Bool) -> Void, placeholder: SearchBarPlaceholderNode) { + public func activate(insertSubnode: (ASDisplayNode, Bool) -> Void, placeholder: SearchBarPlaceholderNode?) { guard let (layout, navigationBarHeight) = self.containerLayout else { return } @@ -110,19 +113,20 @@ public final class SearchDisplayController { self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), navigationBarHeight: navigationBarHeight, transition: .immediate) - let initialTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: nil) - - let contentNodePosition = self.contentNode.layer.position - var contentNavigationBarHeight = navigationBarHeight if layout.statusBarHeight == nil { contentNavigationBarHeight += 28.0 } - self.contentNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), to: contentNodePosition, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) - - self.searchBar.placeholderString = placeholder.placeholderString + if let placeholder = placeholder { + let initialTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: nil) + + let contentNodePosition = self.contentNode.layer.position + + self.contentNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), to: contentNodePosition, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + self.searchBar.placeholderString = placeholder.placeholderString + } let navigationBarFrame: CGRect switch self.mode { @@ -149,18 +153,27 @@ public final class SearchDisplayController { self.searchBar.layout() self.searchBar.activate() - self.searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + if let placeholder = placeholder { + self.searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } else { + self.searchBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + } } public func deactivate(placeholder: SearchBarPlaceholderNode?, animated: Bool = true) { self.searchBar.deactivate() + let searchBar = self.searchBar if let placeholder = placeholder { - let searchBar = self.searchBar searchBar.transitionOut(to: placeholder, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate, completion: { [weak searchBar] in searchBar?.removeFromSupernode() }) + } else { + searchBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak searchBar] _ in + searchBar?.removeFromSupernode() + }) } let contentNode = self.contentNode diff --git a/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift b/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift index ba95b378b7..da4106824a 100644 --- a/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift +++ b/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift @@ -143,7 +143,20 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg } } + public func setSelectedIndex(_ index: Int, animated: Bool) { + guard index != self._selectedIndex else { + return + } + self._selectedIndex = index + if let layout = self.validLayout { + let _ = self.updateLayout(layout, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + } + public var selectedIndexChanged: (Int) -> Void = { _ in } + public var selectedIndexShouldChange: (Int, @escaping (Bool) -> Void) -> Void = { _, f in + f(true) + } public init(theme: SegmentedControlTheme, items: [SegmentedControlItem], selectedIndex: Int) { self.theme = theme @@ -347,16 +360,19 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg return } - self._selectedIndex = index - self.selectedIndexChanged(index) - if let layout = self.validLayout { - let _ = self.updateLayout(layout, transition: .animated(duration: 0.2, curve: .slide)) - } + self.selectedIndexShouldChange(index, { [weak self] commit in + if let strongSelf = self, commit { + strongSelf._selectedIndex = index + strongSelf.selectedIndexChanged(index) + if let layout = strongSelf.validLayout { + let _ = strongSelf.updateLayout(layout, transition: .animated(duration: 0.2, curve: .slide)) + } + } + }) } public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - let location = gestureRecognizer.location(in: self.view) - return self.selectionNode.frame.contains(location) + return self.selectionNode.frame.contains(gestureRecognizer.location(in: self.view)) } @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { @@ -382,8 +398,18 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg case .ended: if let gestureSelectedIndex = self.gestureSelectedIndex { if gestureSelectedIndex != self.selectedIndex { - self._selectedIndex = gestureSelectedIndex - self.selectedIndexChanged(self._selectedIndex) + self.selectedIndexShouldChange(gestureSelectedIndex, { [weak self] commit in + if let strongSelf = self { + if commit { + strongSelf._selectedIndex = gestureSelectedIndex + strongSelf.selectedIndexChanged(gestureSelectedIndex) + } else { + if let layout = strongSelf.validLayout { + let _ = strongSelf.updateLayout(layout, transition: .animated(duration: 0.2, curve: .slide)) + } + } + } + }) } self.gestureSelectedIndex = nil } diff --git a/submodules/SelectablePeerNode/BUCK b/submodules/SelectablePeerNode/BUCK index f4bfc7b54b..c8179d3371 100644 --- a/submodules/SelectablePeerNode/BUCK +++ b/submodules/SelectablePeerNode/BUCK @@ -18,6 +18,7 @@ static_library( "//submodules/LegacyComponents:LegacyComponents", "//submodules/ContextUI:ContextUI", "//submodules/LocalizedPeerData:LocalizedPeerData", + "//submodules/AccountContext:AccountContext", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift index 5116ee9794..56c29fcdf6 100644 --- a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift +++ b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift @@ -12,6 +12,7 @@ import PeerOnlineMarkerNode import LegacyComponents import ContextUI import LocalizedPeerData +import AccountContext private let avatarFont = avatarPlaceholderFont(size: 24.0) private let textFont = Font.regular(11.0) @@ -135,7 +136,7 @@ public final class SelectablePeerNode: ASDisplayNode { } } - public func setup(account: Account, theme: PresentationTheme, strings: PresentationStrings, peer: RenderedPeer, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) { + public func setup(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: RenderedPeer, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) { self.peer = peer guard let mainPeer = peer.chatMainPeer else { return @@ -145,7 +146,7 @@ public final class SelectablePeerNode: ASDisplayNode { let text: String var overrideImage: AvatarNodeImageOverride? - if peer.peerId == account.peerId { + if peer.peerId == context.account.peerId { text = strings.DialogList_SavedMessages overrideImage = .savedMessagesIcon } else { @@ -156,7 +157,7 @@ public final class SelectablePeerNode: ASDisplayNode { } self.textNode.maximumNumberOfLines = UInt(numberOfLines) self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.currentSelected ? self.theme.selectedTextColor : defaultColor, paragraphAlignment: .center) - self.avatarNode.setPeer(account: account, theme: theme, peer: mainPeer, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, synchronousLoad: synchronousLoad) + self.avatarNode.setPeer(context: context, theme: theme, peer: mainPeer, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, synchronousLoad: synchronousLoad) let onlineLayout = self.onlineNode.asyncLayout() let (onlineSize, onlineApply) = onlineLayout(online) diff --git a/submodules/SemanticStatusNode/BUCK b/submodules/SemanticStatusNode/BUCK new file mode 100644 index 0000000000..f2874d0b0a --- /dev/null +++ b/submodules/SemanticStatusNode/BUCK @@ -0,0 +1,17 @@ +load("//Config:buck_rule_macros.bzl", "static_library") + +static_library( + name = "SemanticStatusNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/Display:Display#shared", + "//submodules/AsyncDisplayKit:AsyncDisplayKit#shared", + "//submodules/LegacyComponents:LegacyComponents", + ], + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/UIKit.framework", + ], +) diff --git a/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift b/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift new file mode 100644 index 0000000000..483e62c5a5 --- /dev/null +++ b/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift @@ -0,0 +1,561 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display + +public enum SemanticStatusNodeState: Equatable { + case none + case download + case play + case pause + case progress(value: CGFloat?, cancelEnabled: Bool) + case customIcon(UIImage) +} + +private protocol SemanticStatusNodeStateDrawingState: NSObjectProtocol { + func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) +} + +private protocol SemanticStatusNodeStateContext: class { + var isAnimating: Bool { get } + + func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState +} + +private enum SemanticStatusNodeIcon: Equatable { + case none + case download + case play + case pause + case custom(UIImage) +} + +private func svgPath(_ path: StaticString, scale: CGPoint = CGPoint(x: 1.0, y: 1.0), offset: CGPoint = CGPoint()) throws -> UIBezierPath { + var index: UnsafePointer = path.utf8Start + let end = path.utf8Start.advanced(by: path.utf8CodeUnitCount) + let path = UIBezierPath() + while index < end { + let c = index.pointee + index = index.successor() + + if c == 77 { // M + let x = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x + let y = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y + + path.move(to: CGPoint(x: x, y: y)) + } else if c == 76 { // L + let x = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x + let y = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y + + path.addLine(to: CGPoint(x: x, y: y)) + } else if c == 67 { // C + let x1 = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x + let y1 = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y + let x2 = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x + let y2 = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y + let x = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x + let y = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y + path.addCurve(to: CGPoint(x: x, y: y), controlPoint1: CGPoint(x: x1, y: y1), controlPoint2: CGPoint(x: x2, y: y2)) + } else if c == 32 { // space + continue + } + } + return path +} + +private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContext { + final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState { + let transitionFraction: CGFloat + let icon: SemanticStatusNodeIcon + + init(transitionFraction: CGFloat, icon: SemanticStatusNodeIcon) { + self.transitionFraction = transitionFraction + self.icon = icon + + super.init() + } + + func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) { + context.saveGState() + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: max(0.01, self.transitionFraction), y: max(0.01, self.transitionFraction)) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + if foregroundColor.alpha.isZero { + context.setBlendMode(.destinationOut) + context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) + context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) + } else { + context.setBlendMode(.normal) + context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) + context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) + } + + switch self.icon { + case .none: + break + case .play: + let diameter = size.width + + let factor = diameter / 50.0 + + let size = CGSize(width: 15.0, height: 18.0) + context.translateBy(x: (diameter - size.width) / 2.0 + 1.5, y: (diameter - size.height) / 2.0) + if (diameter < 40.0) { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: factor, y: factor) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + let _ = try? drawSvgPath(context, path: "M1.71891969,0.209353049 C0.769586558,-0.350676705 0,0.0908839327 0,1.18800046 L0,16.8564753 C0,17.9569971 0.750549162,18.357187 1.67393713,17.7519379 L14.1073836,9.60224049 C15.0318735,8.99626906 15.0094718,8.04970371 14.062401,7.49100858 L1.71891969,0.209353049 ") + context.fillPath() + if (diameter < 40.0) { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + context.translateBy(x: -(diameter - size.width) / 2.0 - 1.5, y: -(diameter - size.height) / 2.0) + case .pause: + let diameter = size.width + + let factor = diameter / 50.0 + + let size = CGSize(width: 15.0, height: 16.0) + context.translateBy(x: (diameter - size.width) / 2.0, y: (diameter - size.height) / 2.0) + if (diameter < 40.0) { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: factor, y: factor) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + let _ = try? drawSvgPath(context, path: "M0,1.00087166 C0,0.448105505 0.443716645,0 0.999807492,0 L4.00019251,0 C4.55237094,0 5,0.444630861 5,1.00087166 L5,14.9991283 C5,15.5518945 4.55628335,16 4.00019251,16 L0.999807492,16 C0.447629061,16 0,15.5553691 0,14.9991283 L0,1.00087166 Z M10,1.00087166 C10,0.448105505 10.4437166,0 10.9998075,0 L14.0001925,0 C14.5523709,0 15,0.444630861 15,1.00087166 L15,14.9991283 C15,15.5518945 14.5562834,16 14.0001925,16 L10.9998075,16 C10.4476291,16 10,15.5553691 10,14.9991283 L10,1.00087166 ") + context.fillPath() + if (diameter < 40.0) { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + context.translateBy(x: -(diameter - size.width) / 2.0, y: -(diameter - size.height) / 2.0) + case let .custom(image): + let diameter = size.width + + let imageRect = CGRect(origin: CGPoint(x: floor((diameter - image.size.width) / 2.0), y: floor((diameter - image.size.height) / 2.0)), size: image.size) + + context.saveGState() + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + context.clip(to: imageRect, mask: image.cgImage!) + context.fill(imageRect) + context.restoreGState() + case .download: + let diameter = size.width + let factor = diameter / 50.0 + let lineWidth: CGFloat = max(1.6, 2.25 * factor) + + context.setLineWidth(lineWidth) + context.setLineCap(.round) + context.setLineJoin(.round) + + let arrowHeadSize: CGFloat = 15.0 * factor + let arrowLength: CGFloat = 18.0 * factor + let arrowHeadOffset: CGFloat = 1.0 * factor + + let leftPath = UIBezierPath() + leftPath.lineWidth = lineWidth + leftPath.lineCapStyle = .round + leftPath.lineJoinStyle = .round + leftPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset)) + leftPath.addLine(to: CGPoint(x: diameter / 2.0 - arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset)) + leftPath.stroke() + + let rightPath = UIBezierPath() + rightPath.lineWidth = lineWidth + rightPath.lineCapStyle = .round + rightPath.lineJoinStyle = .round + rightPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset)) + rightPath.addLine(to: CGPoint(x: diameter / 2.0 + arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset)) + rightPath.stroke() + + let bodyPath = UIBezierPath() + bodyPath.lineWidth = lineWidth + bodyPath.lineCapStyle = .round + bodyPath.lineJoinStyle = .round + bodyPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 - arrowLength / 2.0)) + bodyPath.addLine(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0)) + bodyPath.stroke() + } + context.restoreGState() + } + } + + let icon: SemanticStatusNodeIcon + + init(icon: SemanticStatusNodeIcon) { + self.icon = icon + } + + var isAnimating: Bool { + return false + } + + func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState { + return DrawingState(transitionFraction: transitionFraction, icon: self.icon) + } +} + +private final class SemanticStatusNodeProgressTransition { + let beginTime: Double + let initialValue: CGFloat + + init(beginTime: Double, initialValue: CGFloat) { + self.beginTime = beginTime + self.initialValue = initialValue + } + + func valueAt(timestamp: Double, actualValue: CGFloat) -> CGFloat { + let duration = 0.2 + var t = CGFloat((timestamp - self.beginTime) / duration) + t = min(1.0, max(0.0, t)) + return t * actualValue + (1.0 - t) * self.initialValue + } +} + +private final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext { + final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState { + let transitionFraction: CGFloat + let value: CGFloat? + let displayCancel: Bool + let timestamp: Double + + init(transitionFraction: CGFloat, value: CGFloat?, displayCancel: Bool, timestamp: Double) { + self.transitionFraction = transitionFraction + self.value = value + self.displayCancel = displayCancel + self.timestamp = timestamp + + super.init() + } + + func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) { + let diameter = size.width + + let factor = diameter / 50.0 + + context.saveGState() + + if foregroundColor.alpha.isZero { + context.setBlendMode(.destinationOut) + context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) + context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) + } else { + context.setBlendMode(.normal) + context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) + context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) + } + + var progress = self.value ?? 0.1 + var startAngle = -CGFloat.pi / 2.0 + var endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle + + if progress > 1.0 { + progress = 2.0 - progress + let tmp = startAngle + startAngle = endAngle + endAngle = tmp + } + progress = min(1.0, progress) + + let lineWidth: CGFloat = max(1.6, 2.25 * factor) + + let pathDiameter: CGFloat + pathDiameter = diameter - lineWidth - 2.5 * 2.0 + + var angle = self.timestamp.truncatingRemainder(dividingBy: Double.pi * 2.0) + angle *= 4.0 + + context.translateBy(x: diameter / 2.0, y: diameter / 2.0) + context.rotate(by: CGFloat(angle.truncatingRemainder(dividingBy: Double.pi * 2.0))) + context.translateBy(x: -diameter / 2.0, y: -diameter / 2.0) + + let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true) + path.lineWidth = lineWidth + path.lineCapStyle = .round + path.stroke() + + context.restoreGState() + + if self.displayCancel { + if foregroundColor.alpha.isZero { + context.setBlendMode(.destinationOut) + context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) + context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor) + } else { + context.setBlendMode(.normal) + context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) + context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor) + } + + context.saveGState() + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: max(0.01, self.transitionFraction), y: max(0.01, self.transitionFraction)) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + context.setLineWidth(max(1.3, 2.0 * factor)) + context.setLineCap(.round) + + let crossSize: CGFloat = 14.0 * factor + context.move(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0)) + context.addLine(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0)) + context.strokePath() + context.move(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0)) + context.addLine(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0)) + context.strokePath() + + context.restoreGState() + } + } + } + + var value: CGFloat? + let displayCancel: Bool + var transition: SemanticStatusNodeProgressTransition? + + var isAnimating: Bool { + return true + } + + init(value: CGFloat?, displayCancel: Bool) { + self.value = value + self.displayCancel = displayCancel + } + + func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState { + let timestamp = CACurrentMediaTime() + + let resolvedValue: CGFloat? + if let value = self.value { + if let transition = self.transition { + resolvedValue = transition.valueAt(timestamp: timestamp, actualValue: value) + } else { + resolvedValue = value + } + } else { + resolvedValue = nil + } + return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, displayCancel: self.displayCancel, timestamp: timestamp) + } + + func updateValue(value: CGFloat?) { + if value != self.value { + let previousValue = value + self.value = value + let timestamp = CACurrentMediaTime() + if let value = value, let previousValue = previousValue { + if let transition = self.transition { + self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: transition.valueAt(timestamp: timestamp, actualValue: previousValue)) + } else { + self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: previousValue) + } + } else { + self.transition = nil + } + } + } +} + +private extension SemanticStatusNodeState { + func context(current: SemanticStatusNodeStateContext?) -> SemanticStatusNodeStateContext { + switch self { + case .none, .download, .play, .pause, .customIcon: + let icon: SemanticStatusNodeIcon + switch self { + case .none: + icon = .none + case .download: + icon = .download + case .play: + icon = .play + case .pause: + icon = .pause + case let .customIcon(image): + icon = .custom(image) + default: + preconditionFailure() + } + if let current = current as? SemanticStatusNodeIconContext, current.icon == icon { + return current + } else { + return SemanticStatusNodeIconContext(icon: icon) + } + case let .progress(value, cancelEnabled): + if let current = current as? SemanticStatusNodeProgressContext, current.displayCancel == cancelEnabled { + current.updateValue(value: value) + return current + } else { + return SemanticStatusNodeProgressContext(value: value, displayCancel: cancelEnabled) + } + } + } +} + +private final class SemanticStatusNodeTransitionDrawingState { + let transition: CGFloat + let drawingState: SemanticStatusNodeStateDrawingState + + init(transition: CGFloat, drawingState: SemanticStatusNodeStateDrawingState) { + self.transition = transition + self.drawingState = drawingState + } +} + +private final class SemanticStatusNodeDrawingState: NSObject { + let background: UIColor + let foreground: UIColor + let transitionState: SemanticStatusNodeTransitionDrawingState? + let drawingState: SemanticStatusNodeStateDrawingState + + init(background: UIColor, foreground: UIColor, transitionState: SemanticStatusNodeTransitionDrawingState?, drawingState: SemanticStatusNodeStateDrawingState) { + self.background = background + self.foreground = foreground + self.transitionState = transitionState + self.drawingState = drawingState + + super.init() + } +} + +private final class SemanticStatusNodeTransitionContext { + let startTime: Double + let duration: Double + let previousStateContext: SemanticStatusNodeStateContext + let completion: () -> Void + + init(startTime: Double, duration: Double, previousStateContext: SemanticStatusNodeStateContext, completion: @escaping () -> Void) { + self.startTime = startTime + self.duration = duration + self.previousStateContext = previousStateContext + self.completion = completion + } +} + +public final class SemanticStatusNode: ASControlNode { + public var backgroundNodeColor: UIColor { + didSet { + self.setNeedsDisplay() + } + } + + public var foregroundNodeColor: UIColor { + didSet { + self.setNeedsDisplay() + } + } + + private var animator: ConstantDisplayLinkAnimator? + + public private(set) var state: SemanticStatusNodeState + private var transtionContext: SemanticStatusNodeTransitionContext? + private var stateContext: SemanticStatusNodeStateContext + + public init(backgroundNodeColor: UIColor, foregroundNodeColor: UIColor) { + self.backgroundNodeColor = backgroundNodeColor + self.foregroundNodeColor = foregroundNodeColor + self.state = .none + self.stateContext = self.state.context(current: nil) + + super.init() + + self.isOpaque = false + self.displaysAsynchronously = true + } + + private func updateAnimations() { + var animate = false + let timestamp = CACurrentMediaTime() + + if let transtionContext = self.transtionContext { + if transtionContext.startTime + transtionContext.duration < timestamp { + self.transtionContext = nil + transtionContext.completion() + } else { + animate = true + } + } + + if self.stateContext.isAnimating { + animate = true + } + + if animate { + let animator: ConstantDisplayLinkAnimator + if let current = self.animator { + animator = current + } else { + animator = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.updateAnimations() + }) + self.animator = animator + } + animator.isPaused = false + } else { + self.animator?.isPaused = true + } + + self.setNeedsDisplay() + } + + public func transitionToState(_ state: SemanticStatusNodeState, animated: Bool = true, synchronous: Bool = false, completion: @escaping () -> Void = {}) { + var animated = animated + if self.state != state { + let fromState = self.state + self.state = state + let previousStateContext = self.stateContext + self.stateContext = self.state.context(current: self.stateContext) + + if animated && previousStateContext !== self.stateContext { + self.transtionContext = SemanticStatusNodeTransitionContext(startTime: CACurrentMediaTime(), duration: 0.18, previousStateContext: previousStateContext, completion: completion) + } else { + completion() + } + + self.updateAnimations() + self.setNeedsDisplay() + } else { + completion() + } + } + + override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + var transitionState: SemanticStatusNodeTransitionDrawingState? + var transitionFraction: CGFloat = 1.0 + if let transitionContext = self.transtionContext { + let timestamp = CACurrentMediaTime() + var t = CGFloat((timestamp - transitionContext.startTime) / transitionContext.duration) + t = min(1.0, max(0.0, t)) + transitionFraction = t + transitionState = SemanticStatusNodeTransitionDrawingState(transition: t, drawingState: transitionContext.previousStateContext.drawingState(transitionFraction: 1.0 - t)) + } + + return SemanticStatusNodeDrawingState(background: self.backgroundNodeColor, foreground: self.foregroundNodeColor, transitionState: transitionState, drawingState: self.stateContext.drawingState(transitionFraction: transitionFraction)) + } + + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + guard let parameters = parameters as? SemanticStatusNodeDrawingState else { + return + } + + context.setFillColor(parameters.background.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: bounds.size)) + if let transitionState = parameters.transitionState { + transitionState.drawingState.draw(context: context, size: bounds.size, foregroundColor: parameters.foreground) + } + parameters.drawingState.draw(context: context, size: bounds.size, foregroundColor: parameters.foreground) + } +} diff --git a/submodules/SettingsUI/BUCK b/submodules/SettingsUI/BUCK index f703b3fc54..a60dc14f82 100644 --- a/submodules/SettingsUI/BUCK +++ b/submodules/SettingsUI/BUCK @@ -78,20 +78,24 @@ static_library( "//submodules/InstantPageCache:InstantPageCache", "//submodules/AppBundle:AppBundle", "//submodules/ContextUI:ContextUI", - "//submodules/WalletUI:WalletUI", + #"//submodules/WalletUI:WalletUI", "//submodules/Markdown:Markdown", "//submodules/UndoUI:UndoUI", "//submodules/DeleteChatPeerActionSheetItem:DeleteChatPeerActionSheetItem", "//submodules/PhoneNumberFormat:PhoneNumberFormat", "//submodules/OpenInExternalAppUI:OpenInExternalAppUI", + "//submodules/AccountUtils:AccountUtils", + "//submodules/AuthTransferUI:AuthTransferUI", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", "$SDKROOT/System/Library/Frameworks/UIKit.framework", "$SDKROOT/System/Library/Frameworks/MessageUI.framework", "$SDKROOT/System/Library/Frameworks/LocalAuthentication.framework", - "$SDKROOT/System/Library/Frameworks/Photos.framework", "$SDKROOT/System/Library/Frameworks/QuickLook.framework", "$SDKROOT/System/Library/Frameworks/CoreTelephony.framework", ], + weak_frameworks = [ + "Photos", + ], ) diff --git a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift new file mode 100644 index 0000000000..dc1129ff5b --- /dev/null +++ b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift @@ -0,0 +1,549 @@ +import Foundation +import UIKit +import Display +import Postbox +import SwiftSignalKit +import AsyncDisplayKit +import TelegramCore +import SyncCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import ChatListUI +import WallpaperResources +import LegacyComponents +import ItemListUI + +private func generateMaskImage(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let gradientColors = [color.withAlphaComponent(0.0).cgColor, color.cgColor, color.cgColor] as CFArray + + var locations: [CGFloat] = [0.0, 0.75, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 80.0), options: CGGradientDrawingOptions()) + }) +} + +private final class BubbleSettingsControllerNode: ASDisplayNode, UIScrollViewDelegate { + private let context: AccountContext + private var presentationThemeSettings: PresentationThemeSettings + private var presentationData: PresentationData + + private let referenceTimestamp: Int32 + + private let scrollNode: ASScrollNode + + private let maskNode: ASImageNode + private let chatBackgroundNode: WallpaperBackgroundNode + private let messagesContainerNode: ASDisplayNode + private var dateHeaderNode: ListViewItemHeaderNode? + private var messageNodes: [ListViewItemNode]? + private let toolbarNode: BubbleSettingsToolbarNode + + private var validLayout: (ContainerViewLayout, CGFloat)? + + init(context: AccountContext, presentationThemeSettings: PresentationThemeSettings, dismiss: @escaping () -> Void, apply: @escaping (PresentationChatBubbleSettings) -> Void) { + self.context = context + + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationThemeSettings = presentationThemeSettings + + let calendar = Calendar(identifier: .gregorian) + var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: Date()) + components.hour = 13 + components.minute = 0 + components.second = 0 + self.referenceTimestamp = Int32(calendar.date(from: components)?.timeIntervalSince1970 ?? 0.0) + + self.scrollNode = ASScrollNode() + + self.chatBackgroundNode = WallpaperBackgroundNode() + self.chatBackgroundNode.displaysAsynchronously = false + + self.messagesContainerNode = ASDisplayNode() + self.messagesContainerNode.clipsToBounds = true + self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + + self.chatBackgroundNode.image = chatControllerBackgroundImage(theme: self.presentationData.theme, wallpaper: self.presentationData.chatWallpaper, mediaBox: context.sharedContext.accountManager.mediaBox, knockoutMode: false) + self.chatBackgroundNode.motionEnabled = self.presentationData.chatWallpaper.settings?.motion ?? false + if case .gradient = self.presentationData.chatWallpaper { + self.chatBackgroundNode.imageContentMode = .scaleToFill + } + + self.toolbarNode = BubbleSettingsToolbarNode(presentationThemeSettings: self.presentationThemeSettings, presentationData: self.presentationData) + + self.maskNode = ASImageNode() + self.maskNode.displaysAsynchronously = false + self.maskNode.displayWithoutProcessing = true + self.maskNode.contentMode = .scaleToFill + + + super.init() + + self.setViewBlock({ + return UITracingLayerView() + }) + + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + self.maskNode.image = generateMaskImage(color: self.presentationData.theme.chatList.backgroundColor) + + self.addSubnode(self.scrollNode) + self.addSubnode(self.toolbarNode) + + self.scrollNode.addSubnode(self.chatBackgroundNode) + self.scrollNode.addSubnode(self.messagesContainerNode) + + self.toolbarNode.cancel = { + dismiss() + } + var dismissed = false + self.toolbarNode.done = { [weak self] in + guard let strongSelf = self else { + return + } + if !dismissed { + dismissed = true + apply(strongSelf.presentationThemeSettings.chatBubbleSettings) + } + } + self.toolbarNode.updateMergeBubbleCorners = { [weak self] value in + guard let strongSelf = self else { + return + } + strongSelf.presentationThemeSettings.chatBubbleSettings.mergeBubbleCorners = value + strongSelf.updatePresentationThemeSettings(strongSelf.presentationThemeSettings) + } + self.toolbarNode.updateCornerRadius = { [weak self] value in + guard let strongSelf = self else { + return + } + strongSelf.presentationThemeSettings.chatBubbleSettings.mainRadius = Int32(value) + strongSelf.presentationThemeSettings.chatBubbleSettings.auxiliaryRadius = Int32(value / 2) + strongSelf.updatePresentationThemeSettings(strongSelf.presentationThemeSettings) + } + } + + override func didLoad() { + super.didLoad() + + self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true + self.scrollNode.view.showsHorizontalScrollIndicator = false + self.scrollNode.view.isPagingEnabled = true + self.scrollNode.view.delegate = self + self.scrollNode.view.alwaysBounceHorizontal = false + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + } + + func animateIn(completion: (() -> Void)? = nil) { + if let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass { + self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } + } + + func animateOut(completion: (() -> Void)? = nil) { + if let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass { + self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in + completion?() + }) + } else { + completion?() + } + } + + private func updateMessagesLayout(layout: ContainerViewLayout, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + let headerItem = self.context.sharedContext.makeChatMessageDateHeaderItem(context: self.context, timestamp: self.referenceTimestamp, theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder) + + var items: [ListViewItem] = [] + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: 1) + let otherPeerId = self.context.account.peerId + var peers = SimpleDictionary() + var messages = SimpleDictionary() + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + + let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3) + messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + + let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message1, theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil)) + + let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message2, theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil)) + + let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" + let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: MemoryBuffer(data: Data(base64Encoded: waveformBase64)!))] + let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) + + let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: []) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message3, theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil)) + + let message4 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message4, theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil)) + + let width: CGFloat + if case .regular = layout.metrics.widthClass { + width = layout.size.width + } else { + width = layout.size.width + } + + let params = ListViewItemLayoutParams(width: width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) + if let messageNodes = self.messageNodes { + for i in 0 ..< items.count { + let itemNode = messageNodes[i] + items[i].updateNode(async: { $0() }, node: { + return itemNode + }, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: width, height: layout.size.height)) + + itemNode.contentSize = layout.contentSize + itemNode.insets = layout.insets + itemNode.frame = nodeFrame + itemNode.isUserInteractionEnabled = false + + apply(ListViewItemApply(isOnScreen: true)) + }) + } + } else { + var messageNodes: [ListViewItemNode] = [] + for i in 0 ..< items.count { + var itemNode: ListViewItemNode? + items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in + itemNode = node + apply().1(ListViewItemApply(isOnScreen: true)) + }) + itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + itemNode!.isUserInteractionEnabled = false + messageNodes.append(itemNode!) + self.messagesContainerNode.addSubnode(itemNode!) + if let extractedBackgroundNode = itemNode!.extractedBackgroundNode { + extractedBackgroundNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + self.messagesContainerNode.insertSubnode(extractedBackgroundNode, at: 0) + } + } + self.messageNodes = messageNodes + } + + var bottomOffset: CGFloat = 9.0 + bottomInset + if let messageNodes = self.messageNodes { + for itemNode in messageNodes { + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: itemNode.frame.size)) + if let extractedBackgroundNode = itemNode.extractedBackgroundNode { + transition.updateFrame(node: extractedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: itemNode.frame.size)) + } + bottomOffset += itemNode.frame.height + itemNode.updateFrame(itemNode.frame, within: layout.size) + } + } + + let dateHeaderNode: ListViewItemHeaderNode + if let currentDateHeaderNode = self.dateHeaderNode { + dateHeaderNode = currentDateHeaderNode + headerItem.updateNode(dateHeaderNode, previous: nil, next: headerItem) + } else { + dateHeaderNode = headerItem.node() + dateHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + self.messagesContainerNode.addSubnode(dateHeaderNode) + self.dateHeaderNode = dateHeaderNode + } + + transition.updateFrame(node: dateHeaderNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: CGSize(width: layout.size.width, height: headerItem.height))) + dateHeaderNode.updateLayout(size: self.messagesContainerNode.frame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right) + } + + func updatePresentationThemeSettings(_ presentationThemeSettings: PresentationThemeSettings) { + let chatBubbleCorners = PresentationChatBubbleCorners(mainRadius: CGFloat(presentationThemeSettings.chatBubbleSettings.mainRadius), auxiliaryRadius: CGFloat(presentationThemeSettings.chatBubbleSettings.auxiliaryRadius), mergeBubbleCorners: presentationThemeSettings.chatBubbleSettings.mergeBubbleCorners) + + self.presentationData = self.presentationData.withChatBubbleCorners(chatBubbleCorners) + self.toolbarNode.updatePresentationData(presentationData: self.presentationData) + self.toolbarNode.updatePresentationThemeSettings(presentationThemeSettings: self.presentationThemeSettings) + if let (layout, navigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.recursivelyEnsureDisplaySynchronously(true) + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.validLayout = (layout, navigationBarHeight) + + let bounds = CGRect(origin: CGPoint(), size: layout.size) + self.scrollNode.frame = bounds + + let toolbarHeight = self.toolbarNode.updateLayout(width: layout.size.width, bottomInset: layout.intrinsicInsets.bottom, layout: layout, transition: transition) + + var chatFrame = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height) + + let bottomInset: CGFloat + chatFrame = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height) + self.scrollNode.view.contentSize = CGSize(width: bounds.width, height: bounds.height) + + bottomInset = 37.0 + + self.chatBackgroundNode.frame = chatFrame + self.chatBackgroundNode.updateLayout(size: chatFrame.size, transition: transition) + self.messagesContainerNode.frame = chatFrame + + transition.updateFrame(node: self.toolbarNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: toolbarHeight + layout.intrinsicInsets.bottom))) + + self.updateMessagesLayout(layout: layout, bottomInset: toolbarHeight + bottomInset, transition: transition) + + transition.updateFrame(node: self.maskNode, frame: CGRect(x: 0.0, y: layout.size.height - toolbarHeight - 80.0, width: bounds.width, height: 80.0)) + } +} + +final class BubbleSettingsController: ViewController { + private let context: AccountContext + + private var controllerNode: BubbleSettingsControllerNode { + return self.displayNode as! BubbleSettingsControllerNode + } + + private var didPlayPresentationAnimation = false + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private var presentationThemeSettings: PresentationThemeSettings + private var presentationThemeSettingsDisposable: Disposable? + + private var disposable: Disposable? + private var applyDisposable = MetaDisposable() + + public init(context: AccountContext, presentationThemeSettings: PresentationThemeSettings) { + self.context = context + + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationThemeSettings = presentationThemeSettings + + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationTheme: self.presentationData.theme, presentationStrings: self.presentationData.strings)) + + self.blocksBackgroundWhenInOverlay = true + self.navigationPresentation = .modal + + self.navigationItem.title = self.presentationData.strings.Appearance_BubbleCorners_Title + self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + strongSelf.presentationData = presentationData + } + }) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + self.presentationThemeSettingsDisposable?.dispose() + self.disposable?.dispose() + self.applyDisposable.dispose() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation { + self.didPlayPresentationAnimation = true + if case .modalSheet = presentationArguments.presentationAnimation { + self.controllerNode.animateIn() + } + } + } + + override public func loadDisplayNode() { + super.loadDisplayNode() + + self.displayNode = BubbleSettingsControllerNode(context: self.context, presentationThemeSettings: self.presentationThemeSettings, dismiss: { [weak self] in + if let strongSelf = self { + strongSelf.dismiss() + } + }, apply: { [weak self] chatBubbleSettings in + if let strongSelf = self { + strongSelf.apply(chatBubbleSettings: chatBubbleSettings) + } + }) + self.displayNodeDidLoad() + } + + private func apply(chatBubbleSettings: PresentationChatBubbleSettings) { + let _ = (updatePresentationThemeSettingsInteractively(accountManager: self.context.sharedContext.accountManager, { current in + var current = current + current.chatBubbleSettings = chatBubbleSettings + return current + }) + |> deliverOnMainQueue).start(completed: { [weak self] in + self?.dismiss() + }) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } +} + +private enum TextSelectionCustomMode { + case list + case chat +} + +private final class BubbleSettingsToolbarNode: ASDisplayNode { + private var presentationThemeSettings: PresentationThemeSettings + private var presentationData: PresentationData + + private let cancelButton = HighlightableButtonNode() + private let doneButton = HighlightableButtonNode() + private let separatorNode = ASDisplayNode() + private let topSeparatorNode = ASDisplayNode() + + private var switchItemNode: ItemListSwitchItemNode + private var cornerRadiusItemNode: BubbleSettingsRadiusItemNode + + private(set) var customMode: TextSelectionCustomMode = .chat + + var cancel: (() -> Void)? + var done: (() -> Void)? + + var updateMergeBubbleCorners: ((Bool) -> Void)? + var updateCornerRadius: ((Int32) -> Void)? + + init(presentationThemeSettings: PresentationThemeSettings, presentationData: PresentationData) { + self.presentationThemeSettings = presentationThemeSettings + self.presentationData = presentationData + + self.switchItemNode = ItemListSwitchItemNode(type: .regular) + self.cornerRadiusItemNode = BubbleSettingsRadiusItemNode() + + super.init() + + self.addSubnode(self.switchItemNode) + self.addSubnode(self.cornerRadiusItemNode) + self.addSubnode(self.cancelButton) + self.addSubnode(self.doneButton) + self.addSubnode(self.separatorNode) + self.addSubnode(self.topSeparatorNode) + + self.updatePresentationData(presentationData: self.presentationData) + + self.cancelButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.cancelButton.backgroundColor = strongSelf.presentationData.theme.list.itemHighlightedBackgroundColor + } else { + UIView.animate(withDuration: 0.3, animations: { + strongSelf.cancelButton.backgroundColor = .clear + }) + } + } + } + + self.doneButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.doneButton.backgroundColor = strongSelf.presentationData.theme.list.itemHighlightedBackgroundColor + } else { + UIView.animate(withDuration: 0.3, animations: { + strongSelf.doneButton.backgroundColor = .clear + }) + } + } + } + + self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) + self.doneButton.addTarget(self, action: #selector(self.donePressed), forControlEvents: .touchUpInside) + } + + func setDoneEnabled(_ enabled: Bool) { + self.doneButton.alpha = enabled ? 1.0 : 0.4 + self.doneButton.isUserInteractionEnabled = enabled + } + + func setCustomMode(_ customMode: TextSelectionCustomMode) { + self.customMode = customMode + } + + func updatePresentationData(presentationData: PresentationData) { + self.backgroundColor = presentationData.theme.rootController.tabBar.backgroundColor + self.separatorNode.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor + self.topSeparatorNode.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor + + self.cancelButton.setTitle(presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: presentationData.theme.list.itemPrimaryTextColor, for: []) + self.doneButton.setTitle(presentationData.strings.Wallpaper_Set, with: Font.regular(17.0), with: presentationData.theme.list.itemPrimaryTextColor, for: []) + } + + func updatePresentationThemeSettings(presentationThemeSettings: PresentationThemeSettings) { + self.presentationThemeSettings = presentationThemeSettings + } + + func updateLayout(width: CGFloat, bottomInset: CGFloat, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat { + var contentHeight: CGFloat = 0.0 + + let switchItem = ItemListSwitchItem(presentationData: ItemListPresentationData(self.presentationData), title: self.presentationData.strings.Appearance_BubbleCorners_AdjustAdjacent, value: self.presentationThemeSettings.chatBubbleSettings.mergeBubbleCorners, disableLeadingInset: true, sectionId: 0, style: .blocks, updated: { [weak self] value in + self?.updateMergeBubbleCorners?(value) + }) + let cornerRadiusItem = BubbleSettingsRadiusItem(theme: self.presentationData.theme, value: Int(self.presentationData.chatBubbleCorners.mainRadius), enabled: true, disableLeadingInset: false, displayIcons: false, force: false, sectionId: 0, updated: { [weak self] value in + self?.updateCornerRadius?(Int32(max(8, min(16, value)))) + }) + + /*switchItem.updateNode(async: { f in + f() + }, node: { + return self.switchItemNode + }, params: ListViewItemLayoutParams(width: width, leftInset: layout.intrinsicInsets.left, rightInset: layout.intrinsicInsets.right, availableHeight: 1000.0), previousItem: nil, nextItem: cornerRadiusItem, animation: .None, completion: { layout, apply in + self.switchItemNode.contentSize = layout.contentSize + self.switchItemNode.insets = layout.insets + transition.updateFrame(node: self.switchItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: layout.contentSize)) + contentHeight += layout.contentSize.height + apply(ListViewItemApply(isOnScreen: true)) + })*/ + + cornerRadiusItem.updateNode(async: { f in + f() + }, node: { + return self.cornerRadiusItemNode + }, params: ListViewItemLayoutParams(width: width, leftInset: layout.intrinsicInsets.left, rightInset: layout.intrinsicInsets.right, availableHeight: 1000.0), previousItem: switchItem, nextItem: nil, animation: .None, completion: { layout, apply in + self.cornerRadiusItemNode.contentSize = layout.contentSize + self.cornerRadiusItemNode.insets = layout.insets + transition.updateFrame(node: self.cornerRadiusItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: layout.contentSize)) + contentHeight += layout.contentSize.height + apply(ListViewItemApply(isOnScreen: true)) + }) + + self.cancelButton.frame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: floor(width / 2.0), height: 49.0)) + self.doneButton.frame = CGRect(origin: CGPoint(x: floor(width / 2.0), y: contentHeight), size: CGSize(width: width - floor(width / 2.0), height: 49.0)) + + contentHeight += 49.0 + + self.topSeparatorNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel)) + + let resultHeight = contentHeight + bottomInset + + self.separatorNode.frame = CGRect(origin: CGPoint(x: floor(width / 2.0), y: self.cancelButton.frame.minY), size: CGSize(width: UIScreenPixel, height: resultHeight - self.cancelButton.frame.minY)) + + return resultHeight + } + + @objc func cancelPressed() { + self.cancel?() + } + + @objc func donePressed() { + self.doneButton.isUserInteractionEnabled = false + self.done?() + } +} diff --git a/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift b/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift index eb8b98ef4a..3f37c4c2a7 100644 --- a/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift +++ b/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift @@ -86,17 +86,17 @@ private enum ChangePhoneNumberCodeEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ChangePhoneNumberCodeControllerArguments switch self { case let .codeEntry(theme, strings, title, text): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: title, textColor: .black), text: text, placeholder: "", type: .number, spacing: 10.0, tag: ChangePhoneNumberCodeTag.input, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: title, textColor: .black), text: text, placeholder: "", type: .number, spacing: 10.0, tag: ChangePhoneNumberCodeTag.input, sectionId: self.section, textUpdated: { updatedText in arguments.updateEntryText(updatedText) }, action: { arguments.next() }) case let .codeInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -184,7 +184,7 @@ private final class ChangePhoneNumberCodeControllerImpl: ItemListController, Cha self.applyCodeImpl = applyCodeImpl let presentationData = context.sharedContext.currentPresentationData.with { $0 } - super.init(theme: presentationData.theme, strings: presentationData.strings, updatedPresentationData: context.sharedContext.presentationData |> map { ($0.theme, $0.strings) }, state: state, tabBarItem: nil) + super.init(presentationData: ItemListPresentationData(presentationData), updatedPresentationData: context.sharedContext.presentationData |> map(ItemListPresentationData.init(_:)), state: state, tabBarItem: nil) } required init(coder aDecoder: NSCoder) { @@ -313,8 +313,8 @@ func changePhoneNumberCodeController(context: AccountContext, phoneNumber: Strin }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(formatPhoneNumber(phoneNumber)), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: changePhoneNumberCodeControllerEntries(presentationData: presentationData, state: state, codeData: data, timeout: timeout, strings: presentationData.strings), style: .blocks, focusItemTag: ChangePhoneNumberCodeTag.input, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(formatPhoneNumber(phoneNumber)), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: changePhoneNumberCodeControllerEntries(presentationData: presentationData, state: state, codeData: data, timeout: timeout, strings: presentationData.strings), style: .blocks, focusItemTag: ChangePhoneNumberCodeTag.input, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift b/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift index 6d5e408235..bfaa279cff 100644 --- a/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift +++ b/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift @@ -84,7 +84,7 @@ final class ChangePhoneNumberController: ViewController { } } strongSelf.controllerNode.view.endEditing(true) - strongSelf.present(controller, in: .window(.root)) + strongSelf.push(controller) } } } diff --git a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadConnectionTypeController.swift b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadConnectionTypeController.swift index 39b3c1775c..a3405f6639 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadConnectionTypeController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadConnectionTypeController.swift @@ -142,35 +142,35 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! AutodownloadMediaConnectionTypeControllerArguments switch self { case let .master(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleMaster(value) }) case let .dataUsageHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .dataUsageItem(theme, strings, value, customPosition, enabled): return AutodownloadDataUsagePickerItem(theme: theme, strings: strings, value: value, customPosition: customPosition, enabled: enabled, sectionId: self.section, updated: { preset in arguments.changePreset(preset) }) case let .typesHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .photos(theme, text, value, enabled): - return ItemListDisclosureItem(theme: theme, title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: { arguments.customize(.photo) }) case let .videos(theme, text, value, enabled): - return ItemListDisclosureItem(theme: theme, title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: { arguments.customize(.video) }) case let .files(theme, text, value, enabled): - return ItemListDisclosureItem(theme: theme, title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: { arguments.customize(.file) }) case let .voiceMessagesInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -345,8 +345,8 @@ func autodownloadMediaConnectionTypeController(context: AccountContext, connecti title = presentationData.strings.AutoDownloadSettings_WifiTitle } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: autodownloadMediaConnectionTypeControllerEntries(presentationData: presentationData, connectionType: connectionType, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: autodownloadMediaConnectionTypeControllerEntries(presentationData: presentationData, connectionType: connectionType, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadMediaCategoryController.swift b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadMediaCategoryController.swift index c506cb0b60..222efd9093 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadMediaCategoryController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadMediaCategoryController.swift @@ -170,39 +170,39 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! AutodownloadMediaCategoryControllerArguments switch self { case let .peerHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .peerContacts(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.togglePeer(.contact) }) case let .peerOtherPrivate(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.togglePeer(.otherPrivate) }) case let .peerGroups(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.togglePeer(.group) }) case let .peerChannels(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.togglePeer(.channel) }) case let .sizeHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .sizeItem(theme, decimalSeparator, text, value): return AutodownloadSizeLimitItem(theme: theme, decimalSeparator: decimalSeparator, text: text, value: value, sectionId: self.section, updated: { value in arguments.adjustSize(value) }) case let .sizePreload(theme, text, value, enabled): - return ItemListSwitchItem(theme: theme, title: text, value: value && enabled, enableInteractiveChanges: true, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value && enabled, enableInteractiveChanges: true, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleVideoPreload() }) case let .sizePreloadInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -427,8 +427,8 @@ func autodownloadMediaCategoryController(context: AccountContext, connectionType title = presentationData.strings.AutoDownloadSettings_DocumentsTitle } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: autodownloadMediaCategoryControllerEntries(presentationData: presentationData, connectionType: connectionType, category: category, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: autodownloadMediaCategoryControllerEntries(presentationData: presentationData, connectionType: connectionType, category: category, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } @@ -447,7 +447,7 @@ func autodownloadMediaCategoryController(context: AccountContext, connectionType case .wifi: preset = .high } - let settings = AutodownloadPresetSettings(disabled: false, photoSizeMax: categories.photo.sizeLimit, videoSizeMax: categories.video.sizeLimit, fileSizeMax: categories.file.sizeLimit, preloadLargeVideo: categories.video.predownload, lessDataForPhoneCalls: false) + let settings = AutodownloadPresetSettings(disabled: false, photoSizeMax: categories.photo.sizeLimit, videoSizeMax: categories.video.sizeLimit, fileSizeMax: categories.file.sizeLimit, preloadLargeVideo: categories.video.predownload, lessDataForPhoneCalls: false, videoUploadMaxbitrate: 0) return saveAutodownloadSettings(account: context.account, preset: preset, settings: settings) } return .complete() diff --git a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift index 0df0bedfd0..aebcd76418 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift @@ -26,8 +26,10 @@ private final class DataAndStorageControllerArguments { let toggleAutoplayVideos: (Bool) -> Void let toggleDownloadInBackground: (Bool) -> Void let openBrowserSelection: () -> Void + let openIntents: () -> Void + let toggleEnableSensitiveContent: (Bool) -> Void - init(openStorageUsage: @escaping () -> Void, openNetworkUsage: @escaping () -> Void, openProxy: @escaping () -> Void, openAutomaticDownloadConnectionType: @escaping (AutomaticDownloadConnectionType) -> Void, resetAutomaticDownload: @escaping () -> Void, openVoiceUseLessData: @escaping () -> Void, openSaveIncomingPhotos: @escaping () -> Void, toggleSaveEditedPhotos: @escaping (Bool) -> Void, toggleAutoplayGifs: @escaping (Bool) -> Void, toggleAutoplayVideos: @escaping (Bool) -> Void, toggleDownloadInBackground: @escaping (Bool) -> Void, openBrowserSelection: @escaping () -> Void) { + init(openStorageUsage: @escaping () -> Void, openNetworkUsage: @escaping () -> Void, openProxy: @escaping () -> Void, openAutomaticDownloadConnectionType: @escaping (AutomaticDownloadConnectionType) -> Void, resetAutomaticDownload: @escaping () -> Void, openVoiceUseLessData: @escaping () -> Void, openSaveIncomingPhotos: @escaping () -> Void, toggleSaveEditedPhotos: @escaping (Bool) -> Void, toggleAutoplayGifs: @escaping (Bool) -> Void, toggleAutoplayVideos: @escaping (Bool) -> Void, toggleDownloadInBackground: @escaping (Bool) -> Void, openBrowserSelection: @escaping () -> Void, openIntents: @escaping () -> Void, toggleEnableSensitiveContent: @escaping (Bool) -> Void) { self.openStorageUsage = openStorageUsage self.openNetworkUsage = openNetworkUsage self.openProxy = openProxy @@ -40,6 +42,8 @@ private final class DataAndStorageControllerArguments { self.toggleAutoplayVideos = toggleAutoplayVideos self.toggleDownloadInBackground = toggleDownloadInBackground self.openBrowserSelection = openBrowserSelection + self.openIntents = openIntents + self.toggleEnableSensitiveContent = toggleEnableSensitiveContent } } @@ -50,6 +54,7 @@ private enum DataAndStorageSection: Int32 { case voiceCalls case other case connection + case enableSensitiveContent } enum DataAndStorageEntryTag: ItemListItemTag { @@ -81,6 +86,7 @@ private enum DataAndStorageEntry: ItemListNodeEntry { case voiceCallsHeader(PresentationTheme, String) case useLessVoiceData(PresentationTheme, String, String) case otherHeader(PresentationTheme, String) + case shareSheet(PresentationTheme, String) case saveIncomingPhotos(PresentationTheme, String) case saveEditedPhotos(PresentationTheme, String, Bool) case openLinksIn(PresentationTheme, String, String) @@ -88,6 +94,7 @@ private enum DataAndStorageEntry: ItemListNodeEntry { case downloadInBackgroundInfo(PresentationTheme, String) case connectionHeader(PresentationTheme, String) case connectionProxy(PresentationTheme, String, String) + case enableSensitiveContent(String, Bool) var section: ItemListSectionId { switch self { @@ -99,10 +106,12 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return DataAndStorageSection.autoPlay.rawValue case .voiceCallsHeader, .useLessVoiceData: return DataAndStorageSection.voiceCalls.rawValue - case .otherHeader, .saveIncomingPhotos, .saveEditedPhotos, .openLinksIn, .downloadInBackground, .downloadInBackgroundInfo: + case .otherHeader, .shareSheet, .saveIncomingPhotos, .saveEditedPhotos, .openLinksIn, .downloadInBackground, .downloadInBackgroundInfo: return DataAndStorageSection.other.rawValue case .connectionHeader, .connectionProxy: return DataAndStorageSection.connection.rawValue + case .enableSensitiveContent: + return DataAndStorageSection.enableSensitiveContent.rawValue } } @@ -132,20 +141,24 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return 10 case .otherHeader: return 11 - case .saveIncomingPhotos: + case .shareSheet: return 12 - case .saveEditedPhotos: + case .saveIncomingPhotos: return 13 - case .openLinksIn: + case .saveEditedPhotos: return 14 - case .downloadInBackground: + case .openLinksIn: return 15 - case .downloadInBackgroundInfo: + case .downloadInBackground: return 16 - case .connectionHeader: + case .downloadInBackgroundInfo: return 17 - case .connectionProxy: + case .connectionHeader: return 18 + case .connectionProxy: + return 19 + case .enableSensitiveContent: + return 20 } } @@ -223,6 +236,12 @@ private enum DataAndStorageEntry: ItemListNodeEntry { } else { return false } + case let .shareSheet(lhsTheme, lhsText): + if case let .shareSheet(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .saveIncomingPhotos(lhsTheme, lhsText): if case let .saveIncomingPhotos(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -265,6 +284,12 @@ private enum DataAndStorageEntry: ItemListNodeEntry { } else { return false } + case let .enableSensitiveContent(text, value): + if case .enableSensitiveContent(text, value) = rhs { + return true + } else { + return false + } } } @@ -272,75 +297,83 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! DataAndStorageControllerArguments switch self { case let .storageUsage(theme, text): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openStorageUsage() }) case let .networkUsage(theme, text): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openNetworkUsage() }) case let .automaticDownloadHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .automaticDownloadCellular(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: { arguments.openAutomaticDownloadConnectionType(.cellular) }) case let .automaticDownloadWifi(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: { arguments.openAutomaticDownloadConnectionType(.wifi) }) case let .automaticDownloadReset(theme, text, enabled): - return ItemListActionItem(theme: theme, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { if enabled { arguments.resetAutomaticDownload() } }, tag: DataAndStorageEntryTag.automaticDownloadReset) case let .autoplayHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .autoplayGifs(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleAutoplayGifs(value) }, tag: DataAndStorageEntryTag.autoplayGifs) case let .autoplayVideos(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleAutoplayVideos(value) }, tag: DataAndStorageEntryTag.autoplayVideos) case let .voiceCallsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .useLessVoiceData(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openVoiceUseLessData() }) case let .otherHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .shareSheet(theme, text): + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { + arguments.openIntents() + }) case let .saveIncomingPhotos(theme, text): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openSaveIncomingPhotos() }) case let .saveEditedPhotos(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleSaveEditedPhotos(value) }, tag: DataAndStorageEntryTag.saveEditedPhotos) case let .openLinksIn(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openBrowserSelection() }) case let .downloadInBackground(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleDownloadInBackground(value) }, tag: DataAndStorageEntryTag.downloadInBackground) case let .downloadInBackgroundInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .connectionHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .connectionProxy(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openProxy() }) + case let .enableSensitiveContent(text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleEnableSensitiveContent(value) + }, tag: nil) } } } @@ -431,7 +464,7 @@ private func stringForAutoDownloadSetting(strings: PresentationStrings, decimalS } } -private func dataAndStorageControllerEntries(state: DataAndStorageControllerState, data: DataAndStorageData, presentationData: PresentationData, defaultWebBrowser: String) -> [DataAndStorageEntry] { +private func dataAndStorageControllerEntries(state: DataAndStorageControllerState, data: DataAndStorageData, presentationData: PresentationData, defaultWebBrowser: String, contentSettingsConfiguration: ContentSettingsConfiguration?) -> [DataAndStorageEntry] { var entries: [DataAndStorageEntry] = [] entries.append(.storageUsage(presentationData.theme, presentationData.strings.ChatSettings_Cache)) @@ -453,6 +486,9 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat entries.append(.useLessVoiceData(presentationData.theme, presentationData.strings.CallSettings_UseLessData, stringForUseLessDataSetting(dataSaving, strings: presentationData.strings))) entries.append(.otherHeader(presentationData.theme, presentationData.strings.ChatSettings_Other)) + if #available(iOSApplicationExtension 13.2, iOS 13.2, *) { + entries.append(.shareSheet(presentationData.theme, presentationData.strings.ChatSettings_IntentsSettings)) + } entries.append(.saveIncomingPhotos(presentationData.theme, presentationData.strings.Settings_SaveIncomingPhotos)) entries.append(.saveEditedPhotos(presentationData.theme, presentationData.strings.Settings_SaveEditedPhotos, data.generatedMediaStoreSettings.storeEditedPhotos)) entries.append(.openLinksIn(presentationData.theme, presentationData.strings.ChatSettings_OpenLinksIn, defaultWebBrowser)) @@ -473,6 +509,12 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat entries.append(.connectionHeader(presentationData.theme, presentationData.strings.ChatSettings_ConnectionType_Title.uppercased())) entries.append(.connectionProxy(presentationData.theme, presentationData.strings.SocksProxySetup_Title, proxyValue)) + #if DEBUG + if let contentSettingsConfiguration = contentSettingsConfiguration, contentSettingsConfiguration.canAdjustSensitiveContent { + entries.append(.enableSensitiveContent("Display Sensitive Content", contentSettingsConfiguration.sensitiveContentEnabled)) + } + #endif + return entries } @@ -488,6 +530,15 @@ func dataAndStorageController(context: AccountContext, focusOnItemTag: DataAndSt let cacheUsagePromise = Promise() cacheUsagePromise.set(cacheUsageStats(context: context)) + let updateSensitiveContentDisposable = MetaDisposable() + actionsDisposable.add(updateSensitiveContentDisposable) + + let updatedContentSettingsConfiguration = contentSettingsConfiguration(network: context.account.network) + |> map(Optional.init) + let contentSettingsConfiguration = Promise() + contentSettingsConfiguration.set(.single(nil) + |> then(updatedContentSettingsConfiguration)) + let dataAndStorageDataPromise = Promise() dataAndStorageDataPromise.set(context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings, ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings, ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings, ApplicationSpecificSharedDataKeys.voiceCallSettings, SharedDataKeys.proxySettings]) |> map { sharedData -> DataAndStorageData in @@ -538,7 +589,7 @@ func dataAndStorageController(context: AccountContext, focusOnItemTag: DataAndSt pushControllerImpl?(autodownloadMediaConnectionTypeController(context: context, connectionType: connectionType)) }, resetAutomaticDownload: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: presentationData.strings.AutoDownloadSettings_ResetHelp), ActionSheetButtonItem(title: presentationData.strings.AutoDownloadSettings_Reset, color: .destructive, action: { [weak actionSheet] in @@ -553,7 +604,7 @@ func dataAndStorageController(context: AccountContext, focusOnItemTag: DataAndSt }).start() }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -587,10 +638,29 @@ func dataAndStorageController(context: AccountContext, focusOnItemTag: DataAndSt }, openBrowserSelection: { let controller = webBrowserSettingsController(context: context) pushControllerImpl?(controller) + }, openIntents: { + let controller = intentsSettingsController(context: context) + pushControllerImpl?(controller) + }, toggleEnableSensitiveContent: { value in + let _ = (contentSettingsConfiguration.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak contentSettingsConfiguration] settings in + if var settings = settings { + settings.sensitiveContentEnabled = value + contentSettingsConfiguration?.set(.single(settings)) + } + }) + updateSensitiveContentDisposable.set(updateRemoteContentSettingsConfiguration(postbox: context.account.postbox, network: context.account.network, sensitiveContentEnabled: value).start()) }) - let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), dataAndStorageDataPromise.get(), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings])) |> deliverOnMainQueue - |> map { presentationData, state, dataAndStorageData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in + let signal = combineLatest(queue: .mainQueue(), + context.sharedContext.presentationData, + statePromise.get(), + dataAndStorageDataPromise.get(), + context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings]), + contentSettingsConfiguration.get() + ) + |> map { presentationData, state, dataAndStorageData, sharedData, contentSettingsConfiguration -> (ItemListControllerState, (ItemListNodeState, Any)) in let webBrowserSettings = (sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings] as? WebBrowserSettings) ?? WebBrowserSettings.defaultSettings let options = availableOpenInOptions(context: context, item: .url(url: "https://telegram.org")) let defaultWebBrowser: String @@ -600,8 +670,8 @@ func dataAndStorageController(context: AccountContext, focusOnItemTag: DataAndSt defaultWebBrowser = presentationData.strings.WebBrowser_InAppSafari } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ChatSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: dataAndStorageControllerEntries(state: state, data: dataAndStorageData, presentationData: presentationData, defaultWebBrowser: defaultWebBrowser), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: dataAndStorageControllerEntries(state: state, data: dataAndStorageData, presentationData: presentationData, defaultWebBrowser: defaultWebBrowser, contentSettingsConfiguration: contentSettingsConfiguration), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/submodules/SettingsUI/Sources/Data and Storage/IntentsSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/IntentsSettingsController.swift new file mode 100644 index 0000000000..42a06eeccb --- /dev/null +++ b/submodules/SettingsUI/Sources/Data and Storage/IntentsSettingsController.swift @@ -0,0 +1,328 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import ItemListPeerItem +import AccountContext +import TelegramIntents +import AccountUtils + +private final class IntentsSettingsControllerArguments { + let context: AccountContext + let updateSettings: (@escaping (IntentsSettings) -> IntentsSettings) -> Void + let resetAll: () -> Void + + init(context: AccountContext, updateSettings: @escaping (@escaping (IntentsSettings) -> IntentsSettings) -> Void, resetAll: @escaping () -> Void) { + self.context = context + self.updateSettings = updateSettings + self.resetAll = resetAll + } +} + +private enum IntentsSettingsSection: Int32 { + case account + case chats + case suggest + case reset +} + +private enum IntentsSettingsControllerEntry: ItemListNodeEntry { + case accountHeader(PresentationTheme, String) + case account(PresentationTheme, Peer, Bool, Int32) + case accountInfo(PresentationTheme, String) + + case chatsHeader(PresentationTheme, String) + case contacts(PresentationTheme, String, Bool) + case savedMessages(PresentationTheme, String, Bool) + case privateChats(PresentationTheme, String, Bool) + case groups(PresentationTheme, String, Bool) + case chatsInfo(PresentationTheme, String) + + case suggestHeader(PresentationTheme, String) + case suggestAll(PresentationTheme, String, Bool) + case suggestOnlyShared(PresentationTheme, String, Bool) + + case resetAll(PresentationTheme, String) + + var section: ItemListSectionId { + switch self { + case .accountHeader, .account, .accountInfo: + return IntentsSettingsSection.account.rawValue + case .chatsHeader, .contacts, .savedMessages, .privateChats, .groups, .chatsInfo: + return IntentsSettingsSection.chats.rawValue + case .suggestHeader, .suggestAll, .suggestOnlyShared: + return IntentsSettingsSection.suggest.rawValue + case .resetAll: + return IntentsSettingsSection.reset.rawValue + } + } + + var stableId: Int32 { + switch self { + case .accountHeader: + return 0 + case let .account(_, _, _, index): + return 1 + index + case .accountInfo: + return 1000 + case .chatsHeader: + return 1001 + case .contacts: + return 1002 + case .savedMessages: + return 1003 + case .privateChats: + return 1004 + case .groups: + return 1005 + case .chatsInfo: + return 1006 + case .suggestHeader: + return 1007 + case .suggestAll: + return 1008 + case .suggestOnlyShared: + return 1009 + case .resetAll: + return 1010 + } + } + + static func ==(lhs: IntentsSettingsControllerEntry, rhs: IntentsSettingsControllerEntry) -> Bool { + switch lhs { + case let .accountHeader(lhsTheme, lhsText): + if case let .accountHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .account(lhsTheme, lhsPeer, lhsSelected, lhsIndex): + if case let .account(rhsTheme, rhsPeer, rhsSelected, rhsIndex) = rhs, lhsTheme === rhsTheme, arePeersEqual(lhsPeer, rhsPeer), lhsSelected == rhsSelected, lhsIndex == rhsIndex { + return true + } else { + return false + } + case let .accountInfo(lhsTheme, lhsText): + if case let .accountInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + + case let .chatsHeader(lhsTheme, lhsText): + if case let .chatsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .contacts(lhsTheme, lhsText, lhsValue): + if case let .contacts(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .savedMessages(lhsTheme, lhsText, lhsValue): + if case let .savedMessages(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .privateChats(lhsTheme, lhsText, lhsValue): + if case let .privateChats(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .groups(lhsTheme, lhsText, lhsValue): + if case let .groups(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .chatsInfo(lhsTheme, lhsText): + if case let .chatsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .suggestHeader(lhsTheme, lhsText): + if case let .suggestHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .suggestAll(lhsTheme, lhsText, lhsValue): + if case let .suggestAll(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .suggestOnlyShared(lhsTheme, lhsText, lhsValue): + if case let .suggestOnlyShared(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .resetAll(lhsTheme, lhsText): + if case let .resetAll(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + } + } + + static func <(lhs: IntentsSettingsControllerEntry, rhs: IntentsSettingsControllerEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! IntentsSettingsControllerArguments + switch self { + case let .accountHeader(theme, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .account(theme, peer, selected, _): + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .dayFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: ""), nameDisplayOrder: .firstLast, context: arguments.context.sharedContext.makeTempAccountContext(account: arguments.context.account), peer: peer, height: .generic, aliasHandling: .standard, nameStyle: .plain, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: false), revealOptions: nil, switchValue: ItemListPeerItemSwitch(value: selected, style: .check), enabled: true, selectable: true, sectionId: self.section, action: { + arguments.updateSettings { $0.withUpdatedAccount(peer.id) } + }, setPeerIdWithRevealedOptions: { _, _ in}, removePeer: { _ in }) + return ItemListTextItem(presentationData: presentationData, text: .plain(""), sectionId: self.section) + case let .accountInfo(theme, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .chatsHeader(theme, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .contacts(theme, text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateSettings { $0.withUpdatedContacts(value) } + }) + case let .savedMessages(theme, text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateSettings { $0.withUpdatedSavedMessages(value) } + }) + case let .privateChats(theme, text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateSettings { $0.withUpdatedPrivateChats(value) } + }) + case let .groups(theme, text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateSettings { $0.withUpdatedGroups(value) } + }) + case let .chatsInfo(theme, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .suggestHeader(theme, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .suggestAll(theme, text, value): + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.updateSettings { $0.withUpdatedOnlyShared(false) } + }) + case let .suggestOnlyShared(theme, text, value): + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.updateSettings { $0.withUpdatedOnlyShared(true) } + }) + + case let .resetAll(theme, text): + return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.resetAll() + }) + } + } +} + +private func intentsSettingsControllerEntries(context: AccountContext, presentationData: PresentationData, settings: IntentsSettings, accounts: [(Account, Peer)]) -> [IntentsSettingsControllerEntry] { + var entries: [IntentsSettingsControllerEntry] = [] + + if accounts.count > 1 { + entries.append(.accountHeader(presentationData.theme, presentationData.strings.IntentsSettings_MainAccount.uppercased())) + var index: Int32 = 0 + for (_, peer) in accounts { + entries.append(.account(presentationData.theme, peer, peer.id == settings.account, index)) + index += 1 + } + entries.append(.accountInfo(presentationData.theme, presentationData.strings.IntentsSettings_MainAccountInfo)) + } + + entries.append(.chatsHeader(presentationData.theme, presentationData.strings.IntentsSettings_SuggestedChats.uppercased())) + entries.append(.contacts(presentationData.theme, presentationData.strings.IntentsSettings_SuggestedChatsContacts, settings.contacts)) + entries.append(.savedMessages(presentationData.theme, presentationData.strings.IntentsSettings_SuggestedChatsSavedMessages, settings.savedMessages)) + entries.append(.privateChats(presentationData.theme, presentationData.strings.IntentsSettings_SuggestedChatsPrivateChats, settings.privateChats)) + entries.append(.groups(presentationData.theme, presentationData.strings.IntentsSettings_SuggestedChatsGroups, settings.groups)) + + entries.append(.chatsInfo(presentationData.theme, presentationData.strings.IntentsSettings_SuggestedAndSpotlightChatsInfo)) + + entries.append(.suggestHeader(presentationData.theme, presentationData.strings.IntentsSettings_SuggestBy.uppercased())) + entries.append(.suggestAll(presentationData.theme, presentationData.strings.IntentsSettings_SuggestByAll, !settings.onlyShared)) + entries.append(.suggestOnlyShared(presentationData.theme, presentationData.strings.IntentsSettings_SuggestByShare, settings.onlyShared)) + + entries.append(.resetAll(presentationData.theme, presentationData.strings.IntentsSettings_ResetAll)) + + return entries +} + +public func intentsSettingsController(context: AccountContext) -> ViewController { + var pushControllerImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController) -> Void)? + + let updateDisposable = MetaDisposable() + let arguments = IntentsSettingsControllerArguments(context: context, updateSettings: { f in + let _ = updateIntentsSettingsInteractively(accountManager: context.sharedContext.accountManager, f).start(next: { previous, updated in + guard let previous = previous, let updated = updated else { + return + } + let accountPeerId = context.account.peerId + if previous.contacts && !updated.contacts { + deleteAllSendMessageIntents() + } + if previous.savedMessages && !updated.savedMessages { + deleteAllSendMessageIntents() + } + if previous.privateChats && !updated.privateChats { + deleteAllSendMessageIntents() + } + if previous.groups && !updated.groups { + deleteAllSendMessageIntents() + } + if previous.account != updated.account, let previousAccount = previous.account { + deleteAllSendMessageIntents() + } + }) + }, resetAll: { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationData: presentationData) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.IntentsSettings_Reset, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + deleteAllSendMessageIntents() + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + presentControllerImpl?(actionSheet) + }) + + let signal = combineLatest(context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.intentsSettings]), activeAccountsAndPeers(context: context, includePrimary: true)) + |> deliverOnMainQueue + |> map { presentationData, sharedData, accounts -> (ItemListControllerState, (ItemListNodeState, Any)) in + let settings = (sharedData.entries[ApplicationSpecificSharedDataKeys.intentsSettings] as? IntentsSettings) ?? IntentsSettings.defaultSettings + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.IntentsSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: intentsSettingsControllerEntries(context: context, presentationData: presentationData, settings: settings, accounts: accounts.1.map { ($0.0, $0.1) }), style: .blocks, animateChanges: false) + + return (controllerState, (listState, arguments)) + } + + let controller = ItemListController(context: context, state: signal) + pushControllerImpl = { [weak controller] c in + (controller?.navigationController as? NavigationController)?.pushViewController(c) + } + presentControllerImpl = { [weak controller] c in + controller?.present(c, in: .window(.root)) + } + return controller +} diff --git a/submodules/SettingsUI/Sources/Data and Storage/NetworkUsageStatsController.swift b/submodules/SettingsUI/Sources/Data and Storage/NetworkUsageStatsController.swift index 841149016d..87744afa82 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/NetworkUsageStatsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/NetworkUsageStatsController.swift @@ -255,51 +255,51 @@ private enum NetworkUsageStatsEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! NetworkUsageStatsControllerArguments switch self { case let .messagesHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .messagesSent(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) case let .messagesReceived(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) case let .imageHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .imageSent(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) case let .imageReceived(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) case let .videoHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .videoSent(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) case let .videoReceived(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) case let .audioHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .audioSent(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) case let .audioReceived(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) case let .fileHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .fileSent(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) case let .fileReceived(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) case let .callHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .callSent(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) case let .callReceived(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) case let .reset(theme, section, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.resetStatistics(section) }) case let .resetTimestamp(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -389,7 +389,7 @@ func networkUsageStatsController(context: AccountContext) -> ViewController { let arguments = NetworkUsageStatsControllerArguments(resetStatistics: { [weak stats] section in let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -416,8 +416,8 @@ func networkUsageStatsController(context: AccountContext) -> ViewController { let signal = combineLatest(context.sharedContext.presentationData, section.get(), stats.get()) |> deliverOnMainQueue |> map { presentationData, section, stats -> (ItemListControllerState, (ItemListNodeState, Any)) in - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .sectionControl([presentationData.strings.NetworkUsageSettings_Cellular, presentationData.strings.NetworkUsageSettings_Wifi], 0), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: networkUsageStatsControllerEntries(presentationData: presentationData, section: section, stats: stats), style: .blocks, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .sectionControl([presentationData.strings.NetworkUsageSettings_Cellular, presentationData.strings.NetworkUsageSettings_Wifi], 0), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: networkUsageStatsControllerEntries(presentationData: presentationData, section: section, stats: stats), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift index 424e0b6206..c0424de212 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift @@ -202,11 +202,11 @@ private enum ProxySettingsControllerEntry: ItemListNodeEntry { } } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ProxySettingsControllerArguments switch self { case let .enabled(theme, text, value, createsNew): - return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: !createsNew, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: !createsNew, enabled: true, sectionId: self.section, style: .blocks, updated: { value in if createsNew { arguments.addNewServer() } else { @@ -214,9 +214,9 @@ private enum ProxySettingsControllerEntry: ItemListNodeEntry { } }) case let .serversHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .addServer(theme, text, _): - return ProxySettingsActionItem(theme: theme, title: text, icon: .add, sectionId: self.section, editing: false, action: { + return ProxySettingsActionItem(presentationData: presentationData, title: text, icon: .add, sectionId: self.section, editing: false, action: { arguments.addNewServer() }) case let .server(_, theme, strings, settings, active, status, editing, enabled): @@ -230,15 +230,15 @@ private enum ProxySettingsControllerEntry: ItemListNodeEntry { arguments.removeServer(settings) }) case let .shareProxyList(theme, text): - return ProxySettingsActionItem(theme: theme, title: text, sectionId: self.section, editing: false, action: { + return ProxySettingsActionItem(presentationData: presentationData, title: text, sectionId: self.section, editing: false, action: { arguments.shareProxyList() }) case let .useForCalls(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleUseForCalls(value) }) case let .useForCallsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -315,10 +315,10 @@ public enum ProxySettingsControllerMode { public func proxySettingsController(context: AccountContext, mode: ProxySettingsControllerMode = .default) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - return proxySettingsController(accountManager: context.sharedContext.accountManager, context: context, postbox: context.account.postbox, network: context.account.network, mode: mode, theme: presentationData.theme, strings: presentationData.strings, updatedPresentationData: context.sharedContext.presentationData |> map { ($0.theme, $0.strings) }) + return proxySettingsController(accountManager: context.sharedContext.accountManager, context: context, postbox: context.account.postbox, network: context.account.network, mode: mode, presentationData: presentationData, updatedPresentationData: context.sharedContext.presentationData) } -public func proxySettingsController(accountManager: AccountManager, context: AccountContext? = nil, postbox: Postbox, network: Network, mode: ProxySettingsControllerMode, theme: PresentationTheme, strings: PresentationStrings, updatedPresentationData: Signal<(theme: PresentationTheme, strings: PresentationStrings), NoError>) -> ViewController { +public func proxySettingsController(accountManager: AccountManager, context: AccountContext? = nil, postbox: Postbox, network: Network, mode: ProxySettingsControllerMode, presentationData: PresentationData, updatedPresentationData: Signal) -> ViewController { var presentControllerImpl: ((ViewController, Any?) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? @@ -347,7 +347,7 @@ public func proxySettingsController(accountManager: AccountManager, context: Acc return current }).start() }, addNewServer: { - pushControllerImpl?(proxyServerSettingsController(theme: theme, strings: strings, updatedPresentationData: updatedPresentationData, accountManager: accountManager, postbox: postbox, network: network, currentSettings: nil)) + pushControllerImpl?(proxyServerSettingsController(presentationData: presentationData, updatedPresentationData: updatedPresentationData, accountManager: accountManager, postbox: postbox, network: network, currentSettings: nil)) }, activateServer: { server in let _ = updateProxySettingsInteractively(accountManager: accountManager, { current in var current = current @@ -360,7 +360,7 @@ public func proxySettingsController(accountManager: AccountManager, context: Acc return current }).start() }, editServer: { server in - pushControllerImpl?(proxyServerSettingsController(theme: theme, strings: strings, updatedPresentationData: updatedPresentationData, accountManager: accountManager, postbox: postbox, network: network, currentSettings: server)) + pushControllerImpl?(proxyServerSettingsController(presentationData: presentationData, updatedPresentationData: updatedPresentationData, accountManager: accountManager, postbox: postbox, network: network, currentSettings: server)) }, removeServer: { server in let _ = updateProxySettingsInteractively(accountManager: accountManager, { current in var current = current @@ -407,10 +407,10 @@ public func proxySettingsController(accountManager: AccountManager, context: Acc }) let signal = combineLatest(updatedPresentationData, statePromise.get(), proxySettings.get(), statusesContext.statuses(), network.connectionStatus) - |> map { themeAndStrings, state, proxySettings, statuses, connectionStatus -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, state, proxySettings, statuses, connectionStatus -> (ItemListControllerState, (ItemListNodeState, Any)) in var leftNavigationButton: ItemListNavigationButton? if case .modal = mode { - leftNavigationButton = ItemListNavigationButton(content: .text(themeAndStrings.strings.Common_Cancel), style: .regular, enabled: true, action: { + leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) } @@ -419,7 +419,7 @@ public func proxySettingsController(accountManager: AccountManager, context: Acc if proxySettings.servers.isEmpty { rightNavigationButton = nil } else if state.editing { - rightNavigationButton = ItemListNavigationButton(content: .text(strings.Common_Done), style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { updateState { state in var state = state state.editing = false @@ -427,7 +427,7 @@ public func proxySettingsController(accountManager: AccountManager, context: Acc } }) } else { - rightNavigationButton = ItemListNavigationButton(content: .text(strings.Common_Edit), style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { updateState { state in var state = state state.editing = true @@ -436,13 +436,13 @@ public func proxySettingsController(accountManager: AccountManager, context: Acc }) } - let controllerState = ItemListControllerState(theme: themeAndStrings.0, title: .text(themeAndStrings.1.SocksProxySetup_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: themeAndStrings.1.Common_Back)) - let listState = ItemListNodeState(entries: proxySettingsControllerEntries(theme: themeAndStrings.0, strings: themeAndStrings.1, state: state, proxySettings: proxySettings, statuses: statuses, connectionStatus: connectionStatus), style: .blocks) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.SocksProxySetup_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: proxySettingsControllerEntries(theme: presentationData.theme, strings: presentationData.strings, state: state, proxySettings: proxySettings, statuses: statuses, connectionStatus: connectionStatus), style: .blocks) return (controllerState, (listState, arguments)) } - let controller = ItemListController(theme: theme, strings: strings, updatedPresentationData: updatedPresentationData, state: signal, tabBarItem: nil) + let controller = ItemListController(presentationData: ItemListPresentationData(presentationData), updatedPresentationData: updatedPresentationData |> map(ItemListPresentationData.init(_:)), state: signal, tabBarItem: nil) controller.navigationPresentation = .modal presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) @@ -453,10 +453,10 @@ public func proxySettingsController(accountManager: AccountManager, context: Acc dismissImpl = { [weak controller] in controller?.dismiss() } - controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [ProxySettingsControllerEntry]) -> Void in + controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [ProxySettingsControllerEntry]) -> Signal in let fromEntry = entries[fromIndex] guard case let .server(_, _, _, fromServer, _, _, _, _) = fromEntry else { - return + return .single(false) } var referenceServer: ProxyServerSettings? var beforeAll = false @@ -476,7 +476,7 @@ public func proxySettingsController(accountManager: AccountManager, context: Acc afterAll = true } - let _ = updateProxySettingsInteractively(accountManager: accountManager, { current in + return updateProxySettingsInteractively(accountManager: accountManager, { current in var current = current if let index = current.servers.firstIndex(of: fromServer) { current.servers.remove(at: index) @@ -503,7 +503,7 @@ public func proxySettingsController(accountManager: AccountManager, context: Acc current.servers.append(fromServer) } return current - }).start() + }) }) shareProxyListImpl = { [weak controller] in diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerActionSheetController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerActionSheetController.swift index aa56df559d..d2d60c646e 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerActionSheetController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerActionSheetController.swift @@ -25,27 +25,27 @@ public final class ProxyServerActionSheetController: ActionSheetController { convenience public init(context: AccountContext, server: ProxyServerSettings) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.init(theme: presentationData.theme, strings: presentationData.strings, accountManager: context.sharedContext.accountManager, postbox: context.account.postbox, network: context.account.network, server: server, presentationData: context.sharedContext.presentationData) + self.init(presentationData: presentationData, accountManager: context.sharedContext.accountManager, postbox: context.account.postbox, network: context.account.network, server: server, updatedPresentationData: context.sharedContext.presentationData) } - public init(theme: PresentationTheme, strings: PresentationStrings, accountManager: AccountManager, postbox: Postbox, network: Network, server: ProxyServerSettings, presentationData: Signal?) { - let sheetTheme = ActionSheetControllerTheme(presentationTheme: theme) + public init(presentationData: PresentationData, accountManager: AccountManager, postbox: Postbox, network: Network, server: ProxyServerSettings, updatedPresentationData: Signal?) { + let sheetTheme = ActionSheetControllerTheme(presentationData: presentationData) super.init(theme: sheetTheme) self._ready.set(.single(true)) var items: [ActionSheetItem] = [] if case .mtp = server.connection { - items.append(ActionSheetTextItem(title: strings.SocksProxySetup_AdNoticeHelp)) + items.append(ActionSheetTextItem(title: presentationData.strings.SocksProxySetup_AdNoticeHelp)) } - items.append(ProxyServerInfoItem(strings: strings, network: network, server: server)) - items.append(ProxyServerActionItem(accountManager:accountManager, postbox: postbox, network: network, presentationTheme: theme, strings: strings, server: server, dismiss: { [weak self] success in + items.append(ProxyServerInfoItem(strings: presentationData.strings, network: network, server: server)) + items.append(ProxyServerActionItem(accountManager:accountManager, postbox: postbox, network: network, presentationData: presentationData, server: server, dismiss: { [weak self] success in guard let strongSelf = self, !strongSelf.isDismissed else { return } strongSelf.isDismissed = true if success { - strongSelf.present(OverlayStatusController(theme: theme, type: .shieldSuccess(strings.SocksProxySetup_ProxyEnabled, false)), in: .window(.root)) + strongSelf.present(OverlayStatusController(theme: presentationData.theme, type: .shieldSuccess(presentationData.strings.SocksProxySetup_ProxyEnabled, false)), in: .window(.root)) } strongSelf.dismissAnimated() }, present: { [weak self] c, a in @@ -54,16 +54,16 @@ public final class ProxyServerActionSheetController: ActionSheetController { self.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { [weak self] in self?.dismissAnimated() }) ]) ]) - if let presentationData = presentationData { - self.presentationDisposable = presentationData.start(next: { [weak self] presentationData in + if let updatedPresentationData = updatedPresentationData { + self.presentationDisposable = updatedPresentationData.start(next: { [weak self] presentationData in if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationTheme: presentationData.theme) + strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) } }) } @@ -97,8 +97,6 @@ private final class ProxyServerInfoItem: ActionSheetItem { } } -private let textFont = Font.regular(16.0) - private enum ProxyServerInfoStatusType { case generic(String) case failed(String) @@ -107,6 +105,7 @@ private enum ProxyServerInfoStatusType { private final class ProxyServerInfoItemNode: ActionSheetItemNode { private let theme: ActionSheetControllerTheme private let strings: PresentationStrings + private let textFont: UIFont private let network: Network private let server: ProxyServerSettings @@ -122,6 +121,8 @@ private final class ProxyServerInfoItemNode: ActionSheetItemNode { self.network = network self.server = server + self.textFont = Font.regular(floor(theme.baseFontSize * 16.0 / 17.0)) + var fieldNodes: [(ImmediateTextNode, ImmediateTextNode)] = [] let serverTitleNode = ImmediateTextNode() serverTitleNode.isUserInteractionEnabled = false @@ -267,25 +268,23 @@ private final class ProxyServerActionItem: ActionSheetItem { private let accountManager: AccountManager private let postbox: Postbox private let network: Network - private let presentationTheme: PresentationTheme - private let strings: PresentationStrings + private let presentationData: PresentationData private let server: ProxyServerSettings private let dismiss: (Bool) -> Void private let present: (ViewController, Any?) -> Void - init(accountManager: AccountManager, postbox: Postbox, network: Network, presentationTheme: PresentationTheme, strings: PresentationStrings, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { + init(accountManager: AccountManager, postbox: Postbox, network: Network, presentationData: PresentationData, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.accountManager = accountManager self.postbox = postbox self.network = network - self.presentationTheme = presentationTheme - self.strings = strings + self.presentationData = presentationData self.server = server self.dismiss = dismiss self.present = present } func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - return ProxyServerActionItemNode(accountManager: self.accountManager, postbox: self.postbox, network: self.network, presentationTheme: self.presentationTheme, theme: theme, strings: self.strings, server: self.server, dismiss: self.dismiss, present: self.present) + return ProxyServerActionItemNode(accountManager: self.accountManager, postbox: self.postbox, network: self.network, presentationData: self.presentationData, theme: theme, server: self.server, dismiss: self.dismiss, present: self.present) } func updateNode(_ node: ActionSheetItemNode) { @@ -296,9 +295,8 @@ private final class ProxyServerActionItemNode: ActionSheetItemNode { private let accountManager: AccountManager private let postbox: Postbox private let network: Network - private let presentationTheme: PresentationTheme + private let presentationData: PresentationData private let theme: ActionSheetControllerTheme - private let strings: PresentationStrings private let server: ProxyServerSettings private let dismiss: (Bool) -> Void private let present: (ViewController, Any?) -> Void @@ -310,21 +308,22 @@ private final class ProxyServerActionItemNode: ActionSheetItemNode { private let disposable = MetaDisposable() private var revertSettings: ProxySettings? - init(accountManager: AccountManager, postbox: Postbox, network: Network, presentationTheme: PresentationTheme, theme: ActionSheetControllerTheme, strings: PresentationStrings, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { + init(accountManager: AccountManager, postbox: Postbox, network: Network, presentationData: PresentationData, theme: ActionSheetControllerTheme, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.accountManager = accountManager self.postbox = postbox self.network = network self.theme = theme - self.presentationTheme = presentationTheme - self.strings = strings + self.presentationData = presentationData self.server = server self.dismiss = dismiss self.present = present + let titleFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) + self.titleNode = ImmediateTextNode() self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false - self.titleNode.attributedText = NSAttributedString(string: strings.SocksProxySetup_ConnectAndSave, font: Font.regular(20.0), textColor: theme.controlAccentColor) + self.titleNode.attributedText = NSAttributedString(string: presentationData.strings.SocksProxySetup_ConnectAndSave, font: titleFont, textColor: theme.controlAccentColor) self.activityIndicator = ActivityIndicator(type: .custom(theme.controlAccentColor, 22.0, 1.5, false)) self.activityIndicator.isHidden = true @@ -401,7 +400,7 @@ private final class ProxyServerActionItemNode: ActionSheetItemNode { if let strongSelf = self { strongSelf.revertSettings = previousSettings strongSelf.buttonNode.isUserInteractionEnabled = false - strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.strings.SocksProxySetup_Connecting, font: Font.regular(20.0), textColor: strongSelf.theme.primaryTextColor) + strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.SocksProxySetup_Connecting, font: Font.regular(20.0), textColor: strongSelf.theme.primaryTextColor) strongSelf.activityIndicator.isHidden = false strongSelf.setNeedsLayout() @@ -433,11 +432,11 @@ private final class ProxyServerActionItemNode: ActionSheetItemNode { let _ = updateProxySettingsInteractively(accountManager: strongSelf.accountManager, { _ in return previousSettings }) - strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.strings.SocksProxySetup_ConnectAndSave, font: Font.regular(20.0), textColor: strongSelf.theme.controlAccentColor) + strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.SocksProxySetup_ConnectAndSave, font: Font.regular(20.0), textColor: strongSelf.theme.controlAccentColor) strongSelf.buttonNode.isUserInteractionEnabled = true strongSelf.setNeedsLayout() - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationTheme), title: nil, text: strongSelf.strings.SocksProxySetup_FailedToConnect, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), nil) + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.SocksProxySetup_FailedToConnect, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) } } })) diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift index 54b3fcaa95..68dcaa32c1 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift @@ -118,17 +118,17 @@ private enum ProxySettingsEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! proxyServerSettingsControllerArguments switch self { case let .usePasteboardSettings(theme, title): - return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.usePasteboardSettings() }) case let .usePasteboardInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .modeSocks5(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateState { state in var state = state state.mode = .socks5 @@ -136,7 +136,7 @@ private enum ProxySettingsEntry: ItemListNodeEntry { } }) case let .modeMtp(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateState { state in var state = state state.mode = .mtp @@ -144,9 +144,9 @@ private enum ProxySettingsEntry: ItemListNodeEntry { } }) case let .connectionHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .connectionServer(theme, strings, placeholder, text): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), sectionId: self.section, textUpdated: { value in arguments.updateState { current in var state = current state.host = value @@ -154,7 +154,7 @@ private enum ProxySettingsEntry: ItemListNodeEntry { } }, action: {}) case let .connectionPort(theme, strings, placeholder, text): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(), text: text, placeholder: placeholder, type: .number, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: text, placeholder: placeholder, type: .number, sectionId: self.section, textUpdated: { value in arguments.updateState { current in var state = current state.port = value @@ -162,9 +162,9 @@ private enum ProxySettingsEntry: ItemListNodeEntry { } }, action: {}) case let .credentialsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .credentialsUsername(theme, strings, placeholder, text): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(), text: text, placeholder: placeholder, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: text, placeholder: placeholder, sectionId: self.section, textUpdated: { value in arguments.updateState { current in var state = current state.username = value @@ -172,7 +172,7 @@ private enum ProxySettingsEntry: ItemListNodeEntry { } }, action: {}) case let .credentialsPassword(theme, strings, placeholder, text): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(), text: text, placeholder: placeholder, type: .password, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: text, placeholder: placeholder, type: .password, sectionId: self.section, textUpdated: { value in arguments.updateState { current in var state = current state.password = value @@ -180,7 +180,7 @@ private enum ProxySettingsEntry: ItemListNodeEntry { } }, action: {}) case let .credentialsSecret(theme, strings, placeholder, text): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), sectionId: self.section, textUpdated: { value in arguments.updateState { current in var state = current state.secret = value @@ -188,7 +188,7 @@ private enum ProxySettingsEntry: ItemListNodeEntry { } }, action: {}) case let .share(theme, text, enabled): - return ItemListActionItem(theme: theme, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.share() }) } @@ -225,7 +225,7 @@ private struct ProxyServerSettingsControllerState: Equatable { } } -private func proxyServerSettingsControllerEntries(presentationData: (theme: PresentationTheme, strings: PresentationStrings), state: ProxyServerSettingsControllerState, pasteboardSettings: ProxyServerSettings?) -> [ProxySettingsEntry] { +private func proxyServerSettingsControllerEntries(presentationData: PresentationData, state: ProxyServerSettingsControllerState, pasteboardSettings: ProxyServerSettings?) -> [ProxySettingsEntry] { var entries: [ProxySettingsEntry] = [] if let _ = pasteboardSettings { @@ -271,10 +271,10 @@ private func proxyServerSettings(with state: ProxyServerSettingsControllerState) public func proxyServerSettingsController(context: AccountContext, currentSettings: ProxyServerSettings? = nil) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - return proxyServerSettingsController(context: context, theme: presentationData.theme, strings: presentationData.strings, updatedPresentationData: context.sharedContext.presentationData |> map { ($0.theme, $0.strings) }, accountManager: context.sharedContext.accountManager, postbox: context.account.postbox, network: context.account.network, currentSettings: currentSettings) + return proxyServerSettingsController(context: context, presentationData: presentationData, updatedPresentationData: context.sharedContext.presentationData, accountManager: context.sharedContext.accountManager, postbox: context.account.postbox, network: context.account.network, currentSettings: currentSettings) } -func proxyServerSettingsController(context: AccountContext? = nil, theme: PresentationTheme, strings: PresentationStrings, updatedPresentationData: Signal<(theme: PresentationTheme, strings: PresentationStrings), NoError>, accountManager: AccountManager, postbox: Postbox, network: Network, currentSettings: ProxyServerSettings?) -> ViewController { +func proxyServerSettingsController(context: AccountContext? = nil, presentationData: PresentationData, updatedPresentationData: Signal, accountManager: AccountManager, postbox: Postbox, network: Network, currentSettings: ProxyServerSettings?) -> ViewController { var currentMode: ProxyServerSettingsControllerMode = .socks5 var currentUsername: String? var currentPassword: String? @@ -366,13 +366,13 @@ func proxyServerSettingsController(context: AccountContext? = nil, theme: Presen } }) - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.SocksProxySetup_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: proxyServerSettingsControllerEntries(presentationData: presentationData, state: state, pasteboardSettings: pasteboardSettings), style: .blocks, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.SocksProxySetup_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: proxyServerSettingsControllerEntries(presentationData: presentationData, state: state, pasteboardSettings: pasteboardSettings), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } - let controller = ItemListController(theme: theme, strings: strings, updatedPresentationData: updatedPresentationData, state: signal, tabBarItem: nil) + let controller = ItemListController(presentationData: ItemListPresentationData(presentationData), updatedPresentationData: updatedPresentationData |> map(ItemListPresentationData.init(_:)), state: signal, tabBarItem: nil) controller.navigationPresentation = .modal presentControllerImpl = { [weak controller] c, d in controller?.present(c, in: .window(.root), with: d) @@ -389,7 +389,7 @@ func proxyServerSettingsController(context: AccountContext? = nil, theme: Presen let link = shareLink(for: server) controller?.view.endEditing(true) if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { - let controller = ShareProxyServerActionSheetController(theme: theme, strings: strings, updatedPresentationData: updatedPresentationData, link: link) + let controller = ShareProxyServerActionSheetController(presentationData: presentationData, updatedPresentationData: updatedPresentationData, link: link) presentControllerImpl?(controller, nil) } else if let context = context { let controller = ShareController(context: context, subject: .url(link), preferredAction: .default, showInChat: nil, externalShare: true, immediateExternalShare: true, switchableAccounts: []) diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsActionItem.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsActionItem.swift index abdf20b086..6f7f78f52f 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsActionItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsActionItem.swift @@ -13,15 +13,15 @@ enum ProxySettingsActionIcon { } final class ProxySettingsActionItem: ListViewItem, ItemListItem { - let theme: PresentationTheme + let presentationData: ItemListPresentationData let title: String let icon: ProxySettingsActionIcon let editing: Bool let sectionId: ItemListSectionId let action: () -> Void - init(theme: PresentationTheme, title: String, icon: ProxySettingsActionIcon = .none, sectionId: ItemListSectionId, editing: Bool, action: @escaping () -> Void) { - self.theme = theme + init(presentationData: ItemListPresentationData, title: String, icon: ProxySettingsActionIcon = .none, sectionId: ItemListSectionId, editing: Bool, action: @escaping () -> Void) { + self.presentationData = presentationData self.title = title self.icon = icon self.editing = editing @@ -75,8 +75,6 @@ final class ProxySettingsActionItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(17.0) - private final class ProxySettingsActionItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -130,24 +128,26 @@ private final class ProxySettingsActionItemNode: ListViewItemNode { return { item, params, neighbors in var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } let leftInset: CGFloat = (item.icon != .none ? 50.0 : 16.0) + params.leftInset let editingOffset: CGFloat = (item.editing ? 38.0 : 0.0) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - editingOffset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - editingOffset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let separatorHeight = UIScreenPixel let insets = itemListNeighborsGroupedInsets(neighbors) - let contentSize = CGSize(width: params.width, height: 44.0) + let contentSize = CGSize(width: params.width, height: 22.0 + titleLayout.size.height) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size - let icon = item.icon == .add ? PresentationResourcesItemList.plusIconImage(item.theme) : nil + let icon = item.icon == .add ? PresentationResourcesItemList.plusIconImage(item.presentationData.theme) : nil return (layout, { [weak self] animated in if let strongSelf = self { @@ -156,10 +156,10 @@ private final class ProxySettingsActionItemNode: ListViewItemNode { strongSelf.accessibilityLabel = item.title if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } let _ = titleApply() @@ -213,7 +213,7 @@ private final class ProxySettingsActionItemNode: ListViewItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) @@ -222,7 +222,7 @@ private final class ProxySettingsActionItemNode: ListViewItemNode { transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset, y: 11.0), size: titleLayout.size)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel)) } }) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsServerItem.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsServerItem.swift index 3cbecc17e9..5d7e756311 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsServerItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsServerItem.swift @@ -240,20 +240,20 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode { titleAttributedString.append(NSAttributedString(string: ":\(item.server.port)", font: titleFont, textColor: item.theme.list.itemSecondaryTextColor)) let statusAttributedString = NSAttributedString(string: item.label, font: statusFont, textColor: item.labelAccent ? item.theme.list.itemAccentColor : item.theme.list.itemSecondaryTextColor) - var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? - var reorderControlSizeAndApply: (CGSize, (Bool) -> ItemListEditableReorderControlNode)? + var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)? + var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)? let editingOffset: CGFloat var reorderInset: CGFloat = 0.0 if item.editing.editing { - let sizeAndApply = editableControlLayout(64.0, item.theme, false) + let sizeAndApply = editableControlLayout(item.theme, false) editableControlSizeAndApply = sizeAndApply - editingOffset = sizeAndApply.0.width + editingOffset = sizeAndApply.0 - let reorderSizeAndApply = reorderControlLayout(65.0, item.theme) + let reorderSizeAndApply = reorderControlLayout(item.theme) reorderControlSizeAndApply = reorderSizeAndApply - reorderInset = reorderSizeAndApply.0.width + reorderInset = reorderSizeAndApply.0 } else { editingOffset = 0.0 } @@ -310,9 +310,9 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode { } if let editableControlSizeAndApply = editableControlSizeAndApply { - let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: editableControlSizeAndApply.0) + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height)) if strongSelf.editableControlNode == nil { - let editableControlNode = editableControlSizeAndApply.1() + let editableControlNode = editableControlSizeAndApply.1(layout.contentSize.height) editableControlNode.tapped = { if let strongSelf = self { strongSelf.setRevealOptionsOpened(true, animated: true) @@ -341,13 +341,13 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode { if let reorderControlSizeAndApply = reorderControlSizeAndApply { if strongSelf.reorderControlNode == nil { - let reorderControlNode = reorderControlSizeAndApply.1(false) + let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate) strongSelf.reorderControlNode = reorderControlNode strongSelf.addSubnode(reorderControlNode) reorderControlNode.alpha = 0.0 transition.updateAlpha(node: reorderControlNode, alpha: 1.0) } - let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0.width, y: 0.0), size: reorderControlSizeAndApply.0) + let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0, y: 0.0), size: CGSize(width: reorderControlSizeAndApply.0, height: layout.contentSize.height)) strongSelf.reorderControlNode?.frame = reorderControlFrame } else if let reorderControlNode = strongSelf.reorderControlNode { strongSelf.reorderControlNode = nil diff --git a/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift b/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift index f0874558e7..695c8f7ddb 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift @@ -60,25 +60,25 @@ private enum SaveIncomingMediaEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! SaveIncomingMediaControllerArguments switch self { case let .header(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .contacts(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.toggle(.contact) }) case let .otherPrivate(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.toggle(.otherPrivate) }) case let .groups(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.toggle(.group) }) case let .channels(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.toggle(.channel) }) } @@ -125,8 +125,8 @@ func saveIncomingMediaController(context: AccountContext) -> ViewController { automaticMediaDownloadSettings = MediaAutoDownloadSettings.defaultSettings } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.SaveIncomingPhotosSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: saveIncomingMediaControllerEntries(presentationData: presentationData, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.SaveIncomingPhotosSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: saveIncomingMediaControllerEntries(presentationData: presentationData, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/ShareProxyServerActionSheetController.swift b/submodules/SettingsUI/Sources/Data and Storage/ShareProxyServerActionSheetController.swift index e05db4b554..889ec22ec2 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ShareProxyServerActionSheetController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ShareProxyServerActionSheetController.swift @@ -21,8 +21,8 @@ public final class ShareProxyServerActionSheetController: ActionSheetController private var isDismissed: Bool = false - public init(theme: PresentationTheme, strings: PresentationStrings, updatedPresentationData: Signal<(theme: PresentationTheme, strings: PresentationStrings), NoError>, link: String) { - let sheetTheme = ActionSheetControllerTheme(presentationTheme: theme) + public init(presentationData: PresentationData, updatedPresentationData: Signal, link: String) { + let sheetTheme = ActionSheetControllerTheme(presentationData: presentationData) super.init(theme: sheetTheme) let presentActivityController: (Any) -> Void = { [weak self] item in @@ -35,10 +35,10 @@ public final class ShareProxyServerActionSheetController: ActionSheetController } var items: [ActionSheetItem] = [] - items.append(ProxyServerQRCodeItem(strings: strings, link: link, ready: { [weak self] in + items.append(ProxyServerQRCodeItem(strings: presentationData.strings, link: link, ready: { [weak self] in self?._ready.set(.single(true)) })) - items.append(ActionSheetButtonItem(title: strings.SocksProxySetup_ShareQRCode, action: { [weak self] in + items.append(ActionSheetButtonItem(title: presentationData.strings.SocksProxySetup_ShareQRCode, action: { [weak self] in self?.dismissAnimated() let _ = (qrCode(string: link, color: .black, backgroundColor: .white, icon: .proxy) |> map { _, generator -> UIImage? in @@ -52,22 +52,22 @@ public final class ShareProxyServerActionSheetController: ActionSheetController } }) })) - items.append(ActionSheetButtonItem(title: strings.SocksProxySetup_ShareLink, action: { [weak self] in + items.append(ActionSheetButtonItem(title: presentationData.strings.SocksProxySetup_ShareLink, action: { [weak self] in self?.dismissAnimated() presentActivityController(link) })) self.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { [weak self] in self?.dismissAnimated() }) ]) ]) - self.presentationDisposable = updatedPresentationData.start(next: { [weak self] theme, strings in + self.presentationDisposable = updatedPresentationData.start(next: { [weak self] presentationData in if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationTheme: theme) + strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) } }) } @@ -119,13 +119,15 @@ private final class ProxyServerQRCodeItemNode: ActionSheetItemNode { self.link = link self.ready = ready + let textFont = Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)) + self.label = ASTextNode() self.label.isUserInteractionEnabled = false self.label.maximumNumberOfLines = 0 self.label.displaysAsynchronously = false self.label.truncationMode = .byTruncatingTail self.label.isUserInteractionEnabled = false - self.label.attributedText = NSAttributedString(string: strings.SocksProxySetup_ShareQRCodeInfo, font: ActionSheetTextNode.defaultFont, textColor: self.theme.secondaryTextColor, paragraphAlignment: .center) + self.label.attributedText = NSAttributedString(string: strings.SocksProxySetup_ShareQRCodeInfo, font: textFont, textColor: self.theme.secondaryTextColor, paragraphAlignment: .center) self.imageNode = TransformImageNode() self.imageNode.clipsToBounds = true diff --git a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift index 02736f64b7..223946be96 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift @@ -35,15 +35,15 @@ private func freeDiskSpace() -> Int64 { } private final class StorageUsageControllerArguments { - let account: Account + let context: AccountContext let updateKeepMediaTimeout: (Int32) -> Void let openClearAll: () -> Void let openPeerMedia: (PeerId) -> Void let clearPeerMedia: (PeerId) -> Void let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void - init(account: Account, updateKeepMediaTimeout: @escaping (Int32) -> Void, openClearAll: @escaping () -> Void, openPeerMedia: @escaping (PeerId) -> Void, clearPeerMedia: @escaping (PeerId) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void) { - self.account = account + init(context: AccountContext, updateKeepMediaTimeout: @escaping (Int32) -> Void, openClearAll: @escaping () -> Void, openPeerMedia: @escaping (PeerId) -> Void, clearPeerMedia: @escaping (PeerId) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void) { + self.context = context self.updateKeepMediaTimeout = updateKeepMediaTimeout self.openClearAll = openClearAll self.openPeerMedia = openPeerMedia @@ -195,36 +195,36 @@ private enum StorageUsageEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! StorageUsageControllerArguments switch self { case let .keepMediaHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .keepMedia(theme, strings, value): return KeepMediaDurationPickerItem(theme: theme, strings: strings, value: value, sectionId: self.section, updated: { updatedValue in arguments.updateKeepMediaTimeout(updatedValue) }) case let .keepMediaInfo(theme, text): - return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .storageHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .storageUsage(theme, dateTimeFormat, categories): return StorageUsageItem(theme: theme, dateTimeFormat: dateTimeFormat, categories: categories, sectionId: self.section) case let .collecting(theme, text): return CalculatingCacheSizeItem(theme: theme, title: text, sectionId: self.section, style: .blocks) case let .clearAll(theme, text, enabled): - return ItemListActionItem(theme: theme, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { if enabled { arguments.openClearAll() } }) case let .peersHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .peer(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer, chatPeer, value, revealed): var options: [ItemListPeerItemRevealOption] = [ItemListPeerItemRevealOption(type: .destructive, title: strings.ClearCache_Clear, action: { arguments.clearPeerMedia(peer.id) })] - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, aliasHandling: .threatSelfAsSaved, nameColor: chatPeer == nil ? .primary : .secret, presence: nil, text: .none, label: .disclosure(value), editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: revealed), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer, aliasHandling: .threatSelfAsSaved, nameColor: chatPeer == nil ? .primary : .secret, presence: nil, text: .none, label: .disclosure(value), editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: revealed), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { let resolvedPeer = chatPeer ?? peer arguments.openPeerMedia(resolvedPeer.id) }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in @@ -392,7 +392,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P let clearDisposable = MetaDisposable() actionDisposables.add(clearDisposable) - let arguments = StorageUsageControllerArguments(account: context.account, updateKeepMediaTimeout: { value in + let arguments = StorageUsageControllerArguments(context: context, updateKeepMediaTimeout: { value in let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in return current.withUpdatedDefaultCacheStorageTimeout(value) }).start() @@ -402,7 +402,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P |> deliverOnMainQueue).start(next: { [weak statsPromise] result in if let result = result, case let .result(stats) = result { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -588,7 +588,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P clearDisposable.set((signal |> deliverOnMainQueue).start(completed: { statsPromise.set(.single(.result(resultStats))) - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", stringForDeviceType()).0), elevatedLayout: false, action: { _ in }), .current, nil) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", stringForDeviceType()).0), elevatedLayout: false, action: { _ in return false }), .current, nil) })) } @@ -624,7 +624,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -769,7 +769,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P clearDisposable.set((signal |> deliverOnMainQueue).start(completed: { statsPromise.set(.single(.result(resultStats))) - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", stringForDeviceType()).0), elevatedLayout: false, action: { _ in }), .current, nil) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", stringForDeviceType()).0), elevatedLayout: false, action: { _ in return false }), .current, nil) })) } @@ -896,7 +896,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P clearDisposable.set((signal |> deliverOnMainQueue).start(completed: { statsPromise.set(.single(.result(resultStats))) - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(totalSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", stringForDeviceType()).0), elevatedLayout: false, action: { _ in }), .current, nil) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(totalSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", stringForDeviceType()).0), elevatedLayout: false, action: { _ in return false }), .current, nil) })) } } @@ -924,8 +924,8 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P dismissImpl?() }) : nil - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Cache_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: storageUsageControllerEntries(presentationData: presentationData, cacheSettings: cacheSettings, cacheStats: cacheStats, state: state), style: .blocks, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Cache_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: storageUsageControllerEntries(presentationData: presentationData, cacheSettings: cacheSettings, cacheStats: cacheStats, state: state), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/submodules/SettingsUI/Sources/Data and Storage/VoiceCallDataSavingController.swift b/submodules/SettingsUI/Sources/Data and Storage/VoiceCallDataSavingController.swift index 2fc320cfbb..c773aefaef 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/VoiceCallDataSavingController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/VoiceCallDataSavingController.swift @@ -79,23 +79,23 @@ private enum VoiceCallDataSavingEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! VoiceCallDataSavingControllerArguments switch self { case let .never(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateSelection(.never) }) case let .cellular(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateSelection(.cellular) }) case let .always(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateSelection(.always) }) case let .info(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -155,8 +155,8 @@ func voiceCallDataSavingController(context: AccountContext) -> ViewController { let dataSaving = effectiveDataSaving(for: sharedSettings.0, autodownloadSettings: sharedSettings.1) - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.CallSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: voiceCallDataSavingControllerEntries(presentationData: presentationData, dataSaving: dataSaving), style: .blocks, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.CallSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: voiceCallDataSavingControllerEntries(presentationData: presentationData, dataSaving: dataSaving), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift index 65fb2750a9..4c07fd94de 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift @@ -11,16 +11,16 @@ import OpenInExternalAppUI class WebBrowserItem: ListViewItem, ItemListItem { let account: Account - let theme: PresentationTheme + let presentationData: ItemListPresentationData let title: String let application: OpenInApplication let checked: Bool public let sectionId: ItemListSectionId let action: () -> Void - public init(account: Account, theme: PresentationTheme, title: String, application: OpenInApplication, checked: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { + public init(account: Account, presentationData: ItemListPresentationData, title: String, application: OpenInApplication, checked: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { self.account = account - self.theme = theme + self.presentationData = presentationData self.title = title self.application = application self.checked = checked @@ -69,8 +69,6 @@ class WebBrowserItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(17.0) - private final class WebBrowserItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -138,7 +136,7 @@ private final class WebBrowserItemNode: ListViewItemNode { var leftInset: CGFloat = params.leftInset + 16.0 + 43.0 let iconSize = CGSize(width: 29.0, height: 29.0) - let arguments = TransformImageArguments(corners: ImageCorners(radius: 5.0), imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor) + let arguments = TransformImageArguments(corners: ImageCorners(radius: 5.0), imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.presentationData.theme.list.mediaPlaceholderColor) let imageApply = makeIconLayout(arguments) var updatedIconSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? @@ -157,24 +155,23 @@ private final class WebBrowserItemNode: ListViewItemNode { } } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let separatorHeight = UIScreenPixel let insets = itemListNeighborsGroupedInsets(neighbors) - let contentSize = CGSize(width: params.width, height: 44.0) + let contentSize = CGSize(width: params.width, height: 22.0 + titleLayout.size.height) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) var updateCheckImage: UIImage? var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme - } - - if currentItem?.theme !== item.theme { - updateCheckImage = PresentationResourcesItemList.checkIconImage(item.theme) + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + updateCheckImage = PresentationResourcesItemList.checkIconImage(item.presentationData.theme) } return (layout, { [weak self] in @@ -195,10 +192,10 @@ private final class WebBrowserItemNode: ListViewItemNode { } if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } let _ = titleApply() @@ -246,7 +243,7 @@ private final class WebBrowserItemNode: ListViewItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift index 8ab63004f7..5b7a1d76c6 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift @@ -65,13 +65,13 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! WebBrowserSettingsControllerArguments switch self { case let .browserHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .browser(theme, title, application, identifier, selected, _): - return WebBrowserItem(account: arguments.context.account, theme: theme, title: title, application: application, checked: selected, sectionId: self.section) { + return WebBrowserItem(account: arguments.context.account, presentationData: presentationData, title: title, application: application, checked: selected, sectionId: self.section) { arguments.updateDefaultBrowser(identifier) } } @@ -109,8 +109,8 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl |> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in let settings = (sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings] as? WebBrowserSettings) ?? WebBrowserSettings.defaultSettings - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.WebBrowser_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: webBrowserSettingsControllerEntries(context: context, presentationData: presentationData, selectedBrowser: settings.defaultWebBrowser), style: .blocks, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.WebBrowser_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: webBrowserSettingsControllerEntries(context: context, presentationData: presentationData, selectedBrowser: settings.defaultWebBrowser), style: .blocks, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/DebugAccountsController.swift b/submodules/SettingsUI/Sources/DebugAccountsController.swift index 1e525ff0a5..ccc2dc17fb 100644 --- a/submodules/SettingsUI/Sources/DebugAccountsController.swift +++ b/submodules/SettingsUI/Sources/DebugAccountsController.swift @@ -73,15 +73,15 @@ private enum DebugAccountsControllerEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! DebugAccountsControllerArguments switch self { case let .record(theme, record, current): - return ItemListCheckboxItem(theme: theme, title: "\(UInt64(bitPattern: record.id.int64))", style: .left, checked: current, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: "\(UInt64(bitPattern: record.id.int64))", style: .left, checked: current, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.switchAccount(record.id) }) case let .loginNewAccount(theme): - return ItemListActionItem(theme: theme, title: "Login to another account", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: "Login to another account", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.loginNewAccount() }) } @@ -113,7 +113,7 @@ public func debugAccountsController(context: AccountContext, accountManager: Acc }).start() }, loginNewAccount: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -135,8 +135,8 @@ public func debugAccountsController(context: AccountContext, accountManager: Acc let signal = combineLatest(context.sharedContext.presentationData, accountManager.accountRecords()) |> map { presentationData, view -> (ItemListControllerState, (ItemListNodeState, Any)) in - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Accounts"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: debugAccountsControllerEntries(view: view, presentationData: presentationData), style: .blocks) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Accounts"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: debugAccountsControllerEntries(view: view, presentationData: presentationData), style: .blocks) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/DebugController.swift b/submodules/SettingsUI/Sources/DebugController.swift index 0f4d19d8c4..e33c1130f8 100644 --- a/submodules/SettingsUI/Sources/DebugController.swift +++ b/submodules/SettingsUI/Sources/DebugController.swift @@ -17,7 +17,6 @@ import ItemListUI import PresentationDataUtils import OverlayStatusController import AccountContext -import WalletUI @objc private final class DebugControllerMailComposeDelegate: NSObject, MFMailComposeViewControllerDelegate { public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { @@ -68,11 +67,11 @@ private enum DebugControllerEntry: ItemListNodeEntry { case resetData(PresentationTheme) case resetDatabase(PresentationTheme) case resetHoles(PresentationTheme) + case reindexUnread(PresentationTheme) case resetBiometricsData(PresentationTheme) case optimizeDatabase(PresentationTheme) case photoPreview(PresentationTheme, Bool) case knockoutWallpaper(PresentationTheme, Bool) - case gradientBubbles(PresentationTheme, Bool) case hostInfo(PresentationTheme, String) case versionInfo(PresentationTheme) @@ -86,7 +85,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logging.rawValue case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries: return DebugControllerSection.experiments.rawValue - case .clearTips, .reimport, .resetData, .resetDatabase, .resetHoles, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .gradientBubbles: + case .clearTips, .reimport, .resetData, .resetDatabase, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper: return DebugControllerSection.experiments.rawValue case .hostInfo, .versionInfo: return DebugControllerSection.info.rawValue @@ -129,16 +128,16 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 15 case .resetHoles: return 16 - case .resetBiometricsData: + case .reindexUnread: return 17 + case .resetBiometricsData: + return 18 case .optimizeDatabase: return 20 case .photoPreview: return 21 case .knockoutWallpaper: return 22 - case .gradientBubbles: - return 23 case .hostInfo: return 24 case .versionInfo: @@ -150,15 +149,15 @@ private enum DebugControllerEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! DebugControllerArguments switch self { case let .sendLogs(theme): - return ItemListDisclosureItem(theme: theme, title: "Send Logs", label: "", sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: "Send Logs", label: "", sectionId: self.section, style: .blocks, action: { let _ = (Logger.shared.collectLogs() |> deliverOnMainQueue).start(next: { logs in let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetButtonItem] = [] @@ -197,7 +196,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -205,11 +204,11 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }) case let .sendOneLog(theme): - return ItemListDisclosureItem(theme: theme, title: "Send Latest Log", label: "", sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: "Send Latest Log", label: "", sectionId: self.section, style: .blocks, action: { let _ = (Logger.shared.collectLogs() |> deliverOnMainQueue).start(next: { logs in let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetButtonItem] = [] @@ -251,7 +250,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) @@ -260,7 +259,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }) case let .sendNotificationLogs(theme): - return ItemListDisclosureItem(theme: theme, title: "Send Notification Logs", label: "", sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: "Send Notification Logs", label: "", sectionId: self.section, style: .blocks, action: { let _ = (Logger(basePath: arguments.sharedContext.basePath + "/notificationServiceLogs").collectLogs() |> deliverOnMainQueue).start(next: { logs in guard let context = arguments.context else { @@ -283,11 +282,11 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }) case let .sendCriticalLogs(theme): - return ItemListDisclosureItem(theme: theme, title: "Send Critical Logs", label: "", sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: "Send Critical Logs", label: "", sectionId: self.section, style: .blocks, action: { let _ = (Logger.shared.collectShortLogFiles() |> deliverOnMainQueue).start(next: { logs in let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetButtonItem] = [] @@ -326,7 +325,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { arguments.getRootController()?.present(composeController, animated: true, completion: nil) })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -334,38 +333,38 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }) case let .accounts(theme): - return ItemListDisclosureItem(theme: theme, title: "Accounts", label: "", sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: "Accounts", label: "", sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { return } arguments.pushController(debugAccountsController(context: context, accountManager: arguments.sharedContext.accountManager)) }) case let .logToFile(theme, value): - return ItemListSwitchItem(theme: theme, title: "Log to File", value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: "Log to File", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateLoggingSettings(accountManager: arguments.sharedContext.accountManager, { $0.withUpdatedLogToFile(value) }).start() }) case let .logToConsole(theme, value): - return ItemListSwitchItem(theme: theme, title: "Log to Console", value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: "Log to Console", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateLoggingSettings(accountManager: arguments.sharedContext.accountManager, { $0.withUpdatedLogToConsole(value) }).start() }) case let .redactSensitiveData(theme, value): - return ItemListSwitchItem(theme: theme, title: "Remove Sensitive Data", value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: "Remove Sensitive Data", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateLoggingSettings(accountManager: arguments.sharedContext.accountManager, { $0.withUpdatedRedactSensitiveData(value) }).start() }) case let .enableRaiseToSpeak(theme, value): - return ItemListSwitchItem(theme: theme, title: "Enable Raise to Speak", value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: "Enable Raise to Speak", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateMediaInputSettingsInteractively(accountManager: arguments.sharedContext.accountManager, { $0.withUpdatedEnableRaiseToSpeak(value) }).start() }) case let .keepChatNavigationStack(theme, value): - return ItemListSwitchItem(theme: theme, title: "Keep Chat Stack", value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: "Keep Chat Stack", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in var settings = settings settings.keepChatNavigationStack = value @@ -373,7 +372,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { }).start() }) case let .skipReadHistory(theme, value): - return ItemListSwitchItem(theme: theme, title: "Skip read history", value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: "Skip read history", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in var settings = settings settings.skipReadHistory = value @@ -381,7 +380,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { }).start() }) case let .crashOnSlowQueries(theme, value): - return ItemListSwitchItem(theme: theme, title: "Crash when slow", value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: "Crash when slow", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in var settings = settings settings.crashOnLongQueries = value @@ -389,13 +388,18 @@ private enum DebugControllerEntry: ItemListNodeEntry { }).start() }) case let .clearTips(theme): - return ItemListActionItem(theme: theme, title: "Clear Tips", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: "Clear Tips", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { let _ = (arguments.sharedContext.accountManager.transaction { transaction -> Void in transaction.clearNotices() }).start() + if let context = arguments.context { + let _ = (context.account.postbox.transaction { transaction -> Void in + transaction.clearItemCacheCollection(collectionId: Namespaces.CachedItemCollection.cachedPollResults) + }).start() + } }) case let .reimport(theme): - return ItemListActionItem(theme: theme, title: "Reimport Application Data", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: "Reimport Application Data", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { let appGroupName = "group.\(Bundle.main.bundleIdentifier!)" let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) @@ -410,9 +414,9 @@ private enum DebugControllerEntry: ItemListNodeEntry { } }) case let .resetData(theme): - return ItemListActionItem(theme: theme, title: "Reset Data", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: "Reset Data", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: "All data will be lost."), ActionSheetButtonItem(title: "Reset Data", color: .destructive, action: { [weak actionSheet] in @@ -422,19 +426,19 @@ private enum DebugControllerEntry: ItemListNodeEntry { preconditionFailure() }), ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) arguments.presentController(actionSheet, nil) }) case let .resetDatabase(theme): - return ItemListActionItem(theme: theme, title: "Clear Database", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: "Clear Database", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { return } let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: "All secret chats will be lost."), ActionSheetButtonItem(title: "Clear Database", color: .destructive, action: { [weak actionSheet] in @@ -444,14 +448,14 @@ private enum DebugControllerEntry: ItemListNodeEntry { preconditionFailure() }), ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) arguments.presentController(actionSheet, nil) }) case let .resetHoles(theme): - return ItemListActionItem(theme: theme, title: "Reset Holes", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: "Reset Holes", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { return } @@ -465,14 +469,29 @@ private enum DebugControllerEntry: ItemListNodeEntry { controller.dismiss() }) }) + case let .reindexUnread(theme): + return ItemListActionItem(presentationData: presentationData, title: "Reindex Unread Counters", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + guard let context = arguments.context else { + return + } + let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + arguments.presentController(controller, nil) + let _ = (context.account.postbox.transaction { transaction -> Void in + transaction.reindexUnreadCounters() + } + |> deliverOnMainQueue).start(completed: { + controller.dismiss() + }) + }) case let .resetBiometricsData(theme): - return ItemListActionItem(theme: theme, title: "Reset Biometrics Data", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: "Reset Biometrics Data", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { let _ = updatePresentationPasscodeSettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in return settings.withUpdatedBiometricsDomainState(nil).withUpdatedShareBiometricsDomainState(nil) }).start() }) case let .optimizeDatabase(theme): - return ItemListActionItem(theme: theme, title: "Optimize Database", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: "Optimize Database", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { return } @@ -488,7 +507,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }) case let .photoPreview(theme, value): - return ItemListSwitchItem(theme: theme, title: "Photo Preview", value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: "Photo Preview", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings as? ExperimentalUISettings ?? ExperimentalUISettings.defaultSettings @@ -498,7 +517,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { }).start() }) case let .knockoutWallpaper(theme, value): - return ItemListSwitchItem(theme: theme, title: "Knockout Wallpaper", value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: "Knockout Wallpaper", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings as? ExperimentalUISettings ?? ExperimentalUISettings.defaultSettings @@ -507,24 +526,14 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) - case let .gradientBubbles(theme, value): - return ItemListSwitchItem(theme: theme, title: "Gradient", value: value, sectionId: self.section, style: .blocks, updated: { value in - let _ = arguments.sharedContext.accountManager.transaction ({ transaction in - transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in - var settings = settings as? ExperimentalUISettings ?? ExperimentalUISettings.defaultSettings - settings.gradientBubbles = value - return settings - }) - }).start() - }) case let .hostInfo(theme, string): - return ItemListTextItem(theme: theme, text: .plain(string), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(string), sectionId: self.section) case let .versionInfo(theme): let bundle = Bundle.main let bundleId = bundle.bundleIdentifier ?? "" let bundleVersion = bundle.infoDictionary?["CFBundleShortVersionString"] ?? "" let bundleBuild = bundle.infoDictionary?[kCFBundleVersionKey as String] ?? "" - return ItemListTextItem(theme: theme, text: .plain("\(bundleId)\n\(bundleVersion) (\(bundleBuild))"), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain("\(bundleId)\n\(bundleVersion) (\(bundleBuild))"), sectionId: self.section) } } } @@ -555,10 +564,10 @@ private func debugControllerEntries(presentationData: PresentationData, loggingS entries.append(.resetData(presentationData.theme)) entries.append(.resetDatabase(presentationData.theme)) entries.append(.resetHoles(presentationData.theme)) + entries.append(.reindexUnread(presentationData.theme)) entries.append(.optimizeDatabase(presentationData.theme)) entries.append(.photoPreview(presentationData.theme, experimentalSettings.chatListPhotos)) entries.append(.knockoutWallpaper(presentationData.theme, experimentalSettings.knockoutWallpaper)) - entries.append(.gradientBubbles(presentationData.theme, experimentalSettings.gradientBubbles)) if let backupHostOverride = networkSettings?.backupHostOverride { entries.append(.hostInfo(presentationData.theme, "Host: \(backupHostOverride)")) @@ -626,8 +635,8 @@ public func debugController(sharedContext: SharedAccountContext, context: Accoun }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Debug"), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: debugControllerEntries(presentationData: presentationData, loggingSettings: loggingSettings, mediaInputSettings: mediaInputSettings, experimentalSettings: experimentalSettings, networkSettings: networkSettings, hasLegacyAppData: hasLegacyAppData), style: .blocks) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Debug"), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: debugControllerEntries(presentationData: presentationData, loggingSettings: loggingSettings, mediaInputSettings: mediaInputSettings, experimentalSettings: experimentalSettings, networkSettings: networkSettings, hasLegacyAppData: hasLegacyAppData), style: .blocks) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/EditSettingsController.swift b/submodules/SettingsUI/Sources/EditSettingsController.swift index 5d6a514cb4..38a0100565 100644 --- a/submodules/SettingsUI/Sources/EditSettingsController.swift +++ b/submodules/SettingsUI/Sources/EditSettingsController.swift @@ -191,37 +191,37 @@ private enum SettingsEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! EditSettingsItemArguments switch self { case let .userInfo(theme, strings, dateTimeFormat, peer, cachedData, state, updatingImage): - return ItemListAvatarAndNameInfoItem(account: arguments.context.account, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, mode: .editSettings, 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: { editingName in + return ItemListAvatarAndNameInfoItem(accountContext: arguments.context, presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .editSettings, 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: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { arguments.avatarTapAction() }, context: arguments.avatarAndNameInfoContext, updatingImage: updatingImage) case let .userInfoNotice(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .bioText(theme, currentText, placeholder): - return ItemListMultilineInputItem(theme: theme, text: currentText, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: 70, display: true), sectionId: self.section, style: .blocks, textUpdated: { updatedText in + return ItemListMultilineInputItem(presentationData: presentationData, text: currentText, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: 70, display: true), sectionId: self.section, style: .blocks, textUpdated: { updatedText in arguments.updateBioText(currentText, updatedText) }, tag: EditSettingsEntryTag.bio) case let .bioInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .phoneNumber(theme, text, number): - return ItemListDisclosureItem(theme: theme, title: text, label: number, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: number, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.pushController(ChangePhoneNumberIntroController(context: arguments.context, phoneNumber: number)) }) case let .username(theme, text, address): - return ItemListDisclosureItem(theme: theme, title: text, label: address, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: address, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.pushController(usernameSetupController(context: arguments.context)) }) case let .addAccount(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .center, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .center, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.addAccount() }) case let .logOut(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .center, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .center, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.logout() }) } @@ -438,8 +438,8 @@ func editSettingsController(context: AccountContext, currentName: ItemListAvatar }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.EditProfile_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: editSettingsEntries(presentationData: presentationData, state: state, view: view, canAddAccounts: canAddAccounts), style: .blocks, ensureVisibleItemTag: focusOnItemTag) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.EditProfile_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: editSettingsEntries(presentationData: presentationData, state: state, view: view, canAddAccounts: canAddAccounts), style: .blocks, ensureVisibleItemTag: focusOnItemTag) return (controllerState, (listState, arguments)) } |> afterDisposed { @@ -458,7 +458,7 @@ func editSettingsController(context: AccountContext, currentName: ItemListAvatar } avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { - var result: ((ASDisplayNode, () -> (UIView?, UIView?)), CGRect)? + var result: ((ASDisplayNode, CGRect, () -> (UIView?, UIView?)), CGRect)? controller.forEachItemNode { itemNode in if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { result = itemNode.avatarTransitionNode() diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift index 0f2e2d931f..f2a2a2cbe7 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift @@ -52,10 +52,10 @@ private enum LanguageListEntry: Comparable, Identifiable { return lhs.index() < rhs.index() } - func item(theme: PresentationTheme, strings: PresentationStrings, searchMode: Bool, openSearch: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) -> ListViewItem { + func item(presentationData: PresentationData, searchMode: Bool, openSearch: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) -> ListViewItem { switch self { case let .localization(_, info, type, selected, activity, revealed, editing): - return LocalizationListItem(theme: theme, strings: strings, id: info.languageCode, title: info.title, subtitle: info.localizedTitle, checked: selected, activity: activity, editing: LocalizationListItemEditing(editable: !selected && !searchMode && !info.isOfficial, editing: editing, revealed: !selected && revealed, reorderable: false), sectionId: type == .official ? LanguageListSection.official.rawValue : LanguageListSection.unofficial.rawValue, alwaysPlain: searchMode, action: { + return LocalizationListItem(presentationData: ItemListPresentationData(presentationData), id: info.languageCode, title: info.title, subtitle: info.localizedTitle, checked: selected, activity: activity, editing: LocalizationListItemEditing(editable: !selected && !searchMode && !info.isOfficial, editing: editing, revealed: !selected && revealed, reorderable: false), sectionId: type == .official ? LanguageListSection.official.rawValue : LanguageListSection.unofficial.rawValue, alwaysPlain: searchMode, action: { selectLocalization(info) }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem) } @@ -69,12 +69,12 @@ private struct LocalizationListSearchContainerTransition { let isSearching: Bool } -private func preparedLanguageListSearchContainerTransition(theme: PresentationTheme, strings: PresentationStrings, from fromEntries: [LanguageListEntry], to toEntries: [LanguageListEntry], selectLocalization: @escaping (LocalizationInfo) -> Void, isSearching: Bool, forceUpdate: Bool) -> LocalizationListSearchContainerTransition { +private func preparedLanguageListSearchContainerTransition(presentationData: PresentationData, from fromEntries: [LanguageListEntry], to toEntries: [LanguageListEntry], selectLocalization: @escaping (LocalizationInfo) -> Void, isSearching: Bool, forceUpdate: Bool) -> LocalizationListSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(theme: theme, strings: strings, searchMode: true, openSearch: {}, selectLocalization: selectLocalization, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in }), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(theme: theme, strings: strings, searchMode: true, openSearch: {}, selectLocalization: selectLocalization, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in }), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, selectLocalization: selectLocalization, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in }), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, selectLocalization: selectLocalization, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in }), directionHint: nil) } return LocalizationListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) } @@ -92,12 +92,12 @@ private final class LocalizationListSearchContainerNode: SearchDisplayController private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> + private let presentationDataPromise: Promise init(context: AccountContext, listState: LocalizationListState, selectLocalization: @escaping (LocalizationInfo) -> Void, applyingCode: Signal) { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings)) + self.presentationDataPromise = Promise(self.presentationData) self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) @@ -134,18 +134,18 @@ private final class LocalizationListSearchContainerNode: SearchDisplayController } let previousEntriesHolder = Atomic<([LanguageListEntry], PresentationTheme, PresentationStrings)?>(value: nil) - self.searchDisposable.set(combineLatest(queue: .mainQueue(), foundItems, self.themeAndStringsPromise.get(), applyingCode).start(next: { [weak self] items, themeAndStrings, applyingCode in + self.searchDisposable.set(combineLatest(queue: .mainQueue(), foundItems, self.presentationDataPromise.get(), applyingCode).start(next: { [weak self] items, presentationData, applyingCode in guard let strongSelf = self else { return } var entries: [LanguageListEntry] = [] if let items = items { for item in items { - entries.append(.localization(index: entries.count, info: item, type: .official, selected: themeAndStrings.1.primaryComponent.languageCode == item.languageCode, activity: applyingCode == item.languageCode, revealed: false, editing: false)) + entries.append(.localization(index: entries.count, info: item, type: .official, selected: presentationData.strings.primaryComponent.languageCode == item.languageCode, activity: applyingCode == item.languageCode, revealed: false, editing: false)) } } - let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, themeAndStrings.0, themeAndStrings.1)) - let transition = preparedLanguageListSearchContainerTransition(theme: themeAndStrings.0, strings: themeAndStrings.1, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, selectLocalization: selectLocalization, isSearching: items != nil, forceUpdate: previousEntriesAndPresentationData?.1 !== themeAndStrings.0 || previousEntriesAndPresentationData?.2 !== themeAndStrings.1) + let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings)) + let transition = preparedLanguageListSearchContainerTransition(presentationData: presentationData, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, selectLocalization: selectLocalization, isSearching: items != nil, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings) strongSelf.enqueueTransition(transition) })) @@ -159,7 +159,7 @@ private final class LocalizationListSearchContainerNode: SearchDisplayController if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) - strongSelf.themeAndStringsPromise.set(.single((presentationData.theme, presentationData.strings))) + strongSelf.presentationDataPromise.set(.single(presentationData)) } } }) @@ -223,30 +223,10 @@ private final class LocalizationListSearchContainerNode: SearchDisplayController let topInset = navigationBarHeight transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: nil) - } + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: 0.0, bottom: layout.insets(options: [.input]).bottom, right: 0.0), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: 0.0, bottom: layout.insets(options: [.input]).bottom, right: 0.0), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if !self.hasValidLayout { self.hasValidLayout = true @@ -272,12 +252,12 @@ private struct LanguageListNodeTransition { let animated: Bool } -private func preparedLanguageListNodeTransition(theme: PresentationTheme, strings: PresentationStrings, from fromEntries: [LanguageListEntry], to toEntries: [LanguageListEntry], openSearch: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void, firstTime: Bool, isLoading: Bool, forceUpdate: Bool, animated: Bool) -> LanguageListNodeTransition { +private func preparedLanguageListNodeTransition(presentationData: PresentationData, from fromEntries: [LanguageListEntry], to toEntries: [LanguageListEntry], openSearch: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void, firstTime: Bool, isLoading: Bool, forceUpdate: Bool, animated: Bool) -> LanguageListNodeTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(theme: theme, strings: strings, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(theme: theme, strings: strings, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) } return LanguageListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, isLoading: isLoading, animated: animated) } @@ -299,7 +279,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { private var activityIndicator: ActivityIndicator? private var searchDisplayController: SearchDisplayController? - private let presentationDataValue = Promise<(PresentationTheme, PresentationStrings)>() + private let presentationDataValue = Promise() private var updatedDisposable: Disposable? private var listDisposable: Disposable? private let applyDisposable = MetaDisposable() @@ -316,7 +296,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { init(context: AccountContext, presentationData: PresentationData, navigationBar: NavigationBar, requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, updateCanStartEditing: @escaping (Bool?) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.presentationData = presentationData - self.presentationDataValue.set(.single((presentationData.theme, presentationData.strings))) + self.presentationDataValue.set(.single(presentationData)) self.navigationBar = navigationBar self.requestActivateSearch = requestActivateSearch self.requestDeactivateSearch = requestDeactivateSearch @@ -411,8 +391,8 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { entries.append(.localization(index: entries.count, info: info, type: .official, selected: info.languageCode == activeLanguageCode, activity: applyingCode == info.languageCode, revealed: revealedCode == info.languageCode, editing: false)) } } - let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.0, presentationData.1)) - let transition = preparedLanguageListNodeTransition(theme: presentationData.0, strings: presentationData.1, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, openSearch: openSearch, selectLocalization: { [weak self] info in self?.selectLocalization(info) }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem, firstTime: previousEntriesAndPresentationData == nil, isLoading: entries.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.0 || previousEntriesAndPresentationData?.2 !== presentationData.1, animated: (previousEntriesAndPresentationData?.0.count ?? 0) >= entries.count) + let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings)) + let transition = preparedLanguageListNodeTransition(presentationData: presentationData, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, openSearch: openSearch, selectLocalization: { [weak self] info in self?.selectLocalization(info) }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem, firstTime: previousEntriesAndPresentationData == nil, isLoading: entries.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, animated: (previousEntriesAndPresentationData?.0.count ?? 0) >= entries.count) strongSelf.enqueueTransition(transition) }) self.updatedDisposable = synchronizedLocalizationListState(postbox: context.account.postbox, network: context.account.network).start() @@ -426,7 +406,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData - self.presentationDataValue.set(.single((presentationData.theme, presentationData.strings))) + self.presentationDataValue.set(.single(presentationData)) self.backgroundColor = presentationData.theme.list.blocksBackgroundColor self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.chatList.backgroundColor, direction: true) self.searchDisplayController?.updatePresentationData(presentationData) @@ -447,29 +427,8 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: listInsets, duration: duration, curve: listViewCurve) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: listInsets, duration: duration, curve: curve) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) @@ -542,7 +501,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { applyImpl() return } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListItem.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListItem.swift index 80da7638fe..f82df3a6b4 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListItem.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListItem.swift @@ -17,8 +17,7 @@ struct LocalizationListItemEditing: Equatable { } class LocalizationListItem: ListViewItem, ItemListItem { - let theme: PresentationTheme - let strings: PresentationStrings + let presentationData: ItemListPresentationData let id: String let title: String let subtitle: String @@ -31,9 +30,8 @@ class LocalizationListItem: ListViewItem, ItemListItem { let setItemWithRevealedOptions: (String?, String?) -> Void let removeItem: (String) -> Void - init(theme: PresentationTheme, strings: PresentationStrings, id: String, title: String, subtitle: String, checked: Bool, activity: Bool, editing: LocalizationListItemEditing, sectionId: ItemListSectionId, alwaysPlain: Bool, action: @escaping () -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) { - self.theme = theme - self.strings = strings + init(presentationData: ItemListPresentationData, id: String, title: String, subtitle: String, checked: Bool, activity: Bool, editing: LocalizationListItemEditing, sectionId: ItemListSectionId, alwaysPlain: Bool, action: @escaping () -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) { + self.presentationData = presentationData self.id = id self.title = title self.subtitle = subtitle @@ -96,9 +94,6 @@ class LocalizationListItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(17.0) -private let subtitleFont = Font.regular(13.0) - class LocalizationListItemNode: ItemListRevealOptionsItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -176,38 +171,38 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { return { item, params, neighbors in var leftInset: CGFloat = params.leftInset + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0)) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 50.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.subtitle, font: subtitleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 50.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let insets = itemListNeighborsGroupedInsets(neighbors) - let contentSize = CGSize(width: params.width, height: 58.0) + let contentSize = CGSize(width: params.width, height: titleLayout.size.height + 1.0 + subtitleLayout.size.height + 8.0 * 2.0) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? + var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)? var editingOffset: CGFloat = 0.0 if item.editing.editing { - let sizeAndApply = editableControlLayout(layout.contentSize.height, item.theme, false) + let sizeAndApply = editableControlLayout(item.presentationData.theme, false) editableControlSizeAndApply = sizeAndApply - editingOffset = sizeAndApply.0.width + editingOffset = sizeAndApply.0 } leftInset += 16.0 - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 50.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - - let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.subtitle, font: subtitleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 50.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let separatorHeight = UIScreenPixel var updateCheckImage: UIImage? var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme - } - - if currentItem?.theme !== item.theme { - updateCheckImage = PresentationResourcesItemList.checkIconImage(item.theme) + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + updateCheckImage = PresentationResourcesItemList.checkIconImage(item.presentationData.theme) } return (layout, { [weak self] animated in @@ -226,16 +221,16 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { if let updateCheckImage = updateCheckImage { strongSelf.iconNode.image = updateCheckImage - strongSelf.activityNode.type = ActivityIndicatorType.custom(item.theme.list.itemAccentColor, 22.0, 0.0, false) + strongSelf.activityNode.type = ActivityIndicatorType.custom(item.presentationData.theme.list.itemAccentColor, 22.0, 0.0, false) } strongSelf.activityNode.isHidden = !item.activity if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } let _ = titleApply() @@ -275,12 +270,12 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + leftInset, y: 8.0), size: titleLayout.size)) - transition.updateFrame(node: strongSelf.subtitleNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + leftInset, y: 31.0), size: subtitleLayout.size)) + transition.updateFrame(node: strongSelf.subtitleNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + leftInset, y: strongSelf.titleNode.frame.maxY + 1.0), size: subtitleLayout.size)) if let editableControlSizeAndApply = editableControlSizeAndApply { - let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: editableControlSizeAndApply.0) + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height)) if strongSelf.editableControlNode == nil { - let editableControlNode = editableControlSizeAndApply.1() + let editableControlNode = editableControlSizeAndApply.1(layout.contentSize.height) editableControlNode.tapped = { if let strongSelf = self { strongSelf.setRevealOptionsOpened(true, animated: true) @@ -312,7 +307,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) if item.editing.editable { - strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)])) } else { strongSelf.setRevealOptions((left: [], right: [])) } diff --git a/submodules/SettingsUI/Sources/LogoutOptionsController.swift b/submodules/SettingsUI/Sources/LogoutOptionsController.swift index 4c1d21f9f3..e549426303 100644 --- a/submodules/SettingsUI/Sources/LogoutOptionsController.swift +++ b/submodules/SettingsUI/Sources/LogoutOptionsController.swift @@ -73,37 +73,37 @@ private enum LogoutOptionsEntry: ItemListNodeEntry, Equatable { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! LogoutOptionsItemArguments switch self { case let .alternativeHeader(theme, title): - return ItemListSectionHeaderItem(theme: theme, text: title, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .addAccount(theme, title, text): - return ItemListDisclosureItem(theme: theme, icon: PresentationResourcesSettings.addAccount, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.addAccount, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.addAccount() }) case let .setPasscode(theme, title, text): - return ItemListDisclosureItem(theme: theme, icon: PresentationResourcesSettings.setPasscode, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.setPasscode, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.setPasscode() }) case let .clearCache(theme, title, text): - return ItemListDisclosureItem(theme: theme, icon: PresentationResourcesSettings.clearCache, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.clearCache, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.clearCache() }) case let .changePhoneNumber(theme, title, text): - return ItemListDisclosureItem(theme: theme, icon: PresentationResourcesSettings.changePhoneNumber, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.changePhoneNumber, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.changePhoneNumber() }) case let .contactSupport(theme, title, text): - return ItemListDisclosureItem(theme: theme, icon: PresentationResourcesSettings.support, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.support, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.contactSupport() }) case let .logout(theme, title): - return ItemListActionItem(theme: theme, title: title, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: title, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.logout() }) case let .logoutInfo(theme, title): - return ItemListTextItem(theme: theme, text: .plain(title), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(title), sectionId: self.section) } } } @@ -186,7 +186,7 @@ func logoutOptionsController(context: AccountContext, navigationController: Navi context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, present: { controller, arguments in pushControllerImpl?(controller) - }, dismissInput: {}) + }, dismissInput: {}, contentContext: nil) }) } @@ -219,7 +219,11 @@ func logoutOptionsController(context: AccountContext, navigationController: Navi presentControllerImpl?(alertController, nil) }) + #if ENABLE_WALLET let hasWallets = context.hasWallets + #else + let hasWallets: Signal = .single(false) + #endif let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, @@ -239,8 +243,8 @@ func logoutOptionsController(context: AccountContext, navigationController: Navi break } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.LogoutOptions_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: logoutOptionsEntries(presentationData: presentationData, canAddAccounts: canAddAccounts, hasPasscode: hasPasscode, hasWallets: hasWallets), style: .blocks) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.LogoutOptions_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: logoutOptionsEntries(presentationData: presentationData, canAddAccounts: canAddAccounts, hasPasscode: hasPasscode, hasWallets: hasWallets), style: .blocks) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionControllerNode.swift b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionControllerNode.swift index 4401cee2c0..497038325d 100644 --- a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionControllerNode.swift +++ b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionControllerNode.swift @@ -425,7 +425,7 @@ private func notificationsExceptionEntries(presentationData: PresentationData, s } private final class NotificationExceptionArguments { - let account: Account + let context: AccountContext let activateSearch:()->Void let openPeer: (Peer) -> Void let selectPeer: ()->Void @@ -433,8 +433,8 @@ private final class NotificationExceptionArguments { let deletePeer:(Peer) -> Void let removeAll:() -> Void - init(account: Account, activateSearch:@escaping() -> Void, openPeer: @escaping(Peer) -> Void, selectPeer: @escaping()->Void, updateRevealedPeerId:@escaping(PeerId?)->Void, deletePeer: @escaping(Peer) -> Void, removeAll:@escaping() -> Void) { - self.account = account + init(context: AccountContext, activateSearch:@escaping() -> Void, openPeer: @escaping(Peer) -> Void, selectPeer: @escaping()->Void, updateRevealedPeerId:@escaping(PeerId?)->Void, deletePeer: @escaping(Peer) -> Void, removeAll:@escaping() -> Void) { + self.context = context self.activateSearch = activateSearch self.openPeer = openPeer self.selectPeer = selectPeer @@ -510,7 +510,7 @@ private enum NotificationExceptionEntry : ItemListNodeEntry { case addException(PresentationTheme, PresentationStrings, NotificationExceptionMode.Mode, Bool) case removeAll(PresentationTheme, PresentationStrings) - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! NotificationExceptionArguments switch self { case let .search(theme, strings): @@ -527,11 +527,11 @@ private enum NotificationExceptionEntry : ItemListNodeEntry { case .channels: icon = PresentationResourcesItemList.addChannelIcon(theme) } - return ItemListPeerActionItem(theme: theme, icon: icon, title: strings.Notification_Exceptions_AddException, alwaysPlain: true, sectionId: self.section, editing: editing, action: { + return ItemListPeerActionItem(presentationData: presentationData, icon: icon, title: strings.Notification_Exceptions_AddException, alwaysPlain: true, sectionId: self.section, editing: editing, action: { arguments.selectPeer() }) case let .peer(_, peer, theme, strings, dateTimeFormat, nameDisplayOrder, value, _, revealed, editing, isSearching): - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: nil, text: .text(value), label: .none, editing: ItemListPeerItemEditing(editable: true, editing: editing, revealed: revealed), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .text(value), label: .none, editing: ItemListPeerItemEditing(editable: true, editing: editing, revealed: revealed), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { arguments.openPeer(peer) }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in arguments.updateRevealedPeerId(peerId) @@ -539,12 +539,12 @@ private enum NotificationExceptionEntry : ItemListNodeEntry { arguments.deletePeer(peer) }, hasTopStripe: false, hasTopGroupInset: false, noInsets: isSearching) case let .addPeer(_, peer, theme, strings, _, nameDisplayOrder): - return ContactsPeerItem(theme: theme, strings: strings, sortOrder: nameDisplayOrder, displayOrder: nameDisplayOrder, account: arguments.account, peerMode: .peer, peer: .peer(peer: peer, chatPeer: peer), status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .add, index: nil, header: ChatListSearchItemHeader(type: .addToExceptions, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in + return ContactsPeerItem(presentationData: presentationData, sortOrder: nameDisplayOrder, displayOrder: nameDisplayOrder, context: arguments.context, peerMode: .peer, peer: .peer(peer: peer, chatPeer: peer), status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .add, index: nil, header: ChatListSearchItemHeader(type: .addToExceptions, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in arguments.openPeer(peer) }, setPeerIdWithRevealedOptions: { _, _ in }) case let .removeAll(theme, strings): - return ItemListActionItem(theme: theme, title: strings.Notification_Exceptions_DeleteAll, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: strings.Notification_Exceptions_DeleteAll, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.removeAll() }) } @@ -651,12 +651,12 @@ private struct NotificationExceptionNodeTransition { let animated: Bool } -private func preparedExceptionsListNodeTransition(theme: PresentationTheme, strings: PresentationStrings, from fromEntries: [NotificationExceptionEntry], to toEntries: [NotificationExceptionEntry], arguments: NotificationExceptionArguments, firstTime: Bool, forceUpdate: Bool, animated: Bool) -> NotificationExceptionNodeTransition { +private func preparedExceptionsListNodeTransition(presentationData: ItemListPresentationData, from fromEntries: [NotificationExceptionEntry], to toEntries: [NotificationExceptionEntry], arguments: NotificationExceptionArguments, firstTime: Bool, forceUpdate: Bool, animated: Bool) -> NotificationExceptionNodeTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(arguments), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(arguments), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, arguments: arguments), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, arguments: arguments), directionHint: nil) } return NotificationExceptionNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, animated: animated) } @@ -860,7 +860,7 @@ final class NotificationExceptionsControllerNode: ViewControllerTracingNode { }) } - let arguments = NotificationExceptionArguments(account: context.account, activateSearch: { + let arguments = NotificationExceptionArguments(context: context, activateSearch: { openSearch() }, openPeer: { peer in presentPeerSettings(peer.id, {}) @@ -923,7 +923,7 @@ final class NotificationExceptionsControllerNode: ViewControllerTracingNode { }) }, removeAll: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: presentationData.strings.Notification_Exceptions_DeleteAllConfirmation), ActionSheetButtonItem(title: presentationData.strings.Notification_Exceptions_DeleteAll, color: .destructive, action: { [weak actionSheet] in @@ -961,7 +961,7 @@ final class NotificationExceptionsControllerNode: ViewControllerTracingNode { }) }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -995,7 +995,7 @@ final class NotificationExceptionsControllerNode: ViewControllerTracingNode { animated = false } - let transition = preparedExceptionsListNodeTransition(theme: presentationData.theme, strings: presentationData.strings, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, arguments: arguments, firstTime: previousEntriesAndPresentationData == nil, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, animated: animated) + let transition = preparedExceptionsListNodeTransition(presentationData: ItemListPresentationData(presentationData), from: previousEntriesAndPresentationData?.0 ?? [], to: entries, arguments: arguments, firstTime: previousEntriesAndPresentationData == nil, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, animated: animated) self?.listNode.keepTopItemOverscrollBackground = entries.count <= 1 ? nil : ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.chatList.backgroundColor, direction: true) @@ -1038,29 +1038,8 @@ final class NotificationExceptionsControllerNode: ViewControllerTracingNode { self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: listInsets, headerInsets: headerInsets, duration: duration, curve: listViewCurve) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: listInsets, headerInsets: headerInsets, duration: duration, curve: curve) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) @@ -1152,12 +1131,12 @@ private struct NotificationExceptionsSearchContainerTransition { let isSearching: Bool } -private func preparedNotificationExceptionsSearchContainerTransition(theme: PresentationTheme, strings: PresentationStrings, from fromEntries: [NotificationExceptionEntry], to toEntries: [NotificationExceptionEntry], arguments: NotificationExceptionArguments, isSearching: Bool, forceUpdate: Bool) -> NotificationExceptionsSearchContainerTransition { +private func preparedNotificationExceptionsSearchContainerTransition(presentationData: ItemListPresentationData, from fromEntries: [NotificationExceptionEntry], to toEntries: [NotificationExceptionEntry], arguments: NotificationExceptionArguments, isSearching: Bool, forceUpdate: Bool) -> NotificationExceptionsSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(arguments), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(arguments), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, arguments: arguments), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, arguments: arguments), directionHint: nil) } return NotificationExceptionsSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) } @@ -1266,7 +1245,7 @@ private final class NotificationExceptionsSearchContainerNode: SearchDisplayCont let entries = notificationsExceptionEntries(presentationData: presentationData, state: state.0, query: state.1, foundPeers: foundPeers) let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings)) - let transition = preparedNotificationExceptionsSearchContainerTransition(theme: presentationData.theme, strings: presentationData.strings, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, arguments: arguments, isSearching: state.1 != nil && !state.1!.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings) + let transition = preparedNotificationExceptionsSearchContainerTransition(presentationData: ItemListPresentationData(presentationData), from: previousEntriesAndPresentationData?.0 ?? [], to: entries, arguments: arguments, isSearching: state.1 != nil && !state.1!.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings) self?.enqueueTransition(transition) })) @@ -1348,30 +1327,10 @@ private final class NotificationExceptionsSearchContainerNode: SearchDisplayCont let topInset = navigationBarHeight transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: nil) - } + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: 0.0, bottom: layout.insets(options: [.input]).bottom, right: 0.0), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: 0.0, bottom: layout.insets(options: [.input]).bottom, right: 0.0), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if !self.hasValidLayout { self.hasValidLayout = true diff --git a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionSettingsController.swift b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionSettingsController.swift index dec3d00836..15b7590aa4 100644 --- a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionSettingsController.swift +++ b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionSettingsController.swift @@ -156,11 +156,11 @@ private enum NotificationPeerExceptionEntry: ItemListNodeEntry { return lhs.index < rhs.index } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! NotificationPeerExceptionArguments switch self { case let .remove(_, theme, strings): - return ItemListActionItem(theme: theme, title: strings.Notification_Exceptions_RemoveFromExceptions, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: strings.Notification_Exceptions_RemoveFromExceptions, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.removeFromExceptions() }) case let .switcher(_, theme, strings, mode, selected): @@ -171,11 +171,11 @@ private enum NotificationPeerExceptionEntry: ItemListNodeEntry { case .alwaysOff: title = strings.Notification_Exceptions_AlwaysOff } - return ItemListCheckboxItem(theme: theme, title: title, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.selectMode(mode) }) case let .switcherHeader(_, theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .displayPreviews(_, theme, strings, value, selected): let title: String switch value { @@ -184,25 +184,25 @@ private enum NotificationPeerExceptionEntry: ItemListNodeEntry { case .alwaysOff: title = strings.Notification_Exceptions_AlwaysOff } - return ItemListCheckboxItem(theme: theme, title: title, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.selectDisplayPreviews(value) }) case let .displayPreviewsHeader(_, theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .soundModernHeader(_, theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .soundClassicHeader(_, theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .none(_, _, theme, text, selected): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: selected, zeroSeparatorInsets: true, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: true, sectionId: self.section, action: { arguments.selectSound(.none) }) case let .default(_, _, theme, text, selected): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.selectSound(.default) }) case let .sound(_, _, theme, text, sound, selected): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.selectSound(sound) }) } @@ -374,8 +374,8 @@ func notificationPeerExceptionController(context: AccountContext, peer: Peer, mo arguments.complete() }) - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: notificationPeerExceptionEntries(presentationData: presentationData, state: state), style: .blocks, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: notificationPeerExceptionEntries(presentationData: presentationData, state: state), style: .blocks, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/Notifications/NotificationsAndSounds.swift b/submodules/SettingsUI/Sources/Notifications/NotificationsAndSounds.swift index 58655c1eb4..f2ece41feb 100644 --- a/submodules/SettingsUI/Sources/Notifications/NotificationsAndSounds.swift +++ b/submodules/SettingsUI/Sources/Notifications/NotificationsAndSounds.swift @@ -17,6 +17,49 @@ import TelegramNotices import NotificationSoundSelectionUI import TelegramStringFormatting +private struct CounterTagSettings: OptionSet { + var rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue + } + + init(summaryTags: PeerSummaryCounterTags) { + var result = CounterTagSettings() + if summaryTags.contains(.privateChat) { + result.insert(.regularChatsAndPrivateGroups) + } + if summaryTags.contains(.channel) { + result.insert(.channels) + } + if summaryTags.contains(.publicGroup) { + result.insert(.publicGroups) + } + self = result + } + + func toSumaryTags() -> PeerSummaryCounterTags { + var result = PeerSummaryCounterTags() + if self.contains(.regularChatsAndPrivateGroups) { + result.insert(.privateChat) + result.insert(.secretChat) + result.insert(.bot) + result.insert(.privateGroup) + } + if self.contains(.publicGroups) { + result.insert(.publicGroup) + } + if self.contains(.channels) { + result.insert(.channel) + } + return result + } + + static let regularChatsAndPrivateGroups = CounterTagSettings(rawValue: 1 << 0) + static let publicGroups = CounterTagSettings(rawValue: 1 << 1) + static let channels = CounterTagSettings(rawValue: 1 << 2) +} + private final class NotificationsAndSoundsArguments { let context: AccountContext let presentController: (ViewController, ViewControllerPresentationArguments?) -> Void @@ -43,7 +86,7 @@ private final class NotificationsAndSoundsArguments { let updateInAppPreviews: (Bool) -> Void let updateDisplayNameOnLockscreen: (Bool) -> Void - let updateIncludeTag: (PeerSummaryCounterTags, Bool) -> Void + let updateIncludeTag: (CounterTagSettings, Bool) -> Void let updateTotalUnreadCountCategory: (Bool) -> Void let updateJoinedNotifications: (Bool) -> Void @@ -56,7 +99,7 @@ private final class NotificationsAndSoundsArguments { let updateNotificationsFromAllAccounts: (Bool) -> Void - init(context: AccountContext, presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, pushController: @escaping(ViewController)->Void, soundSelectionDisposable: MetaDisposable, authorizeNotifications: @escaping () -> Void, suppressWarning: @escaping () -> Void, updateMessageAlerts: @escaping (Bool) -> Void, updateMessagePreviews: @escaping (Bool) -> Void, updateMessageSound: @escaping (PeerMessageSound) -> Void, updateGroupAlerts: @escaping (Bool) -> Void, updateGroupPreviews: @escaping (Bool) -> Void, updateGroupSound: @escaping (PeerMessageSound) -> Void, updateChannelAlerts: @escaping (Bool) -> Void, updateChannelPreviews: @escaping (Bool) -> Void, updateChannelSound: @escaping (PeerMessageSound) -> Void, updateInAppSounds: @escaping (Bool) -> Void, updateInAppVibration: @escaping (Bool) -> Void, updateInAppPreviews: @escaping (Bool) -> Void, updateDisplayNameOnLockscreen: @escaping (Bool) -> Void, updateIncludeTag: @escaping (PeerSummaryCounterTags, Bool) -> Void, updateTotalUnreadCountCategory: @escaping (Bool) -> Void, resetNotifications: @escaping () -> Void, updatedExceptionMode: @escaping(NotificationExceptionMode) -> Void, openAppSettings: @escaping () -> Void, updateJoinedNotifications: @escaping (Bool) -> Void, updateNotificationsFromAllAccounts: @escaping (Bool) -> Void) { + init(context: AccountContext, presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, pushController: @escaping(ViewController)->Void, soundSelectionDisposable: MetaDisposable, authorizeNotifications: @escaping () -> Void, suppressWarning: @escaping () -> Void, updateMessageAlerts: @escaping (Bool) -> Void, updateMessagePreviews: @escaping (Bool) -> Void, updateMessageSound: @escaping (PeerMessageSound) -> Void, updateGroupAlerts: @escaping (Bool) -> Void, updateGroupPreviews: @escaping (Bool) -> Void, updateGroupSound: @escaping (PeerMessageSound) -> Void, updateChannelAlerts: @escaping (Bool) -> Void, updateChannelPreviews: @escaping (Bool) -> Void, updateChannelSound: @escaping (PeerMessageSound) -> Void, updateInAppSounds: @escaping (Bool) -> Void, updateInAppVibration: @escaping (Bool) -> Void, updateInAppPreviews: @escaping (Bool) -> Void, updateDisplayNameOnLockscreen: @escaping (Bool) -> Void, updateIncludeTag: @escaping (CounterTagSettings, Bool) -> Void, updateTotalUnreadCountCategory: @escaping (Bool) -> Void, resetNotifications: @escaping () -> Void, updatedExceptionMode: @escaping(NotificationExceptionMode) -> Void, openAppSettings: @escaping () -> Void, updateJoinedNotifications: @escaping (Bool) -> Void, updateNotificationsFromAllAccounts: @escaping (Bool) -> Void) { self.context = context self.presentController = presentController self.pushController = pushController @@ -558,37 +601,37 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! NotificationsAndSoundsArguments switch self { case let .accountsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .allAccounts(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateNotificationsFromAllAccounts(updatedValue) }, tag: self.tag) case let .accountsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .permissionInfo(theme, title, text, suppressed): - return ItemListInfoItem(theme: theme, title: title, text: .plain(text), style: .blocks, sectionId: self.section, closeAction: suppressed ? nil : { + return ItemListInfoItem(presentationData: presentationData, title: title, text: .plain(text), style: .blocks, sectionId: self.section, closeAction: suppressed ? nil : { arguments.suppressWarning() }) case let .permissionEnable(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.authorizeNotifications() }) case let .messageHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .messageAlerts(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateMessageAlerts(updatedValue) }, tag: self.tag) case let .messagePreviews(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateMessagePreviews(updatedValue) }, tag: self.tag) case let .messageSound(theme, text, value, sound): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { let controller = notificationSoundSelectionController(context: arguments.context, isModal: true, currentSound: sound, defaultSound: nil, completion: { [weak arguments] value in arguments?.updateMessageSound(value) }) @@ -596,24 +639,24 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { }) case let .userExceptions(theme, strings, text, value): let label = value.settings.count > 0 ? strings.Notifications_Exceptions(Int32(value.settings.count)) : strings.Notification_Exceptions_Add - return ItemListDisclosureItem(theme: theme, title: text, label: label, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: label, sectionId: self.section, style: .blocks, action: { let controller = NotificationExceptionsController(context: arguments.context, mode: value, updatedMode: arguments.updatedExceptionMode) arguments.pushController(controller) }) case let .messageNotice(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .groupHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .groupAlerts(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateGroupAlerts(updatedValue) }, tag: self.tag) case let .groupPreviews(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateGroupPreviews(updatedValue) }, tag: self.tag) case let .groupSound(theme, text, value, sound): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { let controller = notificationSoundSelectionController(context: arguments.context, isModal: true, currentSound: sound, defaultSound: nil, completion: { [weak arguments] value in arguments?.updateGroupSound(value) }) @@ -621,24 +664,24 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { }) case let .groupExceptions(theme, strings, text, value): let label = value.settings.count > 0 ? strings.Notifications_Exceptions(Int32(value.settings.count)) : strings.Notification_Exceptions_Add - return ItemListDisclosureItem(theme: theme, title: text, label: label, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: label, sectionId: self.section, style: .blocks, action: { let controller = NotificationExceptionsController(context: arguments.context, mode: value, updatedMode: arguments.updatedExceptionMode) arguments.pushController(controller) }) case let .groupNotice(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .channelHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .channelAlerts(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateChannelAlerts(updatedValue) }, tag: self.tag) case let .channelPreviews(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateChannelPreviews(updatedValue) }, tag: self.tag) case let .channelSound(theme, text, value, sound): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { let controller = notificationSoundSelectionController(context: arguments.context, isModal: true, currentSound: sound, defaultSound: nil, completion: { [weak arguments] value in arguments?.updateChannelSound(value) }) @@ -646,62 +689,62 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { }) case let .channelExceptions(theme, strings, text, value): let label = value.settings.count > 0 ? strings.Notifications_Exceptions(Int32(value.settings.count)) : strings.Notification_Exceptions_Add - return ItemListDisclosureItem(theme: theme, title: text, label: label, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: label, sectionId: self.section, style: .blocks, action: { let controller = NotificationExceptionsController(context: arguments.context, mode: value, updatedMode: arguments.updatedExceptionMode) arguments.pushController(controller) }) case let .channelNotice(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .inAppHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .inAppSounds(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateInAppSounds(updatedValue) }, tag: self.tag) case let .inAppVibrate(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateInAppVibration(updatedValue) }, tag: self.tag) case let .inAppPreviews(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateInAppPreviews(updatedValue) }, tag: self.tag) case let .displayNamesOnLockscreen(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateDisplayNameOnLockscreen(updatedValue) }, tag: self.tag) case let .displayNamesOnLockscreenInfo(theme, text): - return ItemListTextItem(theme: theme, text: .markdown(text.replacingOccurrences(of: "]", with: "]()")), sectionId: self.section, linkAction: { _ in + return ItemListTextItem(presentationData: presentationData, text: .markdown(text.replacingOccurrences(of: "]", with: "]()")), sectionId: self.section, linkAction: { _ in arguments.openAppSettings() }) case let .badgeHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .includePublicGroups(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateIncludeTag(.publicGroups, updatedValue) }, tag: self.tag) case let .includeChannels(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateIncludeTag(.channels, updatedValue) }, tag: self.tag) case let .unreadCountCategory(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateTotalUnreadCountCategory(updatedValue) }, tag: self.tag) case let .unreadCountCategoryInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .joinedNotifications(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateJoinedNotifications(updatedValue) }, tag: self.tag) case let .joinedNotificationsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .reset(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.resetNotifications() }, tag: self.tag) case let .resetNotice(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -779,8 +822,11 @@ private func notificationsAndSoundsEntries(authorizationStatus: AccessType, warn entries.append(.displayNamesOnLockscreenInfo(presentationData.theme, presentationData.strings.Notifications_DisplayNamesOnLockScreenInfoWithLink)) entries.append(.badgeHeader(presentationData.theme, presentationData.strings.Notifications_Badge.uppercased())) - entries.append(.includePublicGroups(presentationData.theme, presentationData.strings.Notifications_Badge_IncludePublicGroups, inAppSettings.totalUnreadCountIncludeTags.contains(.publicGroups))) - entries.append(.includeChannels(presentationData.theme, presentationData.strings.Notifications_Badge_IncludeChannels, inAppSettings.totalUnreadCountIncludeTags.contains(.channels))) + + let counterTagSettings = CounterTagSettings(summaryTags: inAppSettings.totalUnreadCountIncludeTags) + + entries.append(.includePublicGroups(presentationData.theme, presentationData.strings.Notifications_Badge_IncludePublicGroups, counterTagSettings.contains(.publicGroups))) + entries.append(.includeChannels(presentationData.theme, presentationData.strings.Notifications_Badge_IncludeChannels, counterTagSettings.contains(.channels))) entries.append(.unreadCountCategory(presentationData.theme, presentationData.strings.Notifications_Badge_CountUnreadMessages, inAppSettings.totalUnreadCountDisplayCategory == .messages)) entries.append(.unreadCountCategoryInfo(presentationData.theme, inAppSettings.totalUnreadCountDisplayCategory == .chats ? presentationData.strings.Notifications_Badge_CountUnreadMessages_InfoOff : presentationData.strings.Notifications_Badge_CountUnreadMessages_InfoOn)) entries.append(.joinedNotifications(presentationData.theme, presentationData.strings.NotificationSettings_ContactJoined, globalSettings.contactsJoined)) @@ -911,12 +957,14 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions }).start() }, updateIncludeTag: { tag, value in let _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in - var settings = settings + var currentSettings = CounterTagSettings(summaryTags: settings.totalUnreadCountIncludeTags) if !value { - settings.totalUnreadCountIncludeTags.remove(tag) + currentSettings.remove(tag) } else { - settings.totalUnreadCountIncludeTags.insert(tag) + currentSettings.insert(tag) } + var settings = settings + settings.totalUnreadCountIncludeTags = currentSettings.toSumaryTags() return settings }).start() }, updateTotalUnreadCountCategory: { value in @@ -927,7 +975,7 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions }).start() }, resetNotifications: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Notifications_Reset, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -943,7 +991,7 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions let _ = signal.start() }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -1074,8 +1122,8 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions } } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Notifications_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, initialScrollToItem: scrollToItem) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Notifications_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, initialScrollToItem: scrollToItem) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/OpenSettings.swift b/submodules/SettingsUI/Sources/OpenSettings.swift index 7a8af13c36..b76066a67b 100644 --- a/submodules/SettingsUI/Sources/OpenSettings.swift +++ b/submodules/SettingsUI/Sources/OpenSettings.swift @@ -7,8 +7,7 @@ import SyncCore import OverlayStatusController import AccountContext import PresentationDataUtils - -private let maximumNumberOfAccounts = 3 +import AccountUtils func openEditSettings(context: AccountContext, accountsAndPeers: Signal<((Account, Peer)?, [(Account, Peer, Int32)]), NoError>, focusOnItemTag: EditSettingsEntryTag? = nil, presentController: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void) -> Disposable { let openEditingDisposable = MetaDisposable() diff --git a/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift index 3464fd0f3b..8af1364bf1 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift @@ -14,15 +14,15 @@ import ItemListPeerItem import ItemListPeerActionItem private final class BlockedPeersControllerArguments { - let account: Account + let context: AccountContext let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let addPeer: () -> Void let removePeer: (PeerId) -> Void let openPeer: (Peer) -> Void - init(account: Account, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (Peer) -> Void) { - self.account = account + init(context: AccountContext, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (Peer) -> Void) { + self.context = context self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.addPeer = addPeer self.removePeer = removePeer @@ -121,15 +121,15 @@ private enum BlockedPeersEntry: ItemListNodeEntry { } } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! BlockedPeersControllerArguments switch self { case let .add(theme, text): - return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.addPersonIcon(theme), title: text, sectionId: self.section, editing: false, action: { + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addPersonIcon(theme), title: text, sectionId: self.section, editing: false, action: { arguments.addPeer() }) case let .peerItem(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer, editing, enabled): - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: { + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: { arguments.openPeer(peer) }, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) @@ -217,7 +217,7 @@ public func blockedPeersController(context: AccountContext, blockedPeersContext: let peersPromise = Promise<[Peer]?>(nil) - let arguments = BlockedPeersControllerArguments(account: context.account, setPeerIdWithRevealedOptions: { peerId, fromPeerId in + let arguments = BlockedPeersControllerArguments(context: context, setPeerIdWithRevealedOptions: { peerId, fromPeerId in updateState { state in if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { return state.withUpdatedPeerIdWithRevealedOptions(peerId) @@ -259,7 +259,7 @@ public func blockedPeersController(context: AccountContext, blockedPeersContext: } })) }, openPeer: { peer in - if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { pushControllerImpl?(controller) } }) @@ -294,8 +294,8 @@ public func blockedPeersController(context: AccountContext, blockedPeersContext: let previousStateValue = previousState previousState = blockedPeersState - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.BlockedUsers_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(entries: blockedPeersControllerEntries(presentationData: presentationData, state: state, blockedPeersState: blockedPeersState), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previousStateValue != nil && previousStateValue!.peers.count >= blockedPeersState.peers.count, scrollEnabled: emptyStateItem == nil) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.BlockedUsers_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: blockedPeersControllerEntries(presentationData: presentationData, state: state, blockedPeersState: blockedPeersState), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previousStateValue != nil && previousStateValue!.peers.count >= blockedPeersState.peers.count, scrollEnabled: emptyStateItem == nil) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/ConfirmPhoneNumberController.swift b/submodules/SettingsUI/Sources/Privacy and Security/ConfirmPhoneNumberController.swift index 1b4c8bb18c..ac27bd6795 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/ConfirmPhoneNumberController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/ConfirmPhoneNumberController.swift @@ -85,11 +85,11 @@ private enum ConfirmPhoneNumberCodeEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ConfirmPhoneNumberCodeControllerArguments switch self { case let .codeEntry(theme, strings, title, text): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: title, textColor: .black), text: text, placeholder: "", type: .number, spacing: 10.0, tag: ConfirmPhoneNumberCodeTag.input, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: title, textColor: .black), text: text, placeholder: "", type: .number, spacing: 10.0, tag: ConfirmPhoneNumberCodeTag.input, sectionId: self.section, textUpdated: { updatedText in arguments.updateEntryText(updatedText) }, action: { arguments.next() @@ -108,7 +108,7 @@ private enum ConfirmPhoneNumberCodeEntry: ItemListNodeEntry { if !nextOptionText.isEmpty { result += "\n\n" + nextOptionText } - return ItemListTextItem(theme: theme, text: .markdown(result), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .markdown(result), sectionId: self.section) } } } @@ -169,7 +169,7 @@ private final class ConfirmPhoneNumberCodeControllerImpl: ItemListController, Co self.applyCodeImpl = applyCodeImpl let presentationData = context.sharedContext.currentPresentationData.with { $0 } - super.init(theme: presentationData.theme, strings: presentationData.strings, updatedPresentationData: context.sharedContext.presentationData |> map { ($0.theme, $0.strings) }, state: state, tabBarItem: nil) + super.init(presentationData: ItemListPresentationData(presentationData), updatedPresentationData: context.sharedContext.presentationData |> map(ItemListPresentationData.init(_:)), state: state, tabBarItem: nil) } required init(coder aDecoder: NSCoder) { @@ -311,8 +311,8 @@ public func confirmPhoneNumberCodeController(context: AccountContext, phoneNumbe }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.CancelResetAccount_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: confirmPhoneNumberCodeControllerEntries(presentationData: presentationData, state: state, phoneNumber: phoneNumber, codeData: data, timeout: timeout, strings: presentationData.strings, theme: presentationData.theme), style: .blocks, focusItemTag: ConfirmPhoneNumberCodeTag.input, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.CancelResetAccount_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: confirmPhoneNumberCodeControllerEntries(presentationData: presentationData, state: state, phoneNumber: phoneNumber, codeData: data, timeout: timeout, strings: presentationData.strings, theme: presentationData.theme), style: .blocks, focusItemTag: ConfirmPhoneNumberCodeTag.input, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift b/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift index 539981eb97..5be4d7c79c 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift @@ -118,29 +118,29 @@ private enum CreatePasswordEntry: ItemListNodeEntry, Equatable { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! CreatePasswordControllerArguments switch self { case let .passwordHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .password(theme, strings, text, value): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(), text: value, placeholder: text, type: .password, returnKeyType: .next, spacing: 0.0, tag: CreatePasswordEntryTag.password, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: text, type: .password, returnKeyType: .next, spacing: 0.0, tag: CreatePasswordEntryTag.password, sectionId: self.section, textUpdated: { updatedText in arguments.updateFieldText(.password, updatedText) }, action: { arguments.selectNextInputItem(CreatePasswordEntryTag.password) }) case let .passwordConfirmation(theme, strings, text, value): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(), text: value, placeholder: text, type: .password, returnKeyType: .next, spacing: 0.0, tag: CreatePasswordEntryTag.passwordConfirmation, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: text, type: .password, returnKeyType: .next, spacing: 0.0, tag: CreatePasswordEntryTag.passwordConfirmation, sectionId: self.section, textUpdated: { updatedText in arguments.updateFieldText(.passwordConfirmation, updatedText) }, action: { arguments.selectNextInputItem(CreatePasswordEntryTag.passwordConfirmation) }) case let .passwordInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .hintHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .hint(theme, strings, text, value, last): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(), text: value, placeholder: text, type: .regular(capitalization: true, autocorrection: false), returnKeyType: last ? .done : .next, spacing: 0.0, tag: CreatePasswordEntryTag.hint, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: text, type: .regular(capitalization: true, autocorrection: false), returnKeyType: last ? .done : .next, spacing: 0.0, tag: CreatePasswordEntryTag.hint, sectionId: self.section, textUpdated: { updatedText in arguments.updateFieldText(.hint, updatedText) }, action: { if last { @@ -150,21 +150,21 @@ private enum CreatePasswordEntry: ItemListNodeEntry, Equatable { } }) case let .hintInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .emailHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .email(theme, strings, text, value): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(), text: value, placeholder: text, type: .email, returnKeyType: .done, spacing: 0.0, tag: CreatePasswordEntryTag.email, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: text, type: .email, returnKeyType: .done, spacing: 0.0, tag: CreatePasswordEntryTag.email, sectionId: self.section, textUpdated: { updatedText in arguments.updateFieldText(.email, updatedText) }, action: { arguments.save() }) case let .emailInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .emailConfirmation(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .emailCancel(theme, text, enabled): - return ItemListActionItem(theme: theme, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.cancelEmailConfirmation() }) } @@ -406,8 +406,8 @@ func createPasswordController(context: AccountContext, createPasswordContext: Cr title = presentationData.strings.FastTwoStepSetup_Title } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: createPasswordControllerEntries(presentationData: presentationData, context: createPasswordContext, state: state), style: .blocks, focusItemTag: CreatePasswordEntryTag.password, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createPasswordControllerEntries(presentationData: presentationData, context: createPasswordContext, state: state), style: .blocks, focusItemTag: CreatePasswordEntryTag.password, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift index 04e059923d..2372c84b27 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift @@ -209,49 +209,49 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! DataPrivacyControllerArguments switch self { case let .contactsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .deleteContacts(theme, text, value): - return ItemListActionItem(theme: theme, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.deleteContacts() }) case let .syncContacts(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateSyncContacts(updatedValue) }) case let .syncContactsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .frequentContacts(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: !value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: !value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateSuggestFrequentContacts(updatedValue) }) case let .frequentContactsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .chatsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .deleteCloudDrafts(theme, text, value): - return ItemListActionItem(theme: theme, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.deleteCloudDrafts() }) case let .paymentHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .clearPaymentInfo(theme, text, enabled): - return ItemListActionItem(theme: theme, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.clearPaymentInfo() }) case let .paymentInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .secretChatLinkPreviewsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .secretChatLinkPreviews(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateSecretChatLinkPreviews(updatedValue) }) case let .secretChatLinkPreviewsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -306,7 +306,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { let arguments = DataPrivacyControllerArguments(account: context.account, clearPaymentInfo: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -453,7 +453,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { } }, deleteCloudDrafts: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -515,12 +515,12 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { let rightNavigationButton: ItemListNavigationButton? = nil - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.PrivateDataSettings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PrivateDataSettings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let previousStateValue = previousState.swap(state) let animateChanges = false - let listState = ItemListNodeState(entries: dataPrivacyControllerEntries(presentationData: presentationData, state: state, secretChatLinkPreviews: secretChatLinkPreviews, synchronizeDeviceContacts: synchronizeDeviceContacts, frequentContacts: suggestRecentPeers), style: .blocks, animateChanges: animateChanges) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: dataPrivacyControllerEntries(presentationData: presentationData, state: state, secretChatLinkPreviews: secretChatLinkPreviews, synchronizeDeviceContacts: synchronizeDeviceContacts, frequentContacts: suggestRecentPeers), style: .blocks, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift index 4292da40e1..ccc324d7ef 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift @@ -18,6 +18,7 @@ class ForwardPrivacyChatPreviewItem: ListViewItem, ItemListItem { let strings: PresentationStrings let sectionId: ItemListSectionId let fontSize: PresentationFontSize + let chatBubbleCorners: PresentationChatBubbleCorners let wallpaper: TelegramWallpaper let dateTimeFormat: PresentationDateTimeFormat let nameDisplayOrder: PresentationPersonNameOrder @@ -25,12 +26,13 @@ class ForwardPrivacyChatPreviewItem: ListViewItem, ItemListItem { let linkEnabled: Bool let tooltipText: String - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, fontSize: PresentationFontSize, wallpaper: TelegramWallpaper, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, peerName: String, linkEnabled: Bool, tooltipText: String) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, wallpaper: TelegramWallpaper, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, peerName: String, linkEnabled: Bool, tooltipText: String) { self.context = context self.theme = theme self.strings = strings self.sectionId = sectionId self.fontSize = fontSize + self.chatBubbleCorners = chatBubbleCorners self.wallpaper = wallpaper self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder @@ -119,6 +121,8 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { super.init(layerBacked: false, dynamicBounce: false) + self.clipsToBounds = true + self.addSubnode(self.containerNode) self.tooltipContainerNode.addSubnode(self.textNode) @@ -134,8 +138,13 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { return { item, params, neighbors in var updatedBackgroundImage: UIImage? + var backgroundImageContentMode = UIView.ContentMode.scaleAspectFill if currentItem?.wallpaper != item.wallpaper { updatedBackgroundImage = chatControllerBackgroundImage(theme: item.theme, wallpaper: item.wallpaper, mediaBox: item.context.sharedContext.accountManager.mediaBox, knockoutMode: item.context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper) + + if case .gradient = item.wallpaper { + backgroundImageContentMode = .scaleToFill + } } let insets: UIEdgeInsets @@ -150,7 +159,7 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { let forwardInfo = MessageForwardInfo(author: item.linkEnabled ? peers[peerId] : nil, source: nil, sourceMessageId: nil, date: 0, authorSignature: item.linkEnabled ? nil : item.peerName) - let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, message: Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: forwardInfo, author: nil, text: item.strings.Privacy_Forwards_PreviewMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil) + let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, message: Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: forwardInfo, author: nil, text: item.strings.Privacy_Forwards_PreviewMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil) var node: ListViewItemNode? if let current = currentNode { @@ -222,6 +231,7 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { if let updatedBackgroundImage = updatedBackgroundImage { strongSelf.backgroundNode.image = updatedBackgroundImage + strongSelf.backgroundNode.contentMode = backgroundImageContentMode } strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor @@ -263,8 +273,9 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.backgroundNode.frame = backgroundFrame.insetBy(dx: 0.0, dy: -100.0) + strongSelf.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift index 3b4c1fe359..750686026c 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift @@ -15,6 +15,7 @@ import AccountContext import LocalAuth import PasscodeUI import TelegramStringFormatting +import TelegramIntents private final class PasscodeOptionsControllerArguments { let turnPasscodeOff: () -> Void @@ -106,27 +107,27 @@ private enum PasscodeOptionsEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! PasscodeOptionsControllerArguments switch self { case let .togglePasscode(theme, title, value): - return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { if value { arguments.turnPasscodeOff() } }) case let .changePasscode(theme, title): - return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.changePasscode() }) case let .settingInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .autoLock(theme, title, value): - return ItemListDisclosureItem(theme: theme, title: title, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: title, label: value, sectionId: self.section, style: .blocks, action: { arguments.changePasscodeTimeout() }) case let .touchId(theme, title, value): - return ItemListSwitchItem(theme: theme, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.changeTouchId(value) }) } @@ -233,7 +234,7 @@ func passcodeOptionsController(context: AccountContext) -> ViewController { let arguments = PasscodeOptionsControllerArguments(turnPasscodeOff: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.PasscodeSettings_TurnPasscodeOff, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -278,7 +279,7 @@ func passcodeOptionsController(context: AccountContext) -> ViewController { } }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -313,7 +314,7 @@ func passcodeOptionsController(context: AccountContext) -> ViewController { }) }, changePasscodeTimeout: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetItem] = [] let setAction: (Int32?) -> Void = { value in let _ = (passcodeOptionsDataPromise.get() @@ -345,7 +346,7 @@ func passcodeOptionsController(context: AccountContext) -> ViewController { } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -363,8 +364,8 @@ func passcodeOptionsController(context: AccountContext) -> ViewController { let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), passcodeOptionsDataPromise.get()) |> deliverOnMainQueue |> map { presentationData, state, passcodeOptionsData -> (ItemListControllerState, (ItemListNodeState, Any)) in - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.PasscodeSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: passcodeOptionsControllerEntries(presentationData: presentationData, state: state, passcodeOptionsData: passcodeOptionsData), style: .blocks, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PasscodeSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: passcodeOptionsControllerEntries(presentationData: presentationData, state: state, passcodeOptionsData: passcodeOptionsData), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { @@ -414,6 +415,7 @@ public func passcodeOptionsAccessController(context: AccountContext, animateIn: }, error: { _ in }, completed: { completion(true) + deleteAllSendMessageIntents() }) } pushController?(setupController) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 013c7b52b0..6e741d6517 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -249,67 +249,67 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! PrivacyAndSecurityControllerArguments switch self { case let .privacyHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .blockedPeers(theme, text, value): - return ItemListDisclosureItem(theme: theme, icon: UIImage(bundleImageName: "Settings/MenuIcons/Blocked")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/MenuIcons/Blocked")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openBlockedUsers() }) case let .phoneNumberPrivacy(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openPhoneNumberPrivacy() }) case let .lastSeenPrivacy(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openLastSeenPrivacy() }) case let .profilePhotoPrivacy(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openProfilePhotoPrivacy() }) case let .forwardPrivacy(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openForwardPrivacy() }) case let .groupPrivacy(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openGroupsPrivacy() }) case let .selectivePrivacyInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .voiceCallPrivacy(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openVoiceCallPrivacy() }) case let .passcode(theme, text, hasFaceId, value): - return ItemListDisclosureItem(theme: theme, icon: UIImage(bundleImageName: hasFaceId ? "Settings/MenuIcons/FaceId" : "Settings/MenuIcons/TouchId")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: hasFaceId ? "Settings/MenuIcons/FaceId" : "Settings/MenuIcons/TouchId")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openPasscode() }) case let .twoStepVerification(theme, text, value, data): - return ItemListDisclosureItem(theme: theme, icon: UIImage(bundleImageName: "Settings/MenuIcons/TwoStepAuth")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/MenuIcons/TwoStepAuth")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openTwoStepVerification(data) }) case let .activeSessions(theme, text, value): - return ItemListDisclosureItem(theme: theme, icon: UIImage(bundleImageName: "Settings/MenuIcons/Sessions")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/MenuIcons/Websites")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openActiveSessions() }) case let .accountHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .accountTimeout(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.setupAccountAutoremove() }, tag: PrivacyAndSecurityEntryTag.accountTimeout) case let .accountInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .dataSettings(theme, text): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openDataSettings() }) case let .dataSettingsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -353,11 +353,13 @@ private func stringForSelectiveSettings(strings: PresentationStrings, settings: } } -private func privacyAndSecurityControllerEntries(presentationData: PresentationData, state: PrivacyAndSecurityControllerState, privacySettings: AccountPrivacySettings?, accessChallengeData: PostboxAccessChallengeData, blockedPeerCount: Int?, activeSessionsCount: Int, twoStepAuthData: TwoStepVerificationAccessConfiguration?) -> [PrivacyAndSecurityEntry] { +private func privacyAndSecurityControllerEntries(presentationData: PresentationData, state: PrivacyAndSecurityControllerState, privacySettings: AccountPrivacySettings?, accessChallengeData: PostboxAccessChallengeData, blockedPeerCount: Int?, activeWebsitesCount: Int, hasTwoStepAuth: Bool?, twoStepAuthData: TwoStepVerificationAccessConfiguration?) -> [PrivacyAndSecurityEntry] { var entries: [PrivacyAndSecurityEntry] = [] entries.append(.blockedPeers(presentationData.theme, presentationData.strings.Settings_BlockedUsers, blockedPeerCount == nil ? "" : (blockedPeerCount == 0 ? presentationData.strings.PrivacySettings_BlockedPeersEmpty : "\(blockedPeerCount!)"))) - entries.append(.activeSessions(presentationData.theme, presentationData.strings.PrivacySettings_AuthSessions, activeSessionsCount == 0 ? "" : "\(activeSessionsCount)")) + if activeWebsitesCount != 0 { + entries.append(.activeSessions(presentationData.theme, presentationData.strings.PrivacySettings_WebSessions, activeWebsitesCount == 0 ? "" : "\(activeWebsitesCount)")) + } let passcodeValue: String switch accessChallengeData { @@ -378,13 +380,8 @@ private func privacyAndSecurityControllerEntries(presentationData: PresentationD entries.append(.passcode(presentationData.theme, presentationData.strings.PrivacySettings_Passcode, false, passcodeValue)) } var twoStepAuthString = "" - if let twoStepAuthData = twoStepAuthData { - switch twoStepAuthData { - case .set: - twoStepAuthString = presentationData.strings.PrivacySettings_PasscodeOn - case .notSet: - twoStepAuthString = presentationData.strings.PrivacySettings_PasscodeOff - } + if let hasTwoStepAuth = hasTwoStepAuth { + twoStepAuthString = hasTwoStepAuth ? presentationData.strings.PrivacySettings_PasscodeOn : presentationData.strings.PrivacySettings_PasscodeOff } entries.append(.twoStepVerification(presentationData.theme, presentationData.strings.PrivacySettings_TwoStepAuth, twoStepAuthString, twoStepAuthData)) @@ -427,7 +424,7 @@ private func privacyAndSecurityControllerEntries(presentationData: PresentationD return entries } -public func privacyAndSecurityController(context: AccountContext, initialSettings: AccountPrivacySettings? = nil, updatedSettings: ((AccountPrivacySettings?) -> Void)? = nil, focusOnItemTag: PrivacyAndSecurityEntryTag? = nil) -> ViewController { +public func privacyAndSecurityController(context: AccountContext, initialSettings: AccountPrivacySettings? = nil, updatedSettings: ((AccountPrivacySettings?) -> Void)? = nil, updatedBlockedPeers: ((BlockedPeersContext?) -> Void)? = nil, updatedHasTwoStepAuth: ((Bool) -> Void)? = nil, focusOnItemTag: PrivacyAndSecurityEntryTag? = nil, activeSessionsContext: ActiveSessionsContext? = nil, webSessionsContext: WebSessionsContext? = nil, blockedPeersContext: BlockedPeersContext? = nil, hasTwoStepAuth: Bool? = nil) -> ViewController { let statePromise = ValuePromise(PrivacyAndSecurityControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: PrivacyAndSecurityControllerState()) let updateState: ((PrivacyAndSecurityControllerState) -> PrivacyAndSecurityControllerState) -> Void = { f in @@ -448,23 +445,56 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting let privacySettingsPromise = Promise() privacySettingsPromise.set(.single(initialSettings) |> then(requestAccountPrivacySettings(account: context.account) |> map(Optional.init))) + + let blockedPeersContext = blockedPeersContext ?? BlockedPeersContext(account: context.account) + let activeSessionsContext = activeSessionsContext ?? ActiveSessionsContext(account: context.account) + let webSessionsContext = webSessionsContext ?? WebSessionsContext(account: context.account) - let blockedPeersContext = BlockedPeersContext(account: context.account) - let activeSessionsContext = ActiveSessionsContext(account: context.account) + let blockedPeersState = Promise() + blockedPeersState.set(blockedPeersContext.state) + + webSessionsContext.loadMore() let updateTwoStepAuthDisposable = MetaDisposable() actionsDisposable.add(updateTwoStepAuthDisposable) let twoStepAuthDataValue = Promise(nil) + let hasTwoStepAuthDataValue = twoStepAuthDataValue.get() + |> map { data -> Bool? in + if let data = data { + if case .set = data { + return true + } else { + return false + } + } else { + return nil + } + } + + let twoStepAuth = Promise() + if let hasTwoStepAuth = hasTwoStepAuth { + twoStepAuth.set(.single(hasTwoStepAuth) |> then(hasTwoStepAuthDataValue)) + } else { + twoStepAuth.set(hasTwoStepAuthDataValue) + } + let updateHasTwoStepAuth: () -> Void = { let signal = twoStepVerificationConfiguration(account: context.account) |> map { value -> TwoStepVerificationAccessConfiguration? in - return TwoStepVerificationAccessConfiguration(configuration: value, password: nil) + return TwoStepVerificationAccessConfiguration(configuration: value, password: nil) } |> deliverOnMainQueue updateTwoStepAuthDisposable.set( signal.start(next: { value in twoStepAuthDataValue.set(.single(value)) + if let value = value { + if case .set = value { + updatedHasTwoStepAuth?(true) + } else { + updatedHasTwoStepAuth?(false) + } + } }) ) } @@ -641,14 +671,14 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting case .set: break case let .notSet(pendingEmail): - break //intro = pendingEmail == nil - /*if pendingEmail == nil { + if pendingEmail == nil { let controller = TwoFactorAuthSplashScreen(context: context, mode: .intro) pushControllerImpl?(controller, true) + return } else { - }*/ + } } } if intro { @@ -669,7 +699,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting pushControllerImpl?(controller, true) } }, openActiveSessions: { - pushControllerImpl?(recentSessionsController(context: context, activeSessionsContext: activeSessionsContext), true) + pushControllerImpl?(recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, websitesOnly: true), true) }, setupAccountAutoremove: { let signal = privacySettingsPromise.get() |> take(1) @@ -677,7 +707,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting updateAccountTimeoutDisposable.set(signal.start(next: { [weak updateAccountTimeoutDisposable] privacySettingsValue in if let _ = privacySettingsValue { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -739,16 +769,21 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting updatedSettings?(settings) })) - let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), privacySettingsPromise.get(), context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.secretChatLinkPreviewsKey()), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]), recentPeers(account: context.account), blockedPeersContext.state, activeSessionsContext.state, context.sharedContext.accountManager.accessChallengeData(), twoStepAuthDataValue.get()) - |> map { presentationData, state, privacySettings, noticeView, sharedData, recentPeers, blockedPeersState, activeSessionsState, accessChallengeData, twoStepAuthData -> (ItemListControllerState, (ItemListNodeState, Any)) in + actionsDisposable.add((blockedPeersState.get() + |> deliverOnMainQueue).start(next: { _ in + updatedBlockedPeers?(blockedPeersContext) + })) + + let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), privacySettingsPromise.get(), context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.secretChatLinkPreviewsKey()), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]), recentPeers(account: context.account), blockedPeersState.get(), webSessionsContext.state, context.sharedContext.accountManager.accessChallengeData(), combineLatest(twoStepAuth.get(), twoStepAuthDataValue.get())) + |> map { presentationData, state, privacySettings, noticeView, sharedData, recentPeers, blockedPeersState, activeWebsitesState, accessChallengeData, twoStepAuth -> (ItemListControllerState, (ItemListNodeState, Any)) in var rightNavigationButton: ItemListNavigationButton? if privacySettings == nil || state.updatingAccountTimeoutValue != nil { rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.PrivacySettings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PrivacySettings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings, accessChallengeData: accessChallengeData.data, blockedPeerCount: blockedPeersState.totalCount, activeSessionsCount: activeSessionsState.sessions.count, twoStepAuthData: twoStepAuthData), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings, accessChallengeData: accessChallengeData.data, blockedPeerCount: blockedPeersState.totalCount, activeWebsitesCount: activeWebsitesState.sessions.count, hasTwoStepAuth: twoStepAuth.0, twoStepAuthData: twoStepAuth.1), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroController.swift index ecf41810a2..4de91ff54d 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroController.swift @@ -69,7 +69,7 @@ enum PrivacyIntroControllerMode { } } -final public class PrivacyIntroControllerPresentationArguments { +public final class PrivacyIntroControllerPresentationArguments { let fadeIn: Bool let animateIn: Bool diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroControllerNode.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroControllerNode.swift index 08b4282688..ab70359164 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroControllerNode.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroControllerNode.swift @@ -13,6 +13,7 @@ import AuthorizationUI private func generateButtonImage(backgroundColor: UIColor, borderColor: UIColor, highlightColor: UIColor?) -> UIImage? { return generateImage(CGSize(width: 1.0, height: 44.0), contextGenerator: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) if let highlightColor = highlightColor { context.setFillColor(highlightColor.cgColor) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListRecentSessionItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListRecentSessionItem.swift index b5d3a63945..37937b00d9 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListRecentSessionItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListRecentSessionItem.swift @@ -37,8 +37,7 @@ enum ItemListRecentSessionItemText { } final class ItemListRecentSessionItem: ListViewItem, ItemListItem { - let theme: PresentationTheme - let strings: PresentationStrings + let presentationData: ItemListPresentationData let dateTimeFormat: PresentationDateTimeFormat let session: RecentAccountSession let enabled: Bool @@ -49,9 +48,8 @@ final class ItemListRecentSessionItem: ListViewItem, ItemListItem { let setSessionIdWithRevealedOptions: (Int64?, Int64?) -> Void let removeSession: (Int64) -> Void - init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editable: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void) { - self.theme = theme - self.strings = strings + init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editable: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void) { + self.presentationData = presentationData self.dateTimeFormat = dateTimeFormat self.session = session self.enabled = enabled @@ -102,9 +100,6 @@ final class ItemListRecentSessionItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.medium(15.0) -private let textFont = Font.regular(13.0) - class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -179,8 +174,15 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { return { item, params, neighbors in var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme + let titleFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0)) + + let verticalInset: CGFloat = 10.0 + let titleSpacing: CGFloat = 1.0 + let textSpacing: CGFloat = 3.0 + + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme } var titleAttributedString: NSAttributedString? @@ -190,14 +192,14 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { let peerRevealOptions: [ItemListRevealOption] if item.editable && item.enabled { - peerRevealOptions = [ItemListRevealOption(key: 0, title: item.strings.AuthSessions_Terminate, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)] + peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.AuthSessions_Terminate, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] } else { peerRevealOptions = [] } let rightInset: CGFloat = params.rightInset - titleAttributedString = NSAttributedString(string: "\(item.session.appName) \(item.session.appVersion)", font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) + titleAttributedString = NSAttributedString(string: "\(item.session.appName) \(item.session.appVersion)", font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) var appString = "" if !item.session.deviceModel.isEmpty { @@ -218,36 +220,36 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { appString += item.session.systemVersion } - appAttributedString = NSAttributedString(string: appString, font: textFont, textColor: item.theme.list.itemPrimaryTextColor) - locationAttributedString = NSAttributedString(string: "\(item.session.ip) — \(item.session.country)", font: textFont, textColor: item.theme.list.itemSecondaryTextColor) + appAttributedString = NSAttributedString(string: appString, font: textFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + locationAttributedString = NSAttributedString(string: "\(item.session.ip) — \(item.session.country)", font: textFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) if item.session.isCurrent { - labelAttributedString = NSAttributedString(string: item.strings.Presence_online, font: textFont, textColor: item.theme.list.itemAccentColor) + labelAttributedString = NSAttributedString(string: item.presentationData.strings.Presence_online, font: textFont, textColor: item.presentationData.theme.list.itemAccentColor) } else { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - let dateText = stringForRelativeTimestamp(strings: item.strings, relativeTimestamp: item.session.activityDate, relativeTo: timestamp, dateTimeFormat: item.dateTimeFormat) - labelAttributedString = NSAttributedString(string: dateText, font: textFont, textColor: item.theme.list.itemSecondaryTextColor) + let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.session.activityDate, relativeTo: timestamp, dateTimeFormat: item.dateTimeFormat) + labelAttributedString = NSAttributedString(string: dateText, font: textFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) } let leftInset: CGFloat = 15.0 + params.leftInset - var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? + var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)? let editingOffset: CGFloat if item.editing { - let sizeAndApply = editableControlLayout(75.0, item.theme, false) + let sizeAndApply = editableControlLayout(item.presentationData.theme, false) editableControlSizeAndApply = sizeAndApply - editingOffset = sizeAndApply.0.width + editingOffset = sizeAndApply.0 } else { editingOffset = 0.0 } let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - labelLayout.size.width - 5.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset - labelLayout.size.width - 5.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (appLayout, appApply) = makeAppLayout(TextNodeLayoutArguments(attributedString: appAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (locationLayout, locationApply) = makeLocationLayout(TextNodeLayoutArguments(attributedString: locationAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let insets = itemListNeighborsGroupedInsets(neighbors) - let contentSize = CGSize(width: params.width, height: 75.0) + let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + appLayout.size.height + textSpacing + locationLayout.size.height) let separatorHeight = UIScreenPixel let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -267,10 +269,10 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { strongSelf.layoutParams = (item, params, neighbors) if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } let revealOffset = strongSelf.revealOffset @@ -300,9 +302,9 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { } if let editableControlSizeAndApply = editableControlSizeAndApply { - let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: editableControlSizeAndApply.0) + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height)) if strongSelf.editableControlNode == nil { - let editableControlNode = editableControlSizeAndApply.1() + let editableControlNode = editableControlSizeAndApply.1(layout.contentSize.height) editableControlNode.tapped = { if let strongSelf = self { strongSelf.setRevealOptionsOpened(true, animated: true) @@ -370,17 +372,17 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) - transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - labelLayout.size.width - 15.0 - rightInset, y: 10.0), size: labelLayout.size)) - transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 10.0), size: titleLayout.size)) - transition.updateFrame(node: strongSelf.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 30.0), size: appLayout.size)) - transition.updateFrame(node: strongSelf.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 50.0), size: locationLayout.size)) + transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - labelLayout.size.width - 15.0 - rightInset, y: verticalInset), size: labelLayout.size)) + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: appLayout.size)) + transition.updateFrame(node: strongSelf.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.appNode.frame.maxY + textSpacing), size: locationLayout.size)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 75.0 + UIScreenPixel + UIScreenPixel)) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift index 9a949e1910..802c7bac02 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift @@ -13,6 +13,7 @@ import PresentationDataUtils import AvatarNode import TelegramStringFormatting import LocalizedPeerData +import AccountContext struct ItemListWebsiteItemEditing: Equatable { let editing: Bool @@ -30,7 +31,7 @@ struct ItemListWebsiteItemEditing: Equatable { } final class ItemListWebsiteItem: ListViewItem, ItemListItem { - let account: Account + let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let dateTimeFormat: PresentationDateTimeFormat @@ -44,8 +45,8 @@ final class ItemListWebsiteItem: ListViewItem, ItemListItem { let setSessionIdWithRevealedOptions: (Int64?, Int64?) -> Void let removeSession: (Int64) -> Void - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, website: WebAuthorization, peer: Peer?, enabled: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void) { - self.account = account + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, website: WebAuthorization, peer: Peer?, enabled: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void) { + self.context = context self.theme = theme self.strings = strings self.dateTimeFormat = dateTimeFormat @@ -111,7 +112,7 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { private var disabledOverlayNode: ASDisplayNode? private let maskNode: ASImageNode - private let avatarNode: AvatarNode + let avatarNode: AvatarNode private let titleNode: TextNode private let appNode: TextNode private let locationNode: TextNode @@ -226,13 +227,13 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { let leftInset: CGFloat = 15.0 + params.leftInset - var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? + var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)? let editingOffset: CGFloat if item.editing { - let sizeAndApply = editableControlLayout(75.0, item.theme, false) + let sizeAndApply = editableControlLayout(item.theme, false) editableControlSizeAndApply = sizeAndApply - editingOffset = sizeAndApply.0.width + editingOffset = sizeAndApply.0 } else { editingOffset = 0.0 } @@ -270,7 +271,7 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { } if let peer = item.peer { - strongSelf.avatarNode.setPeer(account: item.account, theme: item.theme, peer: peer, authorOfMessage: nil, overrideImage: nil, emptyColor: nil, clipStyle: .none, synchronousLoad: false) + strongSelf.avatarNode.setPeer(context: item.context, theme: item.theme, peer: peer, authorOfMessage: nil, overrideImage: nil, emptyColor: nil, clipStyle: .none, synchronousLoad: false) } let revealOffset = strongSelf.revealOffset @@ -300,9 +301,9 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { } if let editableControlSizeAndApply = editableControlSizeAndApply { - let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: editableControlSizeAndApply.0) + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height)) if strongSelf.editableControlNode == nil { - let editableControlNode = editableControlSizeAndApply.1() + let editableControlNode = editableControlSizeAndApply.1(layout.contentSize.height) editableControlNode.tapped = { if let strongSelf = self { strongSelf.setRevealOptionsOpened(true, animated: true) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift index 7a94a462ef..9a1d034d43 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift @@ -10,9 +10,10 @@ import TelegramUIPreferences import ItemListUI import PresentationDataUtils import AccountContext +import AuthTransferUI private final class RecentSessionsControllerArguments { - let account: Account + let context: AccountContext let setSessionIdWithRevealedOptions: (Int64?, Int64?) -> Void let removeSession: (Int64) -> Void @@ -21,14 +22,22 @@ private final class RecentSessionsControllerArguments { let removeWebSession: (Int64) -> Void let terminateAllWebSessions: () -> Void - init(account: Account, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, terminateOtherSessions: @escaping () -> Void, removeWebSession: @escaping (Int64) -> Void, terminateAllWebSessions: @escaping () -> Void) { - self.account = account + let addDevice: () -> Void + + let openOtherAppsUrl: () -> Void + + init(context: AccountContext, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, terminateOtherSessions: @escaping () -> Void, removeWebSession: @escaping (Int64) -> Void, terminateAllWebSessions: @escaping () -> Void, addDevice: @escaping () -> Void, openOtherAppsUrl: @escaping () -> Void) { + self.context = context self.setSessionIdWithRevealedOptions = setSessionIdWithRevealedOptions self.removeSession = removeSession self.terminateOtherSessions = terminateOtherSessions self.removeWebSession = removeWebSession self.terminateAllWebSessions = terminateAllWebSessions + + self.addDevice = addDevice + + self.openOtherAppsUrl = openOtherAppsUrl } } @@ -46,32 +55,7 @@ private enum RecentSessionsSection: Int32 { private enum RecentSessionsEntryStableId: Hashable { case session(Int64) case index(Int32) - - var hashValue: Int { - switch self { - case let .session(hash): - return hash.hashValue - case let .index(index): - return index.hashValue - } - } - - static func ==(lhs: RecentSessionsEntryStableId, rhs: RecentSessionsEntryStableId) -> Bool { - switch lhs { - case let .session(hash): - if case .session(hash) = rhs { - return true - } else { - return false - } - case let .index(index): - if case .index(index) = rhs { - return true - } else { - return false - } - } - } + case devicesInfo } private enum RecentSessionsEntry: ItemListNodeEntry { @@ -84,205 +68,246 @@ private enum RecentSessionsEntry: ItemListNodeEntry { case pendingSession(index: Int32, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool) case pendingSessionsInfo(PresentationTheme, String) case otherSessionsHeader(PresentationTheme, String) + case addDevice(PresentationTheme, String) case session(index: Int32, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool) case website(index: Int32, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, website: WebAuthorization, peer: Peer?, enabled: Bool, editing: Bool, revealed: Bool) + case devicesInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { - case .currentSessionHeader, .currentSession, .terminateOtherSessions, .terminateAllWebSessions, .currentSessionInfo: - return RecentSessionsSection.currentSession.rawValue - case .pendingSessionsHeader, .pendingSession, .pendingSessionsInfo: - return RecentSessionsSection.pendingSessions.rawValue - case .otherSessionsHeader, .session, .website: - return RecentSessionsSection.otherSessions.rawValue + case .currentSessionHeader, .currentSession, .terminateOtherSessions, .terminateAllWebSessions, .currentSessionInfo: + return RecentSessionsSection.currentSession.rawValue + case .pendingSessionsHeader, .pendingSession, .pendingSessionsInfo: + return RecentSessionsSection.pendingSessions.rawValue + case .otherSessionsHeader, .addDevice, .session, .website, .devicesInfo: + return RecentSessionsSection.otherSessions.rawValue } } var stableId: RecentSessionsEntryStableId { switch self { - case .currentSessionHeader: - return .index(0) - case .currentSession: - return .index(1) - case .terminateOtherSessions: - return .index(2) - case .terminateAllWebSessions: - return .index(3) - case .currentSessionInfo: - return .index(4) - case .pendingSessionsHeader: - return .index(5) - case let .pendingSession(_, _, _, _, session, _, _, _): - return .session(session.hash) - case .pendingSessionsInfo: - return .index(6) - case .otherSessionsHeader: - return .index(7) - case let .session(_, _, _, _, session, _, _, _): - return .session(session.hash) - case let .website(_, _, _, _, _, website, _, _, _, _): - return .session(website.hash) + case .currentSessionHeader: + return .index(0) + case .currentSession: + return .index(1) + case .terminateOtherSessions: + return .index(2) + case .terminateAllWebSessions: + return .index(3) + case .currentSessionInfo: + return .index(4) + case .pendingSessionsHeader: + return .index(5) + case let .pendingSession(_, _, _, _, session, _, _, _): + return .session(session.hash) + case .pendingSessionsInfo: + return .index(6) + case .otherSessionsHeader: + return .index(7) + case .addDevice: + return .index(8) + case let .session(_, _, _, _, session, _, _, _): + return .session(session.hash) + case let .website(_, _, _, _, _, website, _, _, _, _): + return .session(website.hash) + case .devicesInfo: + return .devicesInfo } } static func ==(lhs: RecentSessionsEntry, rhs: RecentSessionsEntry) -> Bool { switch lhs { - case let .currentSessionHeader(lhsTheme, lhsText): - if case let .currentSessionHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .terminateOtherSessions(lhsTheme, lhsText): - if case let .terminateOtherSessions(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .terminateAllWebSessions(lhsTheme, lhsText): - if case let .terminateAllWebSessions(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .currentSessionInfo(lhsTheme, lhsText): - if case let .currentSessionInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .pendingSessionsHeader(lhsTheme, lhsText): - if case let .pendingSessionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .pendingSession(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsSession, lhsEnabled, lhsEditing, lhsRevealed): - if case let .pendingSession(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsSession, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsSession == rhsSession, lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed { - return true - } else { - return false - } - case let .pendingSessionsInfo(lhsTheme, lhsText): - if case let .pendingSessionsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .otherSessionsHeader(lhsTheme, lhsText): - if case let .otherSessionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .currentSession(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsSession): - if case let .currentSession(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsSession) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsSession == rhsSession { - return true - } else { - return false - } - case let .session(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsSession, lhsEnabled, lhsEditing, lhsRevealed): - if case let .session(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsSession, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsSession == rhsSession, lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed { - return true - } else { - return false - } - case let .website(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameOrder, lhsWebsite, lhsPeer, lhsEnabled, lhsEditing, lhsRevealed): - if case let .website(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsWebsite, rhsPeer, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameOrder == rhsNameOrder, lhsWebsite == rhsWebsite, arePeersEqual(lhsPeer, rhsPeer), lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed { - return true - } else { - return false - } + case let .currentSessionHeader(lhsTheme, lhsText): + if case let .currentSessionHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .terminateOtherSessions(lhsTheme, lhsText): + if case let .terminateOtherSessions(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .terminateAllWebSessions(lhsTheme, lhsText): + if case let .terminateAllWebSessions(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .currentSessionInfo(lhsTheme, lhsText): + if case let .currentSessionInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .pendingSessionsHeader(lhsTheme, lhsText): + if case let .pendingSessionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .pendingSession(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsSession, lhsEnabled, lhsEditing, lhsRevealed): + if case let .pendingSession(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsSession, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsSession == rhsSession, lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed { + return true + } else { + return false + } + case let .pendingSessionsInfo(lhsTheme, lhsText): + if case let .pendingSessionsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .otherSessionsHeader(lhsTheme, lhsText): + if case let .otherSessionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .addDevice(lhsTheme, lhsText): + if case let .addDevice(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .currentSession(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsSession): + if case let .currentSession(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsSession) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsSession == rhsSession { + return true + } else { + return false + } + case let .session(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsSession, lhsEnabled, lhsEditing, lhsRevealed): + if case let .session(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsSession, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsSession == rhsSession, lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed { + return true + } else { + return false + } + case let .website(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameOrder, lhsWebsite, lhsPeer, lhsEnabled, lhsEditing, lhsRevealed): + if case let .website(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsWebsite, rhsPeer, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameOrder == rhsNameOrder, lhsWebsite == rhsWebsite, arePeersEqual(lhsPeer, rhsPeer), lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed { + return true + } else { + return false + } + case let .devicesInfo(lhsTheme, lhsText): + if case let .devicesInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } } } static func <(lhs: RecentSessionsEntry, rhs: RecentSessionsEntry) -> Bool { switch lhs.stableId { - case let .index(lhsIndex): - if case let .index(rhsIndex) = rhs.stableId { - return lhsIndex <= rhsIndex + case let .index(lhsIndex): + if case let .index(rhsIndex) = rhs.stableId { + return lhsIndex <= rhsIndex + } else { + if case .pendingSession = rhs, lhsIndex > 5 { + return false } else { - if case .pendingSession = rhs, lhsIndex > 5 { - return false + return true + } + } + case .session: + switch lhs { + case let .session(lhsIndex, _, _, _, _, _, _, _): + if case let .session(rhsIndex, _, _, _, _, _, _, _) = rhs { + return lhsIndex <= rhsIndex + } else if case .devicesInfo = rhs.stableId { + return true + } else { + return false + } + case let .pendingSession(lhsIndex, _, _, _, _, _, _, _): + if case let .pendingSession(rhsIndex, _, _, _, _, _, _, _) = rhs { + return lhsIndex <= rhsIndex + } else if case .session = rhs { + return true + } else if case .devicesInfo = rhs.stableId { + return true + } else { + if case let .index(rhsIndex) = rhs.stableId { + return rhsIndex == 6 } else { - return true + return false } } - case .session: - switch lhs { - case let .session(lhsIndex, _, _, _, _, _, _, _): - if case let .session(rhsIndex, _, _, _, _, _, _, _) = rhs { - return lhsIndex <= rhsIndex - } else { - return false - } - case let .pendingSession(lhsIndex, _, _, _, _, _, _, _): - if case let .pendingSession(rhsIndex, _, _, _, _, _, _, _) = rhs { - return lhsIndex <= rhsIndex - } else if case .session = rhs { - return true - } else { - if case let .index(rhsIndex) = rhs.stableId { - return rhsIndex == 6 - } else { - return false - } - } - case let .website(lhsIndex, _, _, _, _, _, _, _, _, _): - if case let .website(rhsIndex, _, _, _, _, _, _, _, _, _) = rhs { - return lhsIndex <= rhsIndex - } else { - return false - } - default: - preconditionFailure() + case let .website(lhsIndex, _, _, _, _, _, _, _, _, _): + if case let .website(rhsIndex, _, _, _, _, _, _, _, _, _) = rhs { + return lhsIndex <= rhsIndex + } else if case .devicesInfo = rhs.stableId { + return true + } else { + return false } + default: + preconditionFailure() + } + case .devicesInfo: + if case .devicesInfo = rhs.stableId { + return false + } else { + return false + } } } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! RecentSessionsControllerArguments switch self { - case let .currentSessionHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) - case let .currentSession(theme, strings, dateTimeFormat, session): - return ItemListRecentSessionItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, session: session, enabled: true, editable: false, editing: false, revealed: false, sectionId: self.section, setSessionIdWithRevealedOptions: { _, _ in - }, removeSession: { _ in - }) - case let .terminateOtherSessions(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { - arguments.terminateOtherSessions() - }) - case let .terminateAllWebSessions(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { - arguments.terminateAllWebSessions() - }) - case let .currentSessionInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) - case let .pendingSessionsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) - case let .pendingSession(_, theme, strings, dateTimeFormat, session, enabled, editing, revealed): - return ItemListRecentSessionItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in - arguments.setSessionIdWithRevealedOptions(previousId, id) - }, removeSession: { id in - arguments.removeSession(id) - }) - case let .pendingSessionsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) - case let .otherSessionsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) - case let .session(_, theme, strings, dateTimeFormat, session, enabled, editing, revealed): - return ItemListRecentSessionItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in - arguments.setSessionIdWithRevealedOptions(previousId, id) - }, removeSession: { id in - arguments.removeSession(id) - }) - case let .website(_, theme, strings, dateTimeFormat, nameDisplayOrder, website, peer, enabled, editing, revealed): - return ItemListWebsiteItem(account: arguments.account, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, website: website, peer: peer, enabled: enabled, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in - arguments.setSessionIdWithRevealedOptions(previousId, id) - }, removeSession: { id in - arguments.removeWebSession(id) - }) + case let .currentSessionHeader(theme, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .currentSession(theme, strings, dateTimeFormat, session): + return ItemListRecentSessionItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, session: session, enabled: true, editable: false, editing: false, revealed: false, sectionId: self.section, setSessionIdWithRevealedOptions: { _, _ in + }, removeSession: { _ in + }) + case let .terminateOtherSessions(theme, text): + return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.terminateOtherSessions() + }) + case let .terminateAllWebSessions(theme, text): + return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.terminateAllWebSessions() + }) + case let .currentSessionInfo(theme, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .pendingSessionsHeader(theme, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .pendingSession(_, theme, strings, dateTimeFormat, session, enabled, editing, revealed): + return ItemListRecentSessionItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in + arguments.setSessionIdWithRevealedOptions(previousId, id) + }, removeSession: { id in + arguments.removeSession(id) + }) + case let .pendingSessionsInfo(theme, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .otherSessionsHeader(theme, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .addDevice(theme, text): + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.addDevice() + }) + case let .session(_, theme, strings, dateTimeFormat, session, enabled, editing, revealed): + return ItemListRecentSessionItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in + arguments.setSessionIdWithRevealedOptions(previousId, id) + }, removeSession: { id in + arguments.removeSession(id) + }) + case let .website(_, theme, strings, dateTimeFormat, nameDisplayOrder, website, peer, enabled, editing, revealed): + return ItemListWebsiteItem(context: arguments.context, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, website: website, peer: peer, enabled: enabled, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in + arguments.setSessionIdWithRevealedOptions(previousId, id) + }, removeSession: { id in + arguments.removeWebSession(id) + }) + case let .devicesInfo(theme, text): + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { action in + switch action { + case .tap: + arguments.openOtherAppsUrl() + } + }) } } } @@ -341,7 +366,7 @@ private struct RecentSessionsControllerState: Equatable { } } -private func recentSessionsControllerEntries(presentationData: PresentationData, state: RecentSessionsControllerState, sessionsState: ActiveSessionsContextState) -> [RecentSessionsEntry] { +private func recentSessionsControllerEntries(presentationData: PresentationData, state: RecentSessionsControllerState, sessionsState: ActiveSessionsContextState, enableQRLogin: Bool) -> [RecentSessionsEntry] { var entries: [RecentSessionsEntry] = [] if !sessionsState.sessions.isEmpty { @@ -352,7 +377,7 @@ private func recentSessionsControllerEntries(presentationData: PresentationData, entries.append(.currentSession(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, sessionsState.sessions[index])) } - if sessionsState.sessions.count > 1 { + if sessionsState.sessions.count > 1 || enableQRLogin { entries.append(.terminateOtherSessions(presentationData.theme, presentationData.strings.AuthSessions_TerminateOtherSessions)) entries.append(.currentSessionInfo(presentationData.theme, presentationData.strings.AuthSessions_TerminateOtherSessionsHelp)) @@ -370,6 +395,10 @@ private func recentSessionsControllerEntries(presentationData: PresentationData, entries.append(.otherSessionsHeader(presentationData.theme, presentationData.strings.AuthSessions_OtherSessions)) + if enableQRLogin { + entries.append(.addDevice(presentationData.theme, presentationData.strings.AuthSessions_AddDevice)) + } + let filteredSessions: [RecentAccountSession] = sessionsState.sessions.sorted(by: { lhs, rhs in return lhs.activityDate > rhs.activityDate }) @@ -380,6 +409,10 @@ private func recentSessionsControllerEntries(presentationData: PresentationData, entries.append(.session(index: Int32(i), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, session: filteredSessions[i], enabled: state.removingSessionId != filteredSessions[i].hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == filteredSessions[i].hash)) } } + + if enableQRLogin { + entries.append(.devicesInfo(presentationData.theme, presentationData.strings.AuthSessions_OtherDevices)) + } } } @@ -414,14 +447,22 @@ private func recentSessionsControllerEntries(presentationData: PresentationData, return entries } -public func recentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController { +private final class RecentSessionsControllerImpl: ItemListController, RecentSessionsController { +} + +public func recentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext, webSessionsContext: WebSessionsContext, websitesOnly: Bool) -> ViewController & RecentSessionsController { let statePromise = ValuePromise(RecentSessionsControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: RecentSessionsControllerState()) let updateState: ((RecentSessionsControllerState) -> RecentSessionsControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } + activeSessionsContext.loadMore() + webSessionsContext.loadMore() + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? + var dismissImpl: (() -> Void)? let actionsDisposable = DisposableSet() @@ -431,10 +472,25 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont let terminateOtherSessionsDisposable = MetaDisposable() actionsDisposable.add(terminateOtherSessionsDisposable) - let mode = ValuePromise(.sessions) - let websitesPromise = Promise<([WebAuthorization], [PeerId : Peer])?>(nil) + let didAppearValue = ValuePromise(false) - let arguments = RecentSessionsControllerArguments(account: context.account, setSessionIdWithRevealedOptions: { sessionId, fromSessionId in + if websitesOnly { + let autoDismissDisposable = (webSessionsContext.state + |> filter { !$0.isLoadingMore && $0.sessions.isEmpty } + |> take(1) + |> mapToSignal { _ in + return didAppearValue.get() + |> filter { $0 } + |> take(1) + } + |> deliverOnMainQueue).start(next: { _ in + dismissImpl?() + }) + } + + let mode = ValuePromise(websitesOnly ? .websites : .sessions) + + let arguments = RecentSessionsControllerArguments(context: context, setSessionIdWithRevealedOptions: { sessionId, fromSessionId in updateState { state in if (sessionId == nil && fromSessionId == state.sessionIdWithRevealedOptions) || (sessionId != nil && fromSessionId == nil) { return state.withUpdatedSessionIdWithRevealedOptions(sessionId) @@ -444,7 +500,7 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont } }, removeSession: { sessionId in let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -475,7 +531,7 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, terminateOtherSessions: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -509,33 +565,11 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont return $0.withUpdatedRemovingSessionId(sessionId) } - let applySessions: Signal = websitesPromise.get() - |> filter { $0 != nil } - |> take(1) - |> deliverOnMainQueue - |> mapToSignal { websitesAndPeers -> Signal in - if let websites = websitesAndPeers?.0, let peers = websitesAndPeers?.1 { - var updatedWebsites = websites - for i in 0 ..< updatedWebsites.count { - if updatedWebsites[i].hash == sessionId { - updatedWebsites.remove(at: i) - break - } - } - - if updatedWebsites.isEmpty { - mode.set(.sessions) - } - websitesPromise.set(.single((updatedWebsites, peers))) - } - - return .complete() - } - - removeSessionDisposable.set(((terminateWebSession(network: context.account.network, hash: sessionId) - |> mapToSignal { _ -> Signal in - return .complete() - }) |> then(applySessions) |> deliverOnMainQueue).start(error: { _ in + removeSessionDisposable.set(((webSessionsContext.remove(hash: sessionId) + |> mapToSignal { _ -> Signal in + return .complete() + }) + |> deliverOnMainQueue).start(error: { _ in updateState { return $0.withUpdatedRemovingSessionId(nil) } @@ -546,7 +580,7 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont })) }, terminateAllWebSessions: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -559,7 +593,8 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont return $0.withUpdatedTerminatingOtherSessions(true) } - terminateOtherSessionsDisposable.set((terminateAllWebSessions(network: context.account.network) |> deliverOnMainQueue).start(error: { _ in + terminateOtherSessionsDisposable.set((webSessionsContext.removeAll() + |> deliverOnMainQueue).start(error: { _ in updateState { return $0.withUpdatedTerminatingOtherSessions(false) } @@ -568,26 +603,38 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont return $0.withUpdatedTerminatingOtherSessions(false) } mode.set(.sessions) - websitesPromise.set(.single(([], [:]))) })) }) - ]), + ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, addDevice: { + pushControllerImpl?(AuthDataTransferSplashScreen(context: context, activeSessionsContext: activeSessionsContext)) + }, openOtherAppsUrl: { + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://desktop.telegram.org", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) }) - let websitesSignal: Signal<([WebAuthorization], [PeerId : Peer])?, NoError> = .single(nil) |> then(webSessions(network: context.account.network) |> map(Optional.init)) - websitesPromise.set(websitesSignal) - let previousMode = Atomic(value: .sessions) - let signal = combineLatest(context.sharedContext.presentationData, mode.get(), statePromise.get(), activeSessionsContext.state, websitesPromise.get()) + let enableQRLogin = 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 + + let signal = combineLatest(context.sharedContext.presentationData, mode.get(), statePromise.get(), activeSessionsContext.state, webSessionsContext.state, enableQRLogin) |> deliverOnMainQueue - |> map { presentationData, mode, state, sessionsState, websitesAndPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, mode, state, sessionsState, websitesAndPeers, enableQRLogin -> (ItemListControllerState, (ItemListNodeState, Any)) in var rightNavigationButton: ItemListNavigationButton? - let websites = websitesAndPeers?.0 - let peers = websitesAndPeers?.1 + let websites = websitesAndPeers.sessions + let peers = websitesAndPeers.peers if sessionsState.sessions.count > 1 { if state.terminatingOtherSessions { @@ -607,19 +654,14 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont } } - var emptyStateItem: ItemListControllerEmptyStateItem? - if sessionsState.sessions.isEmpty { - emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) - } else if sessionsState.sessions.count == 1 && mode == .sessions { - emptyStateItem = RecentSessionsEmptyStateItem(theme: presentationData.theme, strings: presentationData.strings) - } + let emptyStateItem: ItemListControllerEmptyStateItem? = nil let title: ItemListControllerTitle let entries: [RecentSessionsEntry] - if let websites = websites, !websites.isEmpty { - title = .sectionControl([presentationData.strings.AuthSessions_Sessions, presentationData.strings.AuthSessions_LoggedIn], mode.rawValue) + if websitesOnly { + title = .text(presentationData.strings.AuthSessions_LoggedIn) } else { - title = .text(presentationData.strings.AuthSessions_Title) + title = .text(presentationData.strings.AuthSessions_DevicesTitle) } var animateChanges = true @@ -627,7 +669,7 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont case (.websites, let websites, let peers): entries = recentSessionsControllerEntries(presentationData: presentationData, state: state, websites: websites, peers: peers) default: - entries = recentSessionsControllerEntries(presentationData: presentationData, state: state, sessionsState: sessionsState) + entries = recentSessionsControllerEntries(presentationData: presentationData, state: state, sessionsState: sessionsState, enableQRLogin: enableQRLogin) } let previousMode = previousMode.swap(mode) @@ -638,23 +680,32 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont animateChanges = false } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: title, leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(entries: entries, style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: crossfadeState, animateChanges: animateChanges, scrollEnabled: emptyStateItem == nil) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: crossfadeState, animateChanges: animateChanges, scrollEnabled: emptyStateItem == nil) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(context: context, state: signal) + let controller = RecentSessionsControllerImpl(context: context, state: signal) controller.titleControlValueChanged = { [weak mode] index in mode?.set(index == 0 ? .sessions : .websites) } + controller.didAppear = { _ in + didAppearValue.set(true) + } presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window(.root), with: p) } } + pushControllerImpl = { [weak controller] c in + controller?.push(c) + } + dismissImpl = { [weak controller] in + controller?.dismiss() + } return controller } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift index 8ab56b4298..3c7774bcc4 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift @@ -87,7 +87,7 @@ private func stringForUserCount(_ peers: [PeerId: SelectivePrivacyPeer], strings private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { case forwardsPreviewHeader(PresentationTheme, String) - case forwardsPreview(PresentationTheme, TelegramWallpaper, PresentationFontSize, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, String, Bool, String) + case forwardsPreview(PresentationTheme, TelegramWallpaper, PresentationFontSize, PresentationChatBubbleCorners, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, String, Bool, String) case settingHeader(PresentationTheme, String) case everybody(PresentationTheme, String, Bool) case contacts(PresentationTheme, String, Bool) @@ -194,8 +194,8 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { } else { return false } - case let .forwardsPreview(lhsTheme, lhsWallpaper, lhsFontSize, lhsStrings, lhsTimeFormat, lhsNameOrder, lhsPeerName, lhsLinkEnabled, lhsTooltipText): - if case let .forwardsPreview(rhsTheme, rhsWallpaper, rhsFontSize, rhsStrings, rhsTimeFormat, rhsNameOrder, rhsPeerName, rhsLinkEnabled, rhsTooltipText) = rhs, lhsTheme === rhsTheme, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsStrings === rhsStrings, lhsTimeFormat == rhsTimeFormat, lhsNameOrder == rhsNameOrder, lhsPeerName == rhsPeerName, lhsLinkEnabled == rhsLinkEnabled, lhsTooltipText == rhsTooltipText { + case let .forwardsPreview(lhsTheme, lhsWallpaper, lhsFontSize, lhsChatBubbleCorners, lhsStrings, lhsTimeFormat, lhsNameOrder, lhsPeerName, lhsLinkEnabled, lhsTooltipText): + if case let .forwardsPreview(rhsTheme, rhsWallpaper, rhsFontSize, rhsChatBubbleCorners, rhsStrings, rhsTimeFormat, rhsNameOrder, rhsPeerName, rhsLinkEnabled, rhsTooltipText) = rhs, lhsTheme === rhsTheme, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsChatBubbleCorners == rhsChatBubbleCorners, lhsStrings === rhsStrings, lhsTimeFormat == rhsTimeFormat, lhsNameOrder == rhsNameOrder, lhsPeerName == rhsPeerName, lhsLinkEnabled == rhsLinkEnabled, lhsTooltipText == rhsTooltipText { return true } else { return false @@ -345,85 +345,85 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! SelectivePrivacySettingsControllerArguments switch self { case let .forwardsPreviewHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, multiline: true, sectionId: self.section) - case let .forwardsPreview(theme, wallpaper, fontSize, strings, dateTimeFormat, nameDisplayOrder, peerName, linkEnabled, tooltipText): - return ForwardPrivacyChatPreviewItem(context: arguments.context, theme: theme, strings: strings, sectionId: self.section, fontSize: fontSize, wallpaper: wallpaper, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, peerName: peerName, linkEnabled: linkEnabled, tooltipText: tooltipText) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section) + case let .forwardsPreview(theme, wallpaper, fontSize, chatBubbleCorners, strings, dateTimeFormat, nameDisplayOrder, peerName, linkEnabled, tooltipText): + return ForwardPrivacyChatPreviewItem(context: arguments.context, theme: theme, strings: strings, sectionId: self.section, fontSize: fontSize, chatBubbleCorners: chatBubbleCorners, wallpaper: wallpaper, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, peerName: peerName, linkEnabled: linkEnabled, tooltipText: tooltipText) case let .settingHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, multiline: true, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section) case let .everybody(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateType(.everybody) }) case let .contacts(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateType(.contacts) }) case let .nobody(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateType(.nobody) }) case let .settingInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .exceptionsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .disableFor(theme, title, value): - return ItemListDisclosureItem(theme: theme, title: title, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: title, label: value, sectionId: self.section, style: .blocks, action: { arguments.openSelective(.main, false) }) case let .enableFor(theme, title, value): - return ItemListDisclosureItem(theme: theme, title: title, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: title, label: value, sectionId: self.section, style: .blocks, action: { arguments.openSelective(.main, true) }) case let .peersInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .callsP2PHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .callsP2PAlways(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateCallP2PMode?(.everybody) }) case let .callsP2PContacts(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateCallP2PMode?(.contacts) }) case let .callsP2PNever(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateCallP2PMode?(.nobody) }) case let .callsP2PInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .callsP2PDisableFor(theme, title, value): - return ItemListDisclosureItem(theme: theme, title: title, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: title, label: value, sectionId: self.section, style: .blocks, action: { arguments.openSelective(.callP2P, false) }) case let .callsP2PEnableFor(theme, title, value): - return ItemListDisclosureItem(theme: theme, title: title, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: title, label: value, sectionId: self.section, style: .blocks, action: { arguments.openSelective(.callP2P, true) }) case let .callsP2PPeersInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .callsIntegrationEnabled(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.updateCallIntegrationEnabled?(value) }) case let .callsIntegrationInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .phoneDiscoveryHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .phoneDiscoveryEverybody(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updatePhoneDiscovery?(true) }) case let .phoneDiscoveryMyContacts(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updatePhoneDiscovery?(false) }) case let .phoneDiscoveryInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -591,7 +591,7 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present linkEnabled = false } entries.append(.forwardsPreviewHeader(presentationData.theme, presentationData.strings.Privacy_Forwards_Preview)) - entries.append(.forwardsPreview(presentationData.theme, presentationData.chatWallpaper, presentationData.fontSize, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, peerName, linkEnabled, tootipText)) + entries.append(.forwardsPreview(presentationData.theme, presentationData.chatWallpaper, presentationData.chatFontSize, presentationData.chatBubbleCorners, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, peerName, linkEnabled, tootipText)) } entries.append(.settingHeader(presentationData.theme, settingTitle)) @@ -954,8 +954,8 @@ func selectivePrivacySettingsController(context: AccountContext, kind: Selective case .phoneNumber: title = presentationData.strings.Privacy_PhoneNumber } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: selectivePrivacySettingsControllerEntries(presentationData: presentationData, kind: kind, state: state, peerName: peerName ?? ""), style: .blocks, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: selectivePrivacySettingsControllerEntries(presentationData: presentationData, kind: kind, state: state, peerName: peerName ?? ""), style: .blocks, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift index 459fddb8f1..47875e24b9 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift @@ -14,15 +14,15 @@ import ItemListPeerItem import ItemListPeerActionItem private final class SelectivePrivacyPeersControllerArguments { - let account: Account + let context: AccountContext let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let removePeer: (PeerId) -> Void let addPeer: () -> Void let openPeer: (PeerId) -> Void - init(account: Account, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, addPeer: @escaping () -> Void, openPeer: @escaping (PeerId) -> Void) { - self.account = account + init(context: AccountContext, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, addPeer: @escaping () -> Void, openPeer: @escaping (PeerId) -> Void) { + self.context = context self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.removePeer = removePeer self.addPeer = addPeer @@ -148,7 +148,7 @@ private enum SelectivePrivacyPeersEntry: ItemListNodeEntry { } } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! SelectivePrivacyPeersControllerArguments switch self { case let .peerItem(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer, editing, enabled): @@ -167,7 +167,7 @@ private enum SelectivePrivacyPeersEntry: ItemListNodeEntry { } } } - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: { + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: { arguments.openPeer(peer.peer.id) }, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) @@ -175,7 +175,7 @@ private enum SelectivePrivacyPeersEntry: ItemListNodeEntry { arguments.removePeer(peerId) }) case let .addItem(theme, text, editing): - return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, editing: editing, action: { + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, editing: editing, action: { arguments.addPeer() }) } @@ -253,7 +253,7 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri return Array(initialPeers.values) }) - let arguments = SelectivePrivacyPeersControllerArguments(account: context.account, setPeerIdWithRevealedOptions: { peerId, fromPeerId in + let arguments = SelectivePrivacyPeersControllerArguments(context: context, setPeerIdWithRevealedOptions: { peerId, fromPeerId in updateState { state in if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { return state.withUpdatedPeerIdWithRevealedOptions(peerId) @@ -341,7 +341,7 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri return transaction.getPeer(peerId) } |> deliverOnMainQueue).start(next: { peer in - guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) else { + guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) else { return } pushControllerImpl?(controller) @@ -373,8 +373,8 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri let previous = previousPeers previousPeers = peers - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(entries: selectivePrivacyPeersControllerEntries(presentationData: presentationData, state: state, peers: peers), style: .blocks, emptyStateItem: nil, animateChanges: previous != nil && previous!.count >= peers.count) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: selectivePrivacyPeersControllerEntries(presentationData: presentationData, state: state, peers: peers), style: .blocks, emptyStateItem: nil, animateChanges: previous != nil && previous!.count >= peers.count) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationPasswordEntryController.swift b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationPasswordEntryController.swift index c02f94bf8b..6a4ab372c1 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationPasswordEntryController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationPasswordEntryController.swift @@ -121,33 +121,33 @@ private enum TwoStepVerificationPasswordEntryEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! TwoStepVerificationPasswordEntryControllerArguments switch self { case let .passwordEntryTitle(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .passwordEntry(theme, strings, text): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: "", type: .password, spacing: 0.0, tag: TwoStepVerificationPasswordEntryTag.input, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: "", type: .password, spacing: 0.0, tag: TwoStepVerificationPasswordEntryTag.input, sectionId: self.section, textUpdated: { updatedText in arguments.updateEntryText(updatedText) }, action: { arguments.next() }) case let .hintTitle(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .hintEntry(theme, strings, text): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: "", type: .password, spacing: 0.0, tag: TwoStepVerificationPasswordEntryTag.input, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: "", type: .password, spacing: 0.0, tag: TwoStepVerificationPasswordEntryTag.input, sectionId: self.section, textUpdated: { updatedText in arguments.updateEntryText(updatedText) }, action: { arguments.next() }) case let .emailEntry(theme, strings, text): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: strings.TwoStepAuth_Email, textColor: .black), text: text, placeholder: "", type: .email, spacing: 10.0, tag: TwoStepVerificationPasswordEntryTag.input, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: strings.TwoStepAuth_Email, textColor: .black), text: text, placeholder: "", type: .email, spacing: 10.0, tag: TwoStepVerificationPasswordEntryTag.input, sectionId: self.section, textUpdated: { updatedText in arguments.updateEntryText(updatedText) }, action: { arguments.next() }) case let .emailInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -433,8 +433,8 @@ func twoStepVerificationPasswordEntryController(context: AccountContext, mode: T title = presentationData.strings.TwoStepAuth_EmailTitle } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: twoStepVerificationPasswordEntryControllerEntries(presentationData: presentationData, state: state, mode: mode), style: .blocks, focusItemTag: TwoStepVerificationPasswordEntryTag.input, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: twoStepVerificationPasswordEntryControllerEntries(presentationData: presentationData, state: state, mode: mode), style: .blocks, focusItemTag: TwoStepVerificationPasswordEntryTag.input, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationResetController.swift b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationResetController.swift index 3aac1dcfb5..2f6091ea8e 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationResetController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationResetController.swift @@ -87,17 +87,17 @@ private enum TwoStepVerificationResetEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! TwoStepVerificationResetControllerArguments switch self { case let .codeEntry(theme, strings, placeholder, text): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: placeholder, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .password, spacing: 10.0, tag: TwoStepVerificationResetTag.input, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: placeholder, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .password, spacing: 10.0, tag: TwoStepVerificationResetTag.input, sectionId: self.section, textUpdated: { updatedText in arguments.updateEntryText(updatedText) }, action: { arguments.next() }) case let .codeInfo(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) } } } @@ -225,8 +225,8 @@ func twoStepVerificationResetController(context: AccountContext, emailPattern: S }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.TwoStepAuth_RecoveryTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: twoStepVerificationResetControllerEntries(presentationData: presentationData, state: state, emailPattern: emailPattern), style: .blocks, focusItemTag: TwoStepVerificationResetTag.input, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.TwoStepAuth_RecoveryTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: twoStepVerificationResetControllerEntries(presentationData: presentationData, state: state, emailPattern: emailPattern), style: .blocks, focusItemTag: TwoStepVerificationResetTag.input, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift index e2e79c9341..b05e9b0f37 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift @@ -123,57 +123,57 @@ private enum TwoStepVerificationUnlockSettingsEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! TwoStepVerificationUnlockSettingsControllerArguments switch self { case let .passwordEntry(theme, strings, text, value): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: text, textColor: theme.list.itemPrimaryTextColor), text: value, placeholder: "", type: .password, spacing: 10.0, tag: TwoStepVerificationUnlockSettingsEntryTag.password, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: text, textColor: theme.list.itemPrimaryTextColor), text: value, placeholder: "", type: .password, spacing: 10.0, tag: TwoStepVerificationUnlockSettingsEntryTag.password, sectionId: self.section, textUpdated: { updatedText in arguments.updatePasswordText(updatedText) }, action: { arguments.checkPassword() }) case let .passwordEntryInfo(theme, text): - return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section, linkAction: { action in + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { action in switch action { case .tap: arguments.openForgotPassword() } }) case let .passwordSetup(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openSetupPassword() }) case let .passwordSetupInfo(theme, text): - return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .changePassword(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openSetupPassword() }) case let .turnPasswordOff(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openDisablePassword() }) case let .setupRecoveryEmail(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openSetupEmail() }) case let .passwordInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .pendingEmailConfirmInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .pendingEmailConfirmCode(theme, strings, title, text): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: ""), text: text, placeholder: title, type: .number, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: ""), text: text, placeholder: title, type: .number, sectionId: self.section, textUpdated: { value in arguments.updateEmailCode(value) }, action: {}) case let .pendingEmailInfo(theme, text): - return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section, linkAction: { action in + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { action in switch action { case .tap: arguments.openResetPendingEmail() } }) case let .pendingEmailOpenConfirm(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openConfirmEmail() }) } @@ -808,8 +808,8 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: } } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: twoStepVerificationUnlockSettingsControllerEntries(presentationData: presentationData, state: state, data: data), style: .blocks, focusItemTag: didAppear ? TwoStepVerificationUnlockSettingsEntryTag.password : nil, emptyStateItem: emptyStateItem, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: twoStepVerificationUnlockSettingsControllerEntries(presentationData: presentationData, state: state, data: data), style: .blocks, focusItemTag: didAppear ? TwoStepVerificationUnlockSettingsEntryTag.password : nil, emptyStateItem: emptyStateItem, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchItem.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchItem.swift index 929ebd35a0..52529e0577 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchItem.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchItem.swift @@ -76,12 +76,14 @@ final class SettingsSearchItem: ItemListControllerSearch { let archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError> let privacySettings: Signal let hasWallet: Signal + let activeSessionsContext: Signal + let webSessionsContext: Signal private var updateActivity: ((Bool) -> Void)? private var activity: ValuePromise = ValuePromise(ignoreRepeated: false) private let activityDisposable = MetaDisposable() - init(context: AccountContext, theme: PresentationTheme, placeholder: String, activated: Bool, updateActivated: @escaping (Bool) -> Void, presentController: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, getNavigationController: (() -> NavigationController?)?, exceptionsList: Signal, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal, hasWallet: Signal) { + init(context: AccountContext, theme: PresentationTheme, placeholder: String, activated: Bool, updateActivated: @escaping (Bool) -> Void, presentController: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, getNavigationController: (() -> NavigationController?)?, exceptionsList: Signal, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal, hasWallet: Signal, activeSessionsContext: Signal, webSessionsContext: Signal) { self.context = context self.theme = theme self.placeholder = placeholder @@ -94,6 +96,8 @@ final class SettingsSearchItem: ItemListControllerSearch { self.archivedStickerPacks = archivedStickerPacks self.privacySettings = privacySettings self.hasWallet = hasWallet + self.activeSessionsContext = activeSessionsContext + self.webSessionsContext = webSessionsContext self.activityDisposable.set((activity.get() |> mapToSignal { value -> Signal in if value { return .single(value) |> delay(0.2, queue: Queue.mainQueue()) @@ -157,7 +161,7 @@ final class SettingsSearchItem: ItemListControllerSearch { pushController(c) }, presentController: { c, a in presentController(c, a) - }, getNavigationController: self.getNavigationController, exceptionsList: self.exceptionsList, archivedStickerPacks: self.archivedStickerPacks, privacySettings: self.privacySettings, hasWallet: self.hasWallet) + }, getNavigationController: self.getNavigationController, exceptionsList: self.exceptionsList, archivedStickerPacks: self.archivedStickerPacks, privacySettings: self.privacySettings, hasWallet: self.hasWallet, activeSessionsContext: self.activeSessionsContext, webSessionsContext: self.webSessionsContext) } } } @@ -331,7 +335,7 @@ private final class SettingsSearchContainerNode: SearchDisplayControllerContentN private var presentationDataDisposable: Disposable? private let presentationDataPromise: Promise - init(context: AccountContext, openResult: @escaping (SettingsSearchableItem) -> Void, exceptionsList: Signal, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal, hasWallet: Signal) { + init(context: AccountContext, openResult: @escaping (SettingsSearchableItem) -> Void, exceptionsList: Signal, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal, hasWallet: Signal, activeSessionsContext: Signal, webSessionsContext: Signal) { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationDataPromise = Promise(self.presentationData) @@ -355,7 +359,7 @@ private final class SettingsSearchContainerNode: SearchDisplayControllerContentN }) let searchableItems = Promise<[SettingsSearchableItem]>() - searchableItems.set(settingsSearchableItems(context: context, notificationExceptionsList: exceptionsList, archivedStickerPacks: archivedStickerPacks, privacySettings: privacySettings, hasWallet: hasWallet)) + searchableItems.set(settingsSearchableItems(context: context, notificationExceptionsList: exceptionsList, archivedStickerPacks: archivedStickerPacks, privacySettings: privacySettings, hasWallet: hasWallet, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext)) let queryAndFoundItems = combineLatest(searchableItems.get(), faqSearchableItems(context: context)) |> mapToSignal { searchableItems, faqSearchableItems -> Signal<(String, [SettingsSearchableItem])?, NoError> in @@ -568,27 +572,7 @@ private final class SettingsSearchContainerNode: SearchDisplayControllerContentN override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: nil) - } + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight @@ -596,10 +580,10 @@ private final class SettingsSearchContainerNode: SearchDisplayControllerContentN insets.right += layout.safeInsets.right self.recentListNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if !self.hasValidLayout { self.hasValidLayout = true @@ -639,10 +623,12 @@ private final class SettingsSearchItemNode: ItemListControllerSearchNode { let archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError> let privacySettings: Signal let hasWallet: Signal + let activeSessionsContext: Signal + let webSessionsContext: Signal var cancel: () -> Void - init(context: AccountContext, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: (() -> NavigationController?)?, exceptionsList: Signal, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal, hasWallet: Signal) { + init(context: AccountContext, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: (() -> NavigationController?)?, exceptionsList: Signal, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal, hasWallet: Signal, activeSessionsContext: Signal, webSessionsContext: Signal) { self.context = context self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.cancel = cancel @@ -653,6 +639,8 @@ private final class SettingsSearchItemNode: ItemListControllerSearchNode { self.archivedStickerPacks = archivedStickerPacks self.privacySettings = privacySettings self.hasWallet = hasWallet + self.activeSessionsContext = activeSessionsContext + self.webSessionsContext = webSessionsContext super.init() } @@ -694,7 +682,7 @@ private final class SettingsSearchItemNode: ItemListControllerSearchNode { } }) } - }, exceptionsList: self.exceptionsList, archivedStickerPacks: self.archivedStickerPacks, privacySettings: self.privacySettings, hasWallet: self.hasWallet), cancel: { [weak self] in + }, exceptionsList: self.exceptionsList, archivedStickerPacks: self.archivedStickerPacks, privacySettings: self.privacySettings, hasWallet: self.hasWallet, activeSessionsContext: self.activeSessionsContext, webSessionsContext: self.webSessionsContext), cancel: { [weak self] in self?.cancel() }) diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index 98ae0b2a24..43bdedb823 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -15,8 +15,7 @@ import CallListUI import NotificationSoundSelectionUI import PresentationDataUtils import PhoneNumberFormat - -private let maximumNumberOfAccounts = 3 +import AccountUtils enum SettingsSearchableItemIcon { case profile @@ -428,7 +427,7 @@ private func notificationSearchableItems(context: AccountContext, settings: Glob ] } -private func privacySearchableItems(context: AccountContext, privacySettings: AccountPrivacySettings?) -> [SettingsSearchableItem] { +private func privacySearchableItems(context: AccountContext, privacySettings: AccountPrivacySettings?, activeSessionsContext: ActiveSessionsContext?, webSessionsContext: WebSessionsContext?) -> [SettingsSearchableItem] { let icon: SettingsSearchableItemIcon = .privacy let strings = context.sharedContext.currentPresentationData.with { $0 }.strings @@ -504,7 +503,7 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac passcodeAlternate = synonyms(strings.SettingsSearch_Synonyms_Privacy_Passcode) } - return [ + return ([ SettingsSearchableItem(id: .privacy(0), title: strings.Settings_PrivacySettings, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Title), icon: icon, breadcrumbs: [], present: { context, _, present in presentPrivacySettings(context, present, nil) }), @@ -528,7 +527,7 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac }), SettingsSearchableItem(id: .privacy(7), title: passcodeTitle, alternate: passcodeAlternate, icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in let _ = passcodeOptionsAccessController(context: context, pushController: { c in - + present(.push, c) }, completion: { animated in let controller = passcodeOptionsController(context: context) if animated { @@ -538,42 +537,44 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac } }).start(next: { controller in if let controller = controller { - present(.modal, controller) + present(.push, controller) } }) }), SettingsSearchableItem(id: .privacy(8), title: strings.PrivacySettings_TwoStepAuth, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_TwoStepAuth), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in present(.push, twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: true, data: nil))) }), - SettingsSearchableItem(id: .privacy(9), title: strings.PrivacySettings_AuthSessions, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_AuthSessions), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in - present(.push, recentSessionsController(context: context, activeSessionsContext: ActiveSessionsContext(account: context.account))) + activeSessionsContext == nil ? nil : SettingsSearchableItem(id: .privacy(9), title: strings.Settings_Devices, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_AuthSessions) + [strings.PrivacySettings_AuthSessions], icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in + present(.push, recentSessionsController(context: context, activeSessionsContext: activeSessionsContext!, webSessionsContext: webSessionsContext ?? WebSessionsContext(account: context.account), websitesOnly: false)) }), - SettingsSearchableItem(id: .privacy(10), title: strings.PrivacySettings_DeleteAccountTitle, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_DeleteAccountIfAwayFor), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in + webSessionsContext == nil ? nil : SettingsSearchableItem(id: .privacy(10), title: strings.PrivacySettings_WebSessions, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_AuthSessions), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in + present(.push, recentSessionsController(context: context, activeSessionsContext: activeSessionsContext ?? ActiveSessionsContext(account: context.account), webSessionsContext: webSessionsContext ?? WebSessionsContext(account: context.account), websitesOnly: true)) + }), + SettingsSearchableItem(id: .privacy(11), title: strings.PrivacySettings_DeleteAccountTitle, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_DeleteAccountIfAwayFor), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in presentPrivacySettings(context, present, .accountTimeout) }), - SettingsSearchableItem(id: .privacy(11), title: strings.PrivacySettings_DataSettings, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_Title), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in + SettingsSearchableItem(id: .privacy(12), title: strings.PrivacySettings_DataSettings, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_Title), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in presentDataPrivacySettings(context, present) }), - - SettingsSearchableItem(id: .privacy(12), title: strings.Privacy_ContactsReset, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_ContactsReset), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], present: { context, _, present in + SettingsSearchableItem(id: .privacy(13), title: strings.Privacy_ContactsReset, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_ContactsReset), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], present: { context, _, present in presentDataPrivacySettings(context, present) }), - SettingsSearchableItem(id: .privacy(13), title: strings.Privacy_ContactsSync, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_ContactsSync), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], present: { context, _, present in + SettingsSearchableItem(id: .privacy(14), title: strings.Privacy_ContactsSync, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_ContactsSync), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], present: { context, _, present in presentDataPrivacySettings(context, present) }), - SettingsSearchableItem(id: .privacy(14), title: strings.Privacy_TopPeers, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_TopPeers), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], present: { context, _, present in + SettingsSearchableItem(id: .privacy(15), title: strings.Privacy_TopPeers, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_TopPeers), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], present: { context, _, present in presentDataPrivacySettings(context, present) }), - SettingsSearchableItem(id: .privacy(15), title: strings.Privacy_DeleteDrafts, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_DeleteDrafts), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], present: { context, _, present in + SettingsSearchableItem(id: .privacy(16), title: strings.Privacy_DeleteDrafts, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_DeleteDrafts), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], present: { context, _, present in presentDataPrivacySettings(context, present) }), - SettingsSearchableItem(id: .privacy(16), title: strings.Privacy_PaymentsClearInfo, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_ClearPaymentsInfo), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], present: { context, _, present in + SettingsSearchableItem(id: .privacy(17), title: strings.Privacy_PaymentsClearInfo, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_ClearPaymentsInfo), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], present: { context, _, present in presentDataPrivacySettings(context, present) }), - SettingsSearchableItem(id: .privacy(17), title: strings.Privacy_SecretChatsLinkPreviews, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_SecretChatLinkPreview), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings, strings.Privacy_SecretChatsTitle], present: { context, _, present in + SettingsSearchableItem(id: .privacy(18), title: strings.Privacy_SecretChatsLinkPreviews, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_SecretChatLinkPreview), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings, strings.Privacy_SecretChatsTitle], present: { context, _, present in presentDataPrivacySettings(context, present) }) - ] + ] as [SettingsSearchableItem?]).compactMap { $0 } } private func dataSearchableItems(context: AccountContext) -> [SettingsSearchableItem] { @@ -676,7 +677,7 @@ private func appearanceSearchableItems(context: AccountContext) -> [SettingsSear SettingsSearchableItem(id: .appearance(0), title: strings.Settings_Appearance, alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_Title), icon: icon, breadcrumbs: [], present: { context, _, present in presentAppearanceSettings(context, present, nil) }), - SettingsSearchableItem(id: .appearance(1), title: strings.Appearance_TextSize, alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_TextSize), icon: icon, breadcrumbs: [strings.Settings_Appearance], present: { context, _, present in + SettingsSearchableItem(id: .appearance(1), title: strings.Appearance_TextSizeSetting, alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_TextSize), icon: icon, breadcrumbs: [strings.Settings_Appearance], present: { context, _, present in presentAppearanceSettings(context, present, .fontSize) }), SettingsSearchableItem(id: .appearance(2), title: strings.Settings_ChatBackground, alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_ChatBackground), icon: icon, breadcrumbs: [strings.Settings_Appearance], present: { context, _, present in @@ -735,7 +736,7 @@ private func languageSearchableItems(context: AccountContext, localizations: [Lo return items } -func settingsSearchableItems(context: AccountContext, notificationExceptionsList: Signal, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal, hasWallet: Signal) -> Signal<[SettingsSearchableItem], NoError> { +func settingsSearchableItems(context: AccountContext, notificationExceptionsList: Signal, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal, hasWallet: Signal, activeSessionsContext: Signal, webSessionsContext: Signal) -> Signal<[SettingsSearchableItem], NoError> { let watchAppInstalled = (context.watchManager?.watchAppInstalled ?? .single(false)) |> take(1) @@ -812,8 +813,27 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList } } - return combineLatest(watchAppInstalled, canAddAccount, localizations, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings, hasWallet) - |> map { watchAppInstalled, canAddAccount, localizations, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings, hasWallet in + let activeWebSessionsContext = webSessionsContext + |> mapToSignal { webSessionsContext -> Signal in + if let webSessionsContext = webSessionsContext { + return webSessionsContext.state + |> map { state -> WebSessionsContext? in + if !state.sessions.isEmpty { + return webSessionsContext + } else { + return nil + } + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs !== rhs + }) + } else { + return .single(nil) + } + } + + return combineLatest(watchAppInstalled, canAddAccount, localizations, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings, hasWallet, activeSessionsContext, activeWebSessionsContext) + |> map { watchAppInstalled, canAddAccount, localizations, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings, hasWallet, activeSessionsContext, activeWebSessionsContext in let strings = context.sharedContext.currentPresentationData.with { $0 }.strings var allItems: [SettingsSearchableItem] = [] @@ -835,7 +855,7 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList let notificationItems = notificationSearchableItems(context: context, settings: notificationSettings, exceptionsList: notificationExceptionsList) allItems.append(contentsOf: notificationItems) - let privacyItems = privacySearchableItems(context: context, privacySettings: privacySettings) + let privacyItems = privacySearchableItems(context: context, privacySettings: privacySettings, activeSessionsContext: activeSessionsContext, webSessionsContext: activeWebSessionsContext) allItems.append(contentsOf: privacyItems) let dataItems = dataSearchableItems(context: context) @@ -862,6 +882,7 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList }) allItems.append(passport) + #if ENABLE_WALLET if hasWallet { let wallet = SettingsSearchableItem(id: .wallet(0), title: strings.Settings_Wallet, alternate: synonyms(strings.SettingsSearch_Synonyms_Wallet), icon: .wallet, breadcrumbs: [], present: { context, _, present in context.sharedContext.openWallet(context: context, walletContext: .generic, present: { c in @@ -870,6 +891,7 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList }) allItems.append(wallet) } + #endif let support = SettingsSearchableItem(id: .support(0), title: strings.Settings_Support, alternate: synonyms(strings.SettingsSearch_Synonyms_Support), icon: .support, breadcrumbs: [], present: { context, _, present in let _ = (supportPeerId(account: context.account) @@ -888,7 +910,7 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, present: { controller, arguments in present(.push, controller) - }, dismissInput: {}) + }, dismissInput: {}, contentContext: nil) }) }) allItems.append(faq) diff --git a/submodules/SettingsUI/Sources/SettingsController.swift b/submodules/SettingsUI/Sources/SettingsController.swift index 6e008b0e47..f9ed1cf03f 100644 --- a/submodules/SettingsUI/Sources/SettingsController.swift +++ b/submodules/SettingsUI/Sources/SettingsController.swift @@ -17,7 +17,6 @@ import AccountContext import OverlayStatusController import AvatarNode import AlertUI -import PresentationDataUtils import TelegramNotices import GalleryUI import LegacyUI @@ -33,10 +32,12 @@ import PeerAvatarGalleryUI import MapResourceToAvatarSizes import AppBundle import ContextUI +#if ENABLE_WALLET import WalletUI +#endif import PhoneNumberFormat - -private let maximumNumberOfAccounts = 3 +import AccountUtils +import AuthTransferUI private let avatarFont = avatarPlaceholderFont(size: 13.0) @@ -81,7 +82,7 @@ private indirect enum SettingsEntryTag: Equatable, ItemListItemTag { } private final class SettingsItemArguments { - let accountManager: AccountManager + let sharedContext: SharedAccountContext let avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext let avatarTapAction: () -> Void @@ -112,9 +113,10 @@ private final class SettingsItemArguments { let keepPhone: () -> Void let openPhoneNumberChange: () -> Void let accountContextAction: (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void + let openDevices: () -> Void init( - accountManager: AccountManager, + sharedContext: SharedAccountContext, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, avatarTapAction: @escaping () -> Void, @@ -144,9 +146,10 @@ private final class SettingsItemArguments { removeAccount: @escaping (AccountRecordId) -> Void, keepPhone: @escaping () -> Void, openPhoneNumberChange: @escaping () -> Void, - accountContextAction: @escaping (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void + accountContextAction: @escaping (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void, + openDevices: @escaping () -> Void ) { - self.accountManager = accountManager + self.sharedContext = sharedContext self.avatarAndNameInfoContext = avatarAndNameInfoContext self.avatarTapAction = avatarTapAction @@ -177,6 +180,7 @@ private final class SettingsItemArguments { self.keepPhone = keepPhone self.openPhoneNumberChange = openPhoneNumberChange self.accountContextAction = accountContextAction + self.openDevices = openDevices } } @@ -205,9 +209,12 @@ private indirect enum SettingsEntry: ItemListNodeEntry { case proxy(PresentationTheme, UIImage?, String, String) + case devices(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?) @@ -215,7 +222,9 @@ private indirect enum SettingsEntry: ItemListNodeEntry { 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) @@ -223,22 +232,28 @@ private indirect enum SettingsEntry: ItemListNodeEntry { 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 .savedMessages, .recentCalls, .stickers: - return SettingsSection.media.rawValue - case .notificationsAndSounds, .privacyAndSecurity, .dataAndStorage, .themes, .language: - return SettingsSection.generalSettings.rawValue - case .passport, .wallet, .watch : - return SettingsSection.advanced.rawValue - case .askAQuestion, .faq: - return SettingsSection.help.rawValue + 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: + 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 } } @@ -268,26 +283,32 @@ private indirect enum SettingsEntry: ItemListNodeEntry { return 1005 case .stickers: return 1006 - case .notificationsAndSounds: + case .devices: return 1007 - case .privacyAndSecurity: + case .notificationsAndSounds: return 1008 - case .dataAndStorage: + case .privacyAndSecurity: return 1009 - case .themes: + case .dataAndStorage: return 1010 - case .language: + case .themes: return 1011 - case .wallet: + case .language: return 1012 - case .passport: + case .contentStickers: return 1013 - case .watch: + #if ENABLE_WALLET + case .wallet: return 1014 - case .askAQuestion: + #endif + case .passport: return 1015 - case .faq: + case .watch: return 1016 + case .askAQuestion: + return 1017 + case .faq: + return 1018 } } @@ -379,6 +400,12 @@ private indirect enum SettingsEntry: ItemListNodeEntry { } 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 .savedMessages(lhsTheme, lhsImage, lhsText): if case let .savedMessages(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true @@ -397,6 +424,12 @@ private indirect enum SettingsEntry: ItemListNodeEntry { } 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 @@ -433,12 +466,14 @@ private indirect enum SettingsEntry: ItemListNodeEntry { } 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 @@ -464,11 +499,11 @@ private indirect enum SettingsEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + 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(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, withExtendedBottomInset: false), editingNameUpdated: { _ in + 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: { @@ -477,25 +512,25 @@ private indirect enum SettingsEntry: ItemListNodeEntry { arguments.displayCopyContextMenu() }) case let .setProfilePhoto(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + 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(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + 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(theme: theme, title: title, text: .markdown(text), style: .blocks, sectionId: self.section, linkAction: { action in + 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(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + 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(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + 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): @@ -503,7 +538,7 @@ private indirect enum SettingsEntry: ItemListNodeEntry { if badgeCount > 0 { label = .badge(compactNumericCountString(Int(badgeCount), decimalSeparator: dateTimeFormat.decimalSeparator)) } - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .dayFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: ""), nameDisplayOrder: .firstLast, 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: { + 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? @@ -521,63 +556,73 @@ private indirect enum SettingsEntry: ItemListNodeEntry { arguments.accountContextAction(account.id, node, gesture) }, tag: SettingsEntryTag.account(account.id)) case let .addAccount(theme, text): - return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: { + 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(theme: theme, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + 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 .savedMessages(theme, image, text): - return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + 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(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + 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(theme: theme, icon: image, title: text, label: value, labelStyle: .badge(theme.list.itemAccentColor), sectionId: ItemListSectionId(self.section), style: .blocks, action: { + 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(theme: theme, icon: image, title: text, label: warning ? "!" : "", labelStyle: warning ? .badge(theme.list.itemDestructiveColor) : .text, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + 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(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + 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(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + 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(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + 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(theme: theme, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + 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(theme: theme, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + 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(theme: theme, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + 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(theme: theme, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + 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(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + 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(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openFaq(nil) }, clearHighlightAutomatically: false) } @@ -590,7 +635,7 @@ private struct SettingsState: Equatable { 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) -> [SettingsEntry] { +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) -> [SettingsEntry] { var entries: [SettingsEntry] = [] if let peer = peerViewMainPeer(view) as? TelegramUser { @@ -638,7 +683,11 @@ private func settingsEntries(account: Account, presentationData: PresentationDat entries.append(.savedMessages(presentationData.theme, PresentationResourcesSettings.savedMessages, presentationData.strings.Settings_SavedMessages)) entries.append(.recentCalls(presentationData.theme, PresentationResourcesSettings.recentCalls, presentationData.strings.CallSettings_RecentCalls)) - entries.append(.stickers(presentationData.theme, PresentationResourcesSettings.stickers, presentationData.strings.ChatSettings_Stickers, unreadTrendingStickerPacks == 0 ? "" : "\(unreadTrendingStickerPacks)", archivedPacks)) + 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)")) + } let notificationsWarning = shouldDisplayNotificationsPermissionWarning(status: notificationsAuthorizationStatus, suppressed: notificationsWarningSuppressed) entries.append(.notificationsAndSounds(presentationData.theme, PresentationResourcesSettings.notifications, presentationData.strings.Settings_NotificationsAndSounds, notifyExceptions, notificationsWarning)) @@ -647,10 +696,13 @@ private func settingsEntries(account: Account, presentationData: PresentationDat 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, "")) } @@ -693,12 +745,11 @@ private final class SettingsControllerImpl: ItemListController, SettingsControll self.contextValue.set(.single(currentContext)) let updatedPresentationData = self.contextValue.get() - |> mapToSignal { context -> Signal<(theme: PresentationTheme, strings: PresentationStrings), NoError> in + |> mapToSignal { context -> Signal in return context.sharedContext.presentationData - |> map { ($0.theme, $0.strings) } } - super.init(theme: presentationData.theme, strings: presentationData.strings, updatedPresentationData: updatedPresentationData, state: state, tabBarItem: tabBarItem) + super.init(presentationData: ItemListPresentationData(presentationData), updatedPresentationData: updatedPresentationData |> map(ItemListPresentationData.init(_:)), state: state, tabBarItem: tabBarItem) self.accountsAndPeersDisposable = (accountsAndPeers |> deliverOnMainQueue).start(next: { [weak self] value in @@ -783,6 +834,8 @@ public func settingsController(context: AccountContext, accountManager: AccountM accountsAndPeers.set(activeAccountsAndPeers(context: context)) let privacySettings = Promise(nil) + + let enableQRLogin = Promise() let openFaq: (Promise, String?) -> Void = { resolvedUrl, customAnchor in let _ = (contextValue.get() @@ -803,7 +856,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM 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: {}) + }, dismissInput: {}, contentContext: nil) }) }) } @@ -819,7 +872,28 @@ public func settingsController(context: AccountContext, accountManager: AccountM let displayPhoneNumberConfirmation = ValuePromise(false) - let arguments = SettingsItemArguments(accountManager: accountManager, avatarAndNameInfoContext: avatarAndNameInfoContext, avatarTapAction: { + 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 arguments = SettingsItemArguments(sharedContext: context.sharedContext, avatarAndNameInfoContext: avatarAndNameInfoContext, avatarTapAction: { var updating = false updateState { updating = $0.updatingAvatar != nil @@ -876,9 +950,18 @@ public func settingsController(context: AccountContext, accountManager: AccountM let _ = (contextValue.get() |> deliverOnMainQueue |> take(1)).start(next: { context in - pushControllerImpl?(privacyAndSecurityController(context: context, initialSettings: privacySettingsValue, updatedSettings: { settings in - privacySettings.set(.single(settings)) - })) + 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)) + }, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, blockedPeersContext: blockedPeersContext, hasTwoStepAuth: hasTwoStepAuth)) + }) }) }, openDataAndStorage: { let _ = (contextValue.get() @@ -921,6 +1004,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM pushControllerImpl?(SecureIdAuthController(context: context, mode: .list)) }) }, openWallet: { + #if ENABLE_WALLET let _ = (contextValue.get() |> deliverOnMainQueue |> take(1)).start(next: { context in @@ -928,6 +1012,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM pushControllerImpl?(c) }) }) + #endif }, openWatch: { let _ = (contextValue.get() |> deliverOnMainQueue @@ -1039,13 +1124,26 @@ public func settingsController(context: AccountContext, accountManager: AccountM let presentationData = accountContext.sharedContext.currentPresentationData.with { $0 } - let contextController = ContextController(account: accountContext.account, theme: presentationData.theme, strings: presentationData.strings, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node)), items: accountContextMenuItems(context: accountContext, logout: { + 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 _ = (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)) + } + }) }) changeProfilePhotoImpl = { @@ -1171,11 +1269,14 @@ public func settingsController(context: AccountContext, accountManager: AccountM } ) ) - + #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 = { @@ -1197,6 +1298,14 @@ public func settingsController(context: AccountContext, accountManager: AccountM } 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( @@ -1282,8 +1391,26 @@ public func settingsController(context: AccountContext, accountManager: AccountM return context.account.viewTracker.featuredStickerPacks() } - 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), accountsAndPeers.get()) - |> map { context, presentationData, state, view, preferencesAndExceptions, featuredAndArchived, hasWalletPassportAndWatch, accountsAndPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in + 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 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()), 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 @@ -1292,7 +1419,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM 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)) + 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 { @@ -1318,10 +1445,10 @@ public func settingsController(context: AccountContext, accountManager: AccountM presentControllerImpl?(c, a) }, pushController: { c in pushControllerImpl?(c) - }, getNavigationController: getNavigationControllerImpl, exceptionsList: notifyExceptions.get(), archivedStickerPacks: archivedPacks.get(), privacySettings: privacySettings.get(), hasWallet: hasWallet) + }, getNavigationController: getNavigationControllerImpl, 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) = hasWalletPassportAndWatch - let listState = ItemListNodeState(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), style: .blocks, searchItem: searchItem, initialScrollToItem: ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)) + let (hasWallet, hasPassport, hasWatchApp, enableQRLogin) = 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), style: .blocks, searchItem: searchItem, initialScrollToItem: ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)) return (controllerState, (listState, arguments)) } @@ -1329,7 +1456,12 @@ public func settingsController(context: AccountContext, accountManager: AccountM actionsDisposable.dispose() } - let icon = UIImage(bundleImageName: "Chat List/Tabs/IconSettings") + 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 @@ -1367,9 +1499,10 @@ public func settingsController(context: AccountContext, accountManager: AccountM 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, peer: primary.1, authorOfMessage: nil, representation: primary.1.profileImageRepresentations.first, displayDimensions: size, inset: 3.0, emptyColor: nil, synchronousLoad: false) { + 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 { image -> (UIImage, UIImage)? in + |> 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) @@ -1390,13 +1523,13 @@ public func settingsController(context: AccountContext, accountManager: AccountM 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: primary.1.displayLetters, accountPeerId: primary.1.id, peerId: primary.1.id) + drawPeerAvatarLetters(context: context, size: CGSize(width: size.width - inset * 2.0, height: size.height - inset * 2.0), font: avatarFont, letters: primary.1.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: primary.1.displayLetters, accountPeerId: primary.1.id, peerId: primary.1.id) + drawPeerAvatarLetters(context: context, size: CGSize(width: size.width - inset * 2.0, height: size.height - inset * 2.0), font: avatarFont, letters: primary.1.displayLetters, peerId: primary.1.id) context.translateBy(x: -inset, y: -inset) context.setLineWidth(1.0) context.setStrokeColor(primary.2.rootController.tabBar.selectedIconColor.cgColor) @@ -1454,7 +1587,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM } avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { - var result: ((ASDisplayNode, () -> (UIView?, UIView?)), CGRect)? + var result: ((ASDisplayNode, CGRect, () -> (UIView?, UIView?)), CGRect)? controller.forEachItemNode { itemNode in if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { result = itemNode.avatarTransitionNode() @@ -1539,7 +1672,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM |> deliverOnMainQueue |> take(1)).start(next: { context in let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -1568,6 +1701,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM controller.didAppear = { _ in updatePassport() updateNotifyExceptions() + updateActiveSessions() } controller.previewItemWithTag = { tag in if let tag = tag as? SettingsEntryTag, case let .account(id) = tag { diff --git a/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift index b6727c786d..e7d013025b 100644 --- a/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift @@ -13,6 +13,7 @@ import OverlayStatusController import AccountContext import StickerPackPreviewUI import ItemListStickerPackItem +import UndoUI public enum ArchivedStickerPacksControllerMode { case stickers @@ -154,13 +155,13 @@ private enum ArchivedStickerPacksEntry: ItemListNodeEntry { } } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ArchivedStickerPacksControllerArguments switch self { case let .info(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .pack(_, theme, strings, info, topItem, count, animatedStickers, enabled, editing): - return ItemListStickerPackItem(theme: theme, strings: strings, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: .installation(installed: false), editing: editing, enabled: enabled, playAnimatedStickers: animatedStickers, sectionId: self.section, action: { + return ItemListStickerPackItem(presentationData: presentationData, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: .installation(installed: false), editing: editing, enabled: enabled, playAnimatedStickers: animatedStickers, sectionId: self.section, action: { arguments.openStickerPack(info) }, setPackIdWithRevealedOptions: { current, previous in arguments.setPackIdWithRevealedOptions(current, previous) @@ -248,6 +249,7 @@ public func archivedStickerPacksController(context: AccountContext, mode: Archiv } var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var navigationControllerImpl: (() -> NavigationController?)? let actionsDisposable = DisposableSet() @@ -300,24 +302,42 @@ public func archivedStickerPacksController(context: AccountContext, mode: Archiv return } let _ = (loadedStickerPack(postbox: context.account.postbox, network: context.account.network, reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false) - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError> in switch result { - case let .result(info, items, installed): - if installed { - return .complete() - } else { - return addStickerPackInteractively(postbox: context.account.postbox, info: info, items: items) - } - case .fetching: - break + case let .result(info, items, installed): + if installed { + return .complete() + } else { + return addStickerPackInteractively(postbox: context.account.postbox, info: info, items: items) + |> ignoreValues + |> mapToSignal { _ -> Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError> in + return .complete() + } + |> then(.single((info, items))) + } + case .fetching: + break case .none: break } return .complete() } - |> deliverOnMainQueue).start(completed: { + |> deliverOnMainQueue).start(next: { info, items in let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .success), nil) + + var animateInAsReplacement = false + if let navigationController = navigationControllerImpl?() { + for controller in navigationController.overlayControllers { + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitActionAndReplacementAnimation() + animateInAsReplacement = true + } + } + } + + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).0, undo: false, info: info, topItem: items.first, account: context.account), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in + return true + }), nil) let applyPacks: Signal = stickerPacks.get() |> filter { $0 != nil } @@ -414,9 +434,9 @@ public func archivedStickerPacksController(context: AccountContext, mode: Archiv emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.StickerPacksSettings_ArchivedPacks), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.StickerPacksSettings_ArchivedPacks), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(entries: archivedStickerPacksControllerEntries(presentationData: presentationData, state: state, packs: packs, installedView: installedView, stickerSettings: stickerSettings), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && packs != nil && (previous! != 0 && previous! >= packs!.count - 10)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: archivedStickerPacksControllerEntries(presentationData: presentationData, state: state, packs: packs, installedView: installedView, stickerSettings: stickerSettings), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && packs != nil && (previous! != 0 && previous! >= packs!.count - 10)) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() @@ -428,9 +448,12 @@ public func archivedStickerPacksController(context: AccountContext, mode: Archiv controller.present(c, in: .window(.root), with: p) } } - + navigationControllerImpl = { [weak controller] in + return controller?.navigationController as? NavigationController + } presentStickerPackController = { [weak controller] info in - presentControllerImpl?(StickerPackPreviewController(context: context, stickerPack: .id(id: info.id.id, accessHash: info.accessHash), mode: .settings, parentNavigationController: controller?.navigationController as? NavigationController), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) + presentControllerImpl?(StickerPackScreen(context: context, mode: .settings, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controller?.navigationController as? NavigationController), nil) } return controller diff --git a/submodules/SettingsUI/Sources/Stickers/FeaturedStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/FeaturedStickerPacksController.swift index ae1c2cd704..febff7b0b1 100644 --- a/submodules/SettingsUI/Sources/Stickers/FeaturedStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/FeaturedStickerPacksController.swift @@ -117,11 +117,11 @@ private enum FeaturedStickerPacksEntry: ItemListNodeEntry { } } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! FeaturedStickerPacksControllerArguments switch self { case let .pack(_, theme, strings, info, unread, topItem, count, playAnimatedStickers, installed): - return ItemListStickerPackItem(theme: theme, strings: strings, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: unread, control: .installation(installed: installed), editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false, reorderable: false), enabled: true, playAnimatedStickers: playAnimatedStickers, sectionId: self.section, action: { + return ItemListStickerPackItem(presentationData: presentationData, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: unread, control: .installation(installed: installed), editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false, reorderable: false), enabled: true, playAnimatedStickers: playAnimatedStickers, sectionId: self.section, action: { arguments.openStickerPack(info) }, setPackIdWithRevealedOptions: { _, _ in }, addPack: { @@ -226,9 +226,9 @@ public func featuredStickerPacksController(context: AccountContext) -> ViewContr let previous = previousPackCount previousPackCount = packCount - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.FeaturedStickerPacks_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.FeaturedStickerPacks_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(entries: featuredStickerPacksControllerEntries(presentationData: presentationData, state: state, view: view, featured: featured, unreadPacks: initialUnreadPacks, stickerSettings: stickerSettings), style: .blocks, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: featuredStickerPacksControllerEntries(presentationData: presentationData, state: state, view: view, featured: featured, unreadPacks: initialUnreadPacks, stickerSettings: stickerSettings), style: .blocks, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() @@ -264,7 +264,8 @@ public func featuredStickerPacksController(context: AccountContext) -> ViewContr } presentStickerPackController = { [weak controller] info in - presentControllerImpl?(StickerPackPreviewController(context: context, stickerPack: .id(id: info.id.id, accessHash: info.accessHash), mode: .settings, parentNavigationController: controller?.navigationController as? NavigationController), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) + presentControllerImpl?(StickerPackScreen(context: context, mode: .settings, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controller?.navigationController as? NavigationController), nil) } return controller diff --git a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift index 0e929357ee..5675e00417 100644 --- a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift @@ -13,6 +13,7 @@ import TextFormat import AccountContext import StickerPackPreviewUI import ItemListStickerPackItem +import UndoUI private final class InstalledStickerPacksControllerArguments { let account: Account @@ -289,35 +290,35 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { } } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! InstalledStickerPacksControllerArguments switch self { case let .suggestOptions(theme, text, value): - return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openSuggestOptions() }, tag: InstalledStickerPacksEntryTag.suggestOptions) case let .trending(theme, text, count): - return ItemListDisclosureItem(theme: theme, title: text, label: count == 0 ? "" : "\(count)", labelStyle: .badge(theme.list.itemAccentColor), sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: count == 0 ? "" : "\(count)", labelStyle: .badge(theme.list.itemAccentColor), sectionId: self.section, style: .blocks, action: { arguments.openFeatured() }) case let .masks(theme, text): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openMasks() }) case let .archived(theme, text, count, archived): - return ItemListDisclosureItem(theme: theme, title: text, label: count == 0 ? "" : "\(count)", sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: count == 0 ? "" : "\(count)", sectionId: self.section, style: .blocks, action: { arguments.openArchived(archived) }) case let .animatedStickers(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleAnimatedStickers(value) }) case let .animatedStickersInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .packsTitle(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .pack(_, theme, strings, info, topItem, count, animatedStickers, enabled, editing): - return ItemListStickerPackItem(theme: theme, strings: strings, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: .none, editing: editing, enabled: enabled, playAnimatedStickers: animatedStickers, sectionId: self.section, action: { + return ItemListStickerPackItem(presentationData: presentationData, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: .none, editing: editing, enabled: enabled, playAnimatedStickers: animatedStickers, sectionId: self.section, action: { arguments.openStickerPack(info) }, setPackIdWithRevealedOptions: { current, previous in arguments.setPackIdWithRevealedOptions(current, previous) @@ -326,7 +327,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { arguments.removePack(ArchivedStickerPackItem(info: info, topItems: topItem != nil ? [topItem!] : [])) }) case let .packsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section, linkAction: { _ in + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in arguments.openStickersBot() }) } @@ -494,6 +495,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta let archivedPromise = Promise<[ArchivedStickerPackItem]?>() var presentStickerPackController: ((StickerPackCollectionInfo) -> Void)? + var navigationControllerImpl: (() -> NavigationController?)? let arguments = InstalledStickerPacksControllerArguments(account: context.account, openStickerPack: { info in presentStickerPackController?(info) @@ -507,10 +509,35 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta } }, removePack: { archivedItem in let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } + let removeAction: (RemoveStickerPackOption) -> Void = { action in + let _ = (removeStickerPackInteractively(postbox: context.account.postbox, id: archivedItem.info.id, option: action) + |> deliverOnMainQueue).start(next: { indexAndItems in + guard let (positionInList, items) = indexAndItems else { + return + } + + var animateInAsReplacement = false + if let navigationController = navigationControllerImpl?() { + for controller in navigationController.overlayControllers { + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitActionAndReplacementAnimation() + animateInAsReplacement = true + } + } + } + + navigationControllerImpl?()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: action == .archive ? presentationData.strings.StickerPackActionInfo_ArchivedTitle : presentationData.strings.StickerPackActionInfo_RemovedTitle, text: presentationData.strings.StickerPackActionInfo_RemovedText(archivedItem.info.title).0, undo: true, info: archivedItem.info, topItem: archivedItem.topItems.first, account: context.account), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { action in + if case .undo = action { + let _ = addStickerPackInteractively(postbox: context.account.postbox, info: archivedItem.info, items: items, positionInList: positionInList).start() + } + return true + })) + }) + } controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetTextItem(title: presentationData.strings.StickerSettings_ContextInfo), @@ -524,12 +551,12 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta archivedPromise.set(.single(packs)) updatedPacks(packs) }) - - let _ = removeStickerPackInteractively(postbox: context.account.postbox, id: archivedItem.info.id, option: .archive).start() + + removeAction(.archive) }), ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { dismissAction() - let _ = removeStickerPackInteractively(postbox: context.account.postbox, id: archivedItem.info.id, option: .delete).start() + removeAction(.delete) }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) @@ -559,7 +586,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta })) }, openSuggestOptions: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -658,9 +685,9 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta title = presentationData.strings.MaskStickerSettings_Title } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(entries: installedStickerPacksControllerEntries(presentationData: presentationData, state: state, mode: mode, view: view, temporaryPackOrder: temporaryPackOrder, featured: featuredAndArchived.0, archived: featuredAndArchived.1, stickerSettings: stickerSettings), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: previous != nil && packCount != nil && (previous! != 0 && previous! >= packCount! - 10)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: installedStickerPacksControllerEntries(presentationData: presentationData, state: state, mode: mode, view: view, temporaryPackOrder: temporaryPackOrder, featured: featuredAndArchived.0, archived: featuredAndArchived.1, stickerSettings: stickerSettings), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: previous != nil && packCount != nil && (previous! != 0 && previous! >= packCount! - 10)) return (controllerState, (listState, arguments)) } |> afterDisposed { @@ -671,10 +698,10 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta if case .modal = mode { controller.navigationPresentation = .modal } - controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [InstalledStickerPacksEntry]) -> Void in + controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [InstalledStickerPacksEntry]) -> Signal in let fromEntry = entries[fromIndex] guard case let .pack(_, _, _, fromPackInfo, _, _, _, _, _) = fromEntry else { - return + return .single(false) } var referenceId: ItemCollectionId? var beforeAll = false @@ -704,20 +731,26 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta } } + var previousIndex: Int? for i in 0 ..< currentIds.count { if currentIds[i] == fromPackInfo.id { + previousIndex = i currentIds.remove(at: i) break } } + var didReorder = false + if let referenceId = referenceId { var inserted = false for i in 0 ..< currentIds.count { if currentIds[i] == referenceId { if fromIndex < toIndex { + didReorder = previousIndex != i + 1 currentIds.insert(fromPackInfo.id, at: i + 1) } else { + didReorder = previousIndex != i currentIds.insert(fromPackInfo.id, at: i) } inserted = true @@ -725,15 +758,20 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta } } if !inserted { + didReorder = previousIndex != currentIds.count currentIds.append(fromPackInfo.id) } } else if beforeAll { + didReorder = previousIndex != 0 currentIds.insert(fromPackInfo.id, at: 0) } else if afterAll { + didReorder = previousIndex != currentIds.count currentIds.append(fromPackInfo.id) } temporaryPackOrder.set(.single(currentIds)) + + return .single(didReorder) }) controller.setReorderCompleted({ (entries: [InstalledStickerPacksEntry]) -> Void in @@ -777,7 +815,59 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta } } presentStickerPackController = { [weak controller] info in - presentControllerImpl?(StickerPackPreviewController(context: context, stickerPack: .id(id: info.id.id, accessHash: info.accessHash), mode: .settings, parentNavigationController: controller?.navigationController as? NavigationController), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + let _ = (stickerPacks.get() + |> take(1) + |> deliverOnMainQueue).start(next: { view in + guard let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [namespaceForMode(mode)])] as? ItemCollectionInfosView, let entries = stickerPacksView.entriesByNamespace[namespaceForMode(mode)] else { + return + } + var mainStickerPack: StickerPackReference? + var packs: [StickerPackReference] = [] + for entry in entries { + if let listInfo = entry.info as? StickerPackCollectionInfo { + let packReference: StickerPackReference = .id(id: listInfo.id.id, accessHash: listInfo.accessHash) + if listInfo.id == info.id { + mainStickerPack = packReference + } + packs.append(packReference) + } + } + if mainStickerPack == nil { + let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) + mainStickerPack = packReference + packs.insert(packReference, at: 0) + } + if let mainStickerPack = mainStickerPack { + presentControllerImpl?(StickerPackScreen(context: context, mode: .settings, mainStickerPack: mainStickerPack, stickerPacks: packs, parentNavigationController: controller?.navigationController as? NavigationController, actionPerformed: { info, items, action in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var animateInAsReplacement = false + if let navigationController = navigationControllerImpl?() { + for controller in navigationController.overlayControllers { + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitActionAndReplacementAnimation() + animateInAsReplacement = true + } + } + } + switch action { + case .add: + navigationControllerImpl?()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).0, undo: false, info: info, topItem: items.first, account: context.account), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in + return true + })) + case let .remove(positionInList): + navigationControllerImpl?()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_RemovedTitle, text: presentationData.strings.StickerPackActionInfo_RemovedText(info.title).0, undo: true, info: info, topItem: items.first, account: context.account), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { action in + if case .undo = action { + let _ = addStickerPackInteractively(postbox: context.account.postbox, info: info, items: items, positionInList: positionInList).start() + } + return true + })) + } + }), nil) + } + }) + } + navigationControllerImpl = { [weak controller] in + return controller?.navigationController as? NavigationController } pushControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.pushViewController(c) diff --git a/submodules/SettingsUI/Sources/TabBarAccountSwitchControllerNode.swift b/submodules/SettingsUI/Sources/TabBarAccountSwitchControllerNode.swift index dee2138faa..ca1c14f2dd 100644 --- a/submodules/SettingsUI/Sources/TabBarAccountSwitchControllerNode.swift +++ b/submodules/SettingsUI/Sources/TabBarAccountSwitchControllerNode.swift @@ -93,7 +93,7 @@ private final class AddAccountItemNode: ASDisplayNode, AbstractSwitchAccountItem } private final class SwitchAccountItemNode: ASDisplayNode, AbstractSwitchAccountItemNode { - private let account: Account + private let context: AccountContext private let peer: Peer private let isCurrent: Bool private let unreadCount: Int32 @@ -110,8 +110,8 @@ private final class SwitchAccountItemNode: ASDisplayNode, AbstractSwitchAccountI private let badgeBackgroundNode: ASImageNode private let badgeTitleNode: ImmediateTextNode - init(account: Account, peer: Peer, isCurrent: Bool, unreadCount: Int32, displaySeparator: Bool, presentationData: PresentationData, action: @escaping () -> Void) { - self.account = account + init(context: AccountContext, peer: Peer, isCurrent: Bool, unreadCount: Int32, displaySeparator: Bool, presentationData: PresentationData, action: @escaping () -> Void) { + self.context = context self.peer = peer self.isCurrent = isCurrent self.unreadCount = unreadCount @@ -190,7 +190,7 @@ private final class SwitchAccountItemNode: ASDisplayNode, AbstractSwitchAccountI return (titleSize.width + leftInset + rightInset, height, { width in let avatarSize = CGSize(width: 30.0, height: 30.0) self.avatarNode.frame = CGRect(origin: CGPoint(x: floor((leftInset - avatarSize.width) / 2.0), y: floor((height - avatarSize.height) / 2.0)), size: avatarSize) - self.avatarNode.setPeer(account: self.account, theme: self.presentationData.theme, peer: self.peer) + self.avatarNode.setPeer(context: self.context, theme: self.presentationData.theme, peer: self.peer) self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize) @@ -266,13 +266,13 @@ final class TabBarAccountSwitchControllerNode: ViewControllerTracingNode { cancel() })) } - contentNodes.append(SwitchAccountItemNode(account: accounts.primary.0, peer: accounts.primary.1, isCurrent: true, unreadCount: 0, displaySeparator: !accounts.other.isEmpty, presentationData: presentationData, action: { + contentNodes.append(SwitchAccountItemNode(context: sharedContext.makeTempAccountContext(account: accounts.primary.0), peer: accounts.primary.1, isCurrent: true, unreadCount: 0, displaySeparator: !accounts.other.isEmpty, presentationData: presentationData, action: { cancel() })) for i in 0 ..< accounts.other.count { let (account, peer, count) = accounts.other[i] let id = account.id - contentNodes.append(SwitchAccountItemNode(account: account, peer: peer, isCurrent: false, unreadCount: count, displaySeparator: i != accounts.other.count - 1, presentationData: presentationData, action: { + contentNodes.append(SwitchAccountItemNode(context: sharedContext.makeTempAccountContext(account: account), peer: peer, isCurrent: false, unreadCount: count, displaySeparator: i != accounts.other.count - 1, presentationData: presentationData, action: { switchToAccount(id) })) } diff --git a/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift index 4ba0f94379..d06773453a 100644 --- a/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift +++ b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift @@ -38,21 +38,6 @@ public extension TermsOfServiceControllerTheme { convenience init(presentationTheme: PresentationTheme) { self.init(statusBarStyle: presentationTheme.rootController.statusBarStyle.style, navigationBackground: presentationTheme.rootController.navigationBar.backgroundColor, navigationSeparator: presentationTheme.rootController.navigationBar.separatorColor, listBackground: presentationTheme.list.blocksBackgroundColor, itemBackground: presentationTheme.list.itemBlocksBackgroundColor, itemSeparator: presentationTheme.list.itemBlocksSeparatorColor, primary: presentationTheme.list.itemPrimaryTextColor, accent: presentationTheme.list.itemAccentColor, disabled: presentationTheme.rootController.navigationBar.disabledButtonColor) } - - var presentationTheme: PresentationTheme { - let theme: PresentationTheme - switch itemBackground.argb { - case defaultPresentationTheme.list.itemBlocksBackgroundColor.argb: - theme = defaultPresentationTheme - case defaultDarkPresentationTheme.list.itemBlocksBackgroundColor.argb: - theme = defaultDarkPresentationTheme - case defaultDarkAccentPresentationTheme.list.itemBlocksBackgroundColor.argb: - theme = defaultDarkAccentPresentationTheme - default: - theme = defaultPresentationTheme - } - return theme - } } public class TermsOfServiceController: ViewController, StandalonePresentableController { @@ -60,8 +45,7 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont return self.displayNode as! TermsOfServiceControllerNode } - private let theme: TermsOfServiceControllerTheme - private let strings: PresentationStrings + private let presentationData: PresentationData private let text: String private let entities: [MessageTextEntity] private let ageConfirmation: Int32? @@ -76,7 +60,7 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont public var inProgress: Bool = false { didSet { if self.inProgress { - let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.theme.accent)) + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.list.itemAccentColor)) self.navigationItem.rightBarButtonItem = item } else { self.navigationItem.rightBarButtonItem = nil @@ -85,9 +69,8 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont } } - public init(theme: TermsOfServiceControllerTheme, strings: PresentationStrings, text: String, entities: [MessageTextEntity], ageConfirmation: Int32?, signingUp: Bool, accept: @escaping (String?) -> Void, decline: @escaping () -> Void, openUrl: @escaping (String) -> Void) { - self.theme = theme - self.strings = strings + public init(presentationData: PresentationData, text: String, entities: [MessageTextEntity], ageConfirmation: Int32?, signingUp: Bool, accept: @escaping (String?) -> Void, decline: @escaping () -> Void, openUrl: @escaping (String) -> Void) { + self.presentationData = presentationData self.text = text self.entities = entities self.ageConfirmation = ageConfirmation @@ -96,13 +79,13 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont self.decline = decline self.openUrl = openUrl - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(buttonColor: self.theme.accent, disabledButtonColor: self.theme.disabled, primaryTextColor: self.theme.primary, backgroundColor: self.theme.navigationBackground, separatorColor: self.theme.navigationSeparator, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(back: strings.Common_Back, close: strings.Common_Close))) + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme), strings: NavigationBarStrings(back: presentationData.strings.Common_Back, close: presentationData.strings.Common_Close))) - self.statusBar.statusBarStyle = self.theme.statusBarStyle + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - self.title = self.strings.Login_TermsOfServiceHeader + self.title = self.presentationData.strings.Login_TermsOfServiceHeader - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.strings.Common_Back, style: .plain, target: nil, action: nil) + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.scrollToTop = { [weak self] in self?.controllerNode.scrollToTop() @@ -117,7 +100,7 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont } override public func loadDisplayNode() { - self.displayNode = TermsOfServiceControllerNode(theme: self.theme, strings: self.strings, text: self.text, entities: self.entities, ageConfirmation: self.ageConfirmation, leftAction: { [weak self] in + self.displayNode = TermsOfServiceControllerNode(presentationData: self.presentationData, text: self.text, entities: self.entities, ageConfirmation: self.ageConfirmation, leftAction: { [weak self] in guard let strongSelf = self else { return } @@ -125,16 +108,16 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont let text: String let declineTitle: String if strongSelf.signingUp { - text = strongSelf.strings.Login_TermsOfServiceSignupDecline - declineTitle = strongSelf.strings.Login_TermsOfServiceDecline + text = strongSelf.presentationData.strings.Login_TermsOfServiceSignupDecline + declineTitle = strongSelf.presentationData.strings.Login_TermsOfServiceDecline } else { - text = strongSelf.strings.PrivacyPolicy_DeclineMessage - declineTitle = strongSelf.strings.PrivacyPolicy_DeclineDeclineAndDelete + text = strongSelf.presentationData.strings.PrivacyPolicy_DeclineMessage + declineTitle = strongSelf.presentationData.strings.PrivacyPolicy_DeclineDeclineAndDelete } - let theme: PresentationTheme = strongSelf.theme.presentationTheme - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: theme), title: strongSelf.strings.PrivacyPolicy_Decline, text: text, actions: [TextAlertAction(type: .destructiveAction, title: declineTitle, action: { + let theme: PresentationTheme = strongSelf.presentationData.theme + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.PrivacyPolicy_Decline, text: text, actions: [TextAlertAction(type: .destructiveAction, title: declineTitle, action: { self?.decline() - }), TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_Cancel, action: { + }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { })], actionLayout: .vertical), in: .window(.root)) }, rightAction: { [weak self] in guard let strongSelf = self else { @@ -142,8 +125,8 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont } if let ageConfirmation = strongSelf.ageConfirmation { - let theme: PresentationTheme = strongSelf.theme.presentationTheme - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: theme), title: strongSelf.strings.PrivacyPolicy_AgeVerificationTitle, text: strongSelf.strings.PrivacyPolicy_AgeVerificationMessage("\(ageConfirmation)").0, actions: [TextAlertAction(type: .genericAction, title: strongSelf.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.strings.PrivacyPolicy_AgeVerificationAgree, action: { + let theme: PresentationTheme = strongSelf.presentationData.theme + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationTitle, text: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationMessage("\(ageConfirmation)").0, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationAgree, action: { self?.accept(self?.proccessBotNameAfterAccept) })]), in: .window(.root)) } else { diff --git a/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceControllerNode.swift b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceControllerNode.swift index cf5c360569..248452424c 100644 --- a/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceControllerNode.swift +++ b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceControllerNode.swift @@ -10,8 +10,7 @@ import TelegramPresentationData import TextFormat final class TermsOfServiceControllerNode: ViewControllerTracingNode { - private let theme: TermsOfServiceControllerTheme - private let strings: PresentationStrings + private let presentationData: PresentationData private let text: String private let entities: [MessageTextEntity] private let ageConfirmation: Int32? @@ -43,9 +42,8 @@ final class TermsOfServiceControllerNode: ViewControllerTracingNode { } } - init(theme: TermsOfServiceControllerTheme, strings: PresentationStrings, text: String, entities: [MessageTextEntity], ageConfirmation: Int32?, leftAction: @escaping () -> Void, rightAction: @escaping () -> Void, openUrl: @escaping (String) -> Void, present: @escaping (ViewController, Any?) -> Void, setToProcceedBot:@escaping(String)->Void) { - self.theme = theme - self.strings = strings + init(presentationData: PresentationData, text: String, entities: [MessageTextEntity], ageConfirmation: Int32?, leftAction: @escaping () -> Void, rightAction: @escaping () -> Void, openUrl: @escaping (String) -> Void, present: @escaping (ViewController, Any?) -> Void, setToProcceedBot:@escaping(String)->Void) { + self.presentationData = presentationData self.text = text self.entities = entities self.ageConfirmation = ageConfirmation @@ -59,7 +57,10 @@ final class TermsOfServiceControllerNode: ViewControllerTracingNode { self.contentTextNode = ImmediateTextNode() self.contentTextNode.displaysAsynchronously = false self.contentTextNode.maximumNumberOfLines = 0 - self.contentTextNode.attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: theme.primary, linkColor: theme.accent, baseFont: Font.regular(15.0), linkFont: Font.regular(15.0), boldFont: Font.semibold(15.0), italicFont: Font.italic(15.0), boldItalicFont: Font.semiboldItalic(15.0), fixedFont: Font.monospace(15.0), blockQuoteFont: Font.regular(15.0)) + + let fontSize = floor(presentationData.listsFontSize.baseDisplaySize * 15.0 / 17.0) + + self.contentTextNode.attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: presentationData.theme.list.itemPrimaryTextColor, linkColor: presentationData.theme.list.itemAccentColor, baseFont: Font.regular(fontSize), linkFont: Font.regular(fontSize), boldFont: Font.semibold(fontSize), italicFont: Font.italic(fontSize), boldItalicFont: Font.semiboldItalic(fontSize), fixedFont: Font.monospace(fontSize), blockQuoteFont: Font.regular(fontSize)) self.toolbarNode = ASDisplayNode() self.toolbarSeparatorNode = ASDisplayNode() @@ -67,20 +68,20 @@ final class TermsOfServiceControllerNode: ViewControllerTracingNode { self.leftActionTextNode = ImmediateTextNode() self.leftActionTextNode.displaysAsynchronously = false self.leftActionTextNode.isUserInteractionEnabled = false - self.leftActionTextNode.attributedText = NSAttributedString(string: self.strings.PrivacyPolicy_Decline, font: Font.regular(17.0), textColor: self.theme.accent) + self.leftActionTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.PrivacyPolicy_Decline, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: self.presentationData.theme.list.itemAccentColor) self.rightActionNode = HighlightableButtonNode() self.rightActionTextNode = ImmediateTextNode() self.rightActionTextNode.displaysAsynchronously = false self.rightActionTextNode.isUserInteractionEnabled = false - self.rightActionTextNode.attributedText = NSAttributedString(string: self.strings.PrivacyPolicy_Accept, font: Font.semibold(17.0), textColor: self.theme.accent) + self.rightActionTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.PrivacyPolicy_Accept, font: Font.semibold(presentationData.listsFontSize.baseDisplaySize), textColor: self.presentationData.theme.list.itemAccentColor) super.init() - self.backgroundColor = self.theme.listBackground - self.toolbarNode.backgroundColor = self.theme.navigationBackground - self.toolbarSeparatorNode.backgroundColor = self.theme.navigationSeparator + self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor + self.toolbarNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + self.toolbarSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor - self.contentBackgroundNode.backgroundColor = self.theme.itemBackground + self.contentBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor self.addSubnode(self.scrollNode) self.scrollNode.addSubnode(self.contentBackgroundNode) @@ -119,7 +120,7 @@ final class TermsOfServiceControllerNode: ViewControllerTracingNode { self.leftActionNode.addTarget(self, action: #selector(self.leftActionPressed), forControlEvents: .touchUpInside) self.rightActionNode.addTarget(self, action: #selector(self.rightActionPressed), forControlEvents: .touchUpInside) - self.contentTextNode.linkHighlightColor = self.theme.accent.withAlphaComponent(0.5) + self.contentTextNode.linkHighlightColor = self.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.5) self.contentTextNode.highlightAttributeAction = { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) @@ -136,16 +137,16 @@ final class TermsOfServiceControllerNode: ViewControllerTracingNode { guard let strongSelf = self else { return } - let theme: PresentationTheme = strongSelf.theme.presentationTheme - let actionSheet = ActionSheetController(presentationTheme: theme) + let theme: PresentationTheme = strongSelf.presentationData.theme + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: strongSelf.strings.Login_TermsOfService_ProceedBot(mention).0), - ActionSheetButtonItem(title: strongSelf.strings.PrivacyPolicy_Accept, color: .accent, action: { [weak actionSheet] in + ActionSheetTextItem(title: strongSelf.presentationData.strings.Login_TermsOfService_ProceedBot(mention).0), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.PrivacyPolicy_Accept, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() setToProcceedBot(mention) rightAction() }) - ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, action: { [weak actionSheet] in + ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, action: { [weak actionSheet] in actionSheet?.dismissAnimated() })])]) strongSelf.present(actionSheet, nil) @@ -168,20 +169,20 @@ final class TermsOfServiceControllerNode: ViewControllerTracingNode { return } if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - let theme: PresentationTheme = strongSelf.theme.presentationTheme - let actionSheet = ActionSheetController(presentationTheme: theme) + let theme: PresentationTheme = strongSelf.presentationData.theme + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: url), - ActionSheetButtonItem(title: strongSelf.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() self?.openUrl(url) }), - ActionSheetButtonItem(title: strongSelf.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() UIPasteboard.general.string = url }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift new file mode 100644 index 0000000000..675dc81df5 --- /dev/null +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -0,0 +1,722 @@ +import Foundation +import UIKit +import Display +import Postbox +import SwiftSignalKit +import AsyncDisplayKit +import TelegramCore +import SyncCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import ChatListUI +import WallpaperResources +import LegacyComponents +import ItemListUI + +private func generateMaskImage(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let gradientColors = [color.withAlphaComponent(0.0).cgColor, color.cgColor, color.cgColor] as CFArray + + var locations: [CGFloat] = [0.0, 0.75, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 80.0), options: CGGradientDrawingOptions()) + }) +} + +private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollViewDelegate { + private let context: AccountContext + private var presentationThemeSettings: PresentationThemeSettings + private var presentationData: PresentationData + + private let referenceTimestamp: Int32 + + private let scrollNode: ASScrollNode + private let pageControlBackgroundNode: ASDisplayNode + private let pageControlNode: PageControlNode + + private let chatListBackgroundNode: ASDisplayNode + private var chatNodes: [ListViewItemNode]? + private let maskNode: ASImageNode + private let separatorNode: ASDisplayNode + private let chatBackgroundNode: WallpaperBackgroundNode + private let messagesContainerNode: ASDisplayNode + private var dateHeaderNode: ListViewItemHeaderNode? + private var messageNodes: [ListViewItemNode]? + private let toolbarNode: TextSelectionToolbarNode + + private var validLayout: (ContainerViewLayout, CGFloat)? + + init(context: AccountContext, presentationThemeSettings: PresentationThemeSettings, dismiss: @escaping () -> Void, apply: @escaping (Bool, PresentationFontSize, PresentationFontSize) -> Void) { + self.context = context + + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationThemeSettings = presentationThemeSettings + + let calendar = Calendar(identifier: .gregorian) + var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: Date()) + components.hour = 13 + components.minute = 0 + components.second = 0 + self.referenceTimestamp = Int32(calendar.date(from: components)?.timeIntervalSince1970 ?? 0.0) + + self.scrollNode = ASScrollNode() + + self.pageControlBackgroundNode = ASDisplayNode() + self.pageControlBackgroundNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.3) + self.pageControlBackgroundNode.cornerRadius = 10.5 + + self.pageControlNode = PageControlNode(dotSpacing: 7.0, dotColor: .white, inactiveDotColor: UIColor.white.withAlphaComponent(0.4)) + + self.chatListBackgroundNode = ASDisplayNode() + self.chatBackgroundNode = WallpaperBackgroundNode() + self.chatBackgroundNode.displaysAsynchronously = false + + self.messagesContainerNode = ASDisplayNode() + self.messagesContainerNode.clipsToBounds = true + self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + + self.chatBackgroundNode.image = chatControllerBackgroundImage(theme: self.presentationData.theme, wallpaper: self.presentationData.chatWallpaper, mediaBox: context.sharedContext.accountManager.mediaBox, knockoutMode: false) + self.chatBackgroundNode.motionEnabled = self.presentationData.chatWallpaper.settings?.motion ?? false + if case .gradient = self.presentationData.chatWallpaper { + self.chatBackgroundNode.imageContentMode = .scaleToFill + } + + self.toolbarNode = TextSelectionToolbarNode(presentationThemeSettings: self.presentationThemeSettings, presentationData: self.presentationData) + + self.maskNode = ASImageNode() + self.maskNode.displaysAsynchronously = false + self.maskNode.displayWithoutProcessing = true + self.maskNode.contentMode = .scaleToFill + + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = self.presentationData.theme.rootController.tabBar.separatorColor + + super.init() + + self.setViewBlock({ + return UITracingLayerView() + }) + + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + self.chatListBackgroundNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor + self.maskNode.image = generateMaskImage(color: self.presentationData.theme.chatList.backgroundColor) + + self.pageControlNode.isUserInteractionEnabled = false + self.pageControlNode.pagesCount = 2 + + self.addSubnode(self.scrollNode) + self.chatListBackgroundNode.addSubnode(self.maskNode) + self.addSubnode(self.pageControlBackgroundNode) + self.addSubnode(self.pageControlNode) + self.addSubnode(self.toolbarNode) + + self.scrollNode.addSubnode(self.chatListBackgroundNode) + self.scrollNode.addSubnode(self.chatBackgroundNode) + self.scrollNode.addSubnode(self.messagesContainerNode) + + self.addSubnode(self.separatorNode) + + self.toolbarNode.cancel = { + dismiss() + } + var dismissed = false + self.toolbarNode.done = { [weak self] in + guard let strongSelf = self else { + return + } + if !dismissed { + dismissed = true + apply(strongSelf.presentationThemeSettings.useSystemFont, strongSelf.presentationThemeSettings.fontSize, strongSelf.presentationThemeSettings.listsFontSize) + } + } + self.toolbarNode.updateUseSystemFont = { [weak self] value in + guard let strongSelf = self else { + return + } + strongSelf.presentationThemeSettings.useSystemFont = value + strongSelf.updatePresentationThemeSettings(strongSelf.presentationThemeSettings) + } + self.toolbarNode.updateCustomFontSize = { [weak self] value in + guard let strongSelf = self else { + return + } + switch strongSelf.toolbarNode.customMode { + case .chat: + strongSelf.presentationThemeSettings.fontSize = value + case .list: + strongSelf.presentationThemeSettings.listsFontSize = value + } + strongSelf.updatePresentationThemeSettings(strongSelf.presentationThemeSettings) + } + + let _ = (chatServiceBackgroundColor(wallpaper: self.presentationData.chatWallpaper, mediaBox: context.account.postbox.mediaBox) + |> deliverOnMainQueue).start(next: { [weak self] serviceColor in + self?.pageControlBackgroundNode.backgroundColor = serviceColor + }) + } + + override func didLoad() { + super.didLoad() + + self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true + self.scrollNode.view.showsHorizontalScrollIndicator = false + self.scrollNode.view.isPagingEnabled = true + self.scrollNode.view.delegate = self + self.pageControlNode.setPage(0.0) + } + + func updateFontSize() { + if let (layout, navigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + let bounds = scrollView.bounds + if !bounds.width.isZero { + self.pageControlNode.setPage(scrollView.contentOffset.x / bounds.width) + let pageIndex = Int(round(scrollView.contentOffset.x / bounds.width)) + let customMode: TextSelectionCustomMode = pageIndex >= 1 ? .list : .chat + if customMode != self.toolbarNode.customMode { + self.toolbarNode.setCustomMode(customMode) + if let (layout, navigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.recursivelyEnsureDisplaySynchronously(true) + } + } + } + } + + func animateIn(completion: (() -> Void)? = nil) { + if let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass { + self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } + } + + func animateOut(completion: (() -> Void)? = nil) { + if let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass { + self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in + completion?() + }) + } else { + completion?() + } + } + + private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) { + var items: [ChatListItem] = [] + + let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, activateChatPreview: { _, _, gesture in + gesture?.cancel() + }) + let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) + + let peers = SimpleDictionary() + let messages = SimpleDictionary() + let selfPeer = TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + let peer1 = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: 1), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + let peer2 = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: 2), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + let peer3 = TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: 3), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil) + let peer3Author = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: 4), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + let peer4 = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: 4), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + let peer5 = TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: 5), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil) + let peer6 = TelegramUser(id: PeerId(namespace: Namespaces.Peer.SecretChat, id: 5), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + let peer7 = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: 6), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_7_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + + let timestamp = self.referenceTimestamp + + let timestamp1 = timestamp + 120 + items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex(id: MessageId(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer1.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp1, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: selfPeer, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer1), combinedReadState: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, PeerReadState.idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: 0, markedUnread: false))]), notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) + + let presenceTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + 60 * 60) + let timestamp2 = timestamp + 3660 + items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer2.id, namespace: 0, id: 0), timestamp: timestamp2)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer2.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp2, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer2, text: "", attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer2), combinedReadState: nil, notificationSettings: nil, presence: TelegramUserPresence(status: .present(until: presenceTimestamp), lastActivity: presenceTimestamp), summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: [(peer2, .typingText)], isAd: false, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) + + let timestamp3 = timestamp + 3200 + items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer3.id, namespace: 0, id: 0), timestamp: timestamp3)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer3.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp3, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer3Author, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer3), combinedReadState: nil, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) + + let timestamp4 = timestamp + 3000 + items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer4.id, namespace: 0, id: 0), timestamp: timestamp4)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer4.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp4, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer4, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer4), combinedReadState: nil, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) + + let timestamp5 = timestamp + 1000 + items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer5.id, namespace: 0, id: 0), timestamp: timestamp5)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer4.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp5, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer5, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer5), combinedReadState: nil, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) + + items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer6.id, namespace: 0, id: 0), timestamp: timestamp - 360)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer6.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp - 360, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer6, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer6), combinedReadState: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, PeerReadState.idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: 1, markedUnread: false))]), notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) + + let width: CGFloat + if case .regular = layout.metrics.widthClass { + width = layout.size.width / 2.0 + } else { + width = layout.size.width + } + + let params = ListViewItemLayoutParams(width: width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) + if let chatNodes = self.chatNodes { + for i in 0 ..< items.count { + let itemNode = chatNodes[i] + items[i].updateNode(async: { $0() }, node: { + return itemNode + }, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: width, height: layout.size.height)) + + itemNode.contentSize = layout.contentSize + itemNode.insets = layout.insets + itemNode.frame = nodeFrame + itemNode.isUserInteractionEnabled = false + + apply(ListViewItemApply(isOnScreen: true)) + }) + } + } else { + var chatNodes: [ListViewItemNode] = [] + for i in 0 ..< items.count { + var itemNode: ListViewItemNode? + items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in + itemNode = node + apply().1(ListViewItemApply(isOnScreen: true)) + }) + itemNode!.isUserInteractionEnabled = false + chatNodes.append(itemNode!) + if self.maskNode.supernode != nil { + self.chatListBackgroundNode.insertSubnode(itemNode!, belowSubnode: self.maskNode) + } else { + self.chatListBackgroundNode.addSubnode(itemNode!) + } + } + self.chatNodes = chatNodes + } + + if let chatNodes = self.chatNodes { + var topOffset: CGFloat = topInset + for itemNode in chatNodes { + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: itemNode.frame.size)) + topOffset += itemNode.frame.height + } + } + } + + private func updateMessagesLayout(layout: ContainerViewLayout, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + let headerItem = self.context.sharedContext.makeChatMessageDateHeaderItem(context: self.context, timestamp: self.referenceTimestamp, theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder) + + var items: [ListViewItem] = [] + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: 1) + let otherPeerId = self.context.account.peerId + var peers = SimpleDictionary() + var messages = SimpleDictionary() + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + + let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3) + messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + + let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message1, theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil)) + + let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message2, theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil)) + + let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" + let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: MemoryBuffer(data: Data(base64Encoded: waveformBase64)!))] + let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) + + let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: []) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message3, theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil)) + + let message4 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message4, theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil)) + + let width: CGFloat + if case .regular = layout.metrics.widthClass { + width = layout.size.width / 2.0 + } else { + width = layout.size.width + } + + let params = ListViewItemLayoutParams(width: width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) + if let messageNodes = self.messageNodes { + for i in 0 ..< items.count { + let itemNode = messageNodes[i] + items[i].updateNode(async: { $0() }, node: { + return itemNode + }, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: width, height: layout.size.height)) + + itemNode.contentSize = layout.contentSize + itemNode.insets = layout.insets + itemNode.frame = nodeFrame + itemNode.isUserInteractionEnabled = false + + apply(ListViewItemApply(isOnScreen: true)) + }) + } + } else { + var messageNodes: [ListViewItemNode] = [] + for i in 0 ..< items.count { + var itemNode: ListViewItemNode? + items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in + itemNode = node + apply().1(ListViewItemApply(isOnScreen: true)) + }) + itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + itemNode!.isUserInteractionEnabled = false + messageNodes.append(itemNode!) + self.messagesContainerNode.addSubnode(itemNode!) + } + self.messageNodes = messageNodes + } + + var bottomOffset: CGFloat = 9.0 + bottomInset + if let messageNodes = self.messageNodes { + for itemNode in messageNodes { + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: itemNode.frame.size)) + bottomOffset += itemNode.frame.height + itemNode.updateFrame(itemNode.frame, within: layout.size) + } + } + + let dateHeaderNode: ListViewItemHeaderNode + if let currentDateHeaderNode = self.dateHeaderNode { + dateHeaderNode = currentDateHeaderNode + headerItem.updateNode(dateHeaderNode, previous: nil, next: headerItem) + } else { + dateHeaderNode = headerItem.node() + dateHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + self.messagesContainerNode.addSubnode(dateHeaderNode) + self.dateHeaderNode = dateHeaderNode + } + + transition.updateFrame(node: dateHeaderNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: CGSize(width: layout.size.width, height: headerItem.height))) + dateHeaderNode.updateLayout(size: self.messagesContainerNode.frame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right) + } + + func updatePresentationThemeSettings(_ presentationThemeSettings: PresentationThemeSettings) { + let fontSize: PresentationFontSize + let listsFontSize: PresentationFontSize + if presentationThemeSettings.useSystemFont { + let pointSize = UIFont.preferredFont(forTextStyle: .body).pointSize + fontSize = PresentationFontSize(systemFontSize: pointSize) + listsFontSize = fontSize + } else { + fontSize = presentationThemeSettings.fontSize + listsFontSize = presentationThemeSettings.listsFontSize + } + self.presentationData = self.presentationData.withFontSizes(chatFontSize: fontSize, listsFontSize: listsFontSize) + self.toolbarNode.updatePresentationData(presentationData: self.presentationData) + self.toolbarNode.updatePresentationThemeSettings(presentationThemeSettings: self.presentationThemeSettings) + if let (layout, navigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.recursivelyEnsureDisplaySynchronously(true) + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.validLayout = (layout, navigationBarHeight) + + let bounds = CGRect(origin: CGPoint(), size: layout.size) + self.scrollNode.frame = bounds + + let toolbarHeight = self.toolbarNode.updateLayout(width: layout.size.width, bottomInset: layout.intrinsicInsets.bottom, layout: layout, transition: transition) + + self.chatListBackgroundNode.frame = CGRect(x: bounds.width, y: 0.0, width: bounds.width, height: bounds.height) + var chatFrame = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height) + + let bottomInset: CGFloat + if case .regular = layout.metrics.widthClass { + self.chatListBackgroundNode.frame = CGRect(x: 0.0, y: 0.0, width: bounds.width / 2.0, height: bounds.height) + chatFrame = CGRect(x: bounds.width / 2.0, y: 0.0, width: bounds.width / 2.0, height: bounds.height) + self.scrollNode.view.contentSize = CGSize(width: bounds.width, height: bounds.height) + + self.pageControlNode.isHidden = true + self.pageControlBackgroundNode.isHidden = true + self.separatorNode.isHidden = false + + self.separatorNode.frame = CGRect(x: bounds.width / 2.0, y: 0.0, width: UIScreenPixel, height: bounds.height - toolbarHeight) + + bottomInset = 0.0 + } else { + self.chatListBackgroundNode.frame = CGRect(x: bounds.width, y: 0.0, width: bounds.width, height: bounds.height) + chatFrame = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height) + self.scrollNode.view.contentSize = CGSize(width: bounds.width * 2.0, height: bounds.height) + + self.pageControlNode.isHidden = false + self.pageControlBackgroundNode.isHidden = false + self.separatorNode.isHidden = true + + bottomInset = 37.0 + } + + self.chatBackgroundNode.frame = chatFrame + self.chatBackgroundNode.updateLayout(size: chatFrame.size, transition: transition) + self.messagesContainerNode.frame = chatFrame + + transition.updateFrame(node: self.toolbarNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: toolbarHeight + layout.intrinsicInsets.bottom))) + + self.updateChatsLayout(layout: layout, topInset: navigationBarHeight, transition: transition) + self.updateMessagesLayout(layout: layout, bottomInset: toolbarHeight + bottomInset, transition: transition) + + let pageControlSize = self.pageControlNode.measure(CGSize(width: bounds.width, height: 100.0)) + let pageControlFrame = CGRect(origin: CGPoint(x: floor((bounds.width - pageControlSize.width) / 2.0), y: layout.size.height - toolbarHeight - 28.0), size: pageControlSize) + self.pageControlNode.frame = pageControlFrame + self.pageControlBackgroundNode.frame = CGRect(x: pageControlFrame.minX - 7.0, y: pageControlFrame.minY - 7.0, width: pageControlFrame.width + 14.0, height: 21.0) + + transition.updateFrame(node: self.maskNode, frame: CGRect(x: 0.0, y: layout.size.height - toolbarHeight - 80.0, width: bounds.width, height: 80.0)) + } +} + +final class TextSizeSelectionController: ViewController { + private let context: AccountContext + + private var controllerNode: TextSizeSelectionControllerNode { + return self.displayNode as! TextSizeSelectionControllerNode + } + + private var didPlayPresentationAnimation = false + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private var presentationThemeSettings: PresentationThemeSettings + private var presentationThemeSettingsDisposable: Disposable? + + private var disposable: Disposable? + private var applyDisposable = MetaDisposable() + + public init(context: AccountContext, presentationThemeSettings: PresentationThemeSettings) { + self.context = context + + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationThemeSettings = presentationThemeSettings + + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationTheme: self.presentationData.theme, presentationStrings: self.presentationData.strings)) + + self.blocksBackgroundWhenInOverlay = true + self.navigationPresentation = .modal + + self.navigationItem.title = self.presentationData.strings.Appearance_TextSize_Title + self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + strongSelf.presentationData = presentationData + } + }) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + self.presentationThemeSettingsDisposable?.dispose() + self.disposable?.dispose() + self.applyDisposable.dispose() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation { + self.didPlayPresentationAnimation = true + if case .modalSheet = presentationArguments.presentationAnimation { + self.controllerNode.animateIn() + } + } + } + + override public func loadDisplayNode() { + super.loadDisplayNode() + + self.displayNode = TextSizeSelectionControllerNode(context: self.context, presentationThemeSettings: self.presentationThemeSettings, dismiss: { [weak self] in + if let strongSelf = self { + strongSelf.dismiss() + } + }, apply: { [weak self] useSystemFont, fontSize, listsFontSize in + if let strongSelf = self { + strongSelf.apply(useSystemFont: useSystemFont, fontSize: fontSize, listsFontSize: listsFontSize) + } + }) + self.displayNodeDidLoad() + } + + private func apply(useSystemFont: Bool, fontSize: PresentationFontSize, listsFontSize: PresentationFontSize) { + let _ = (updatePresentationThemeSettingsInteractively(accountManager: self.context.sharedContext.accountManager, { current in + var current = current + current.useSystemFont = useSystemFont + current.fontSize = fontSize + current.listsFontSize = listsFontSize + return current + }) + |> deliverOnMainQueue).start(completed: { [weak self] in + self?.dismiss() + }) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } +} + +private enum TextSelectionCustomMode { + case list + case chat +} + +private final class TextSelectionToolbarNode: ASDisplayNode { + private var presentationThemeSettings: PresentationThemeSettings + private var presentationData: PresentationData + + private let cancelButton = HighlightableButtonNode() + private let doneButton = HighlightableButtonNode() + private let separatorNode = ASDisplayNode() + private let topSeparatorNode = ASDisplayNode() + + private var switchItemNode: ItemListSwitchItemNode + private var fontSizeItemNode: ThemeSettingsFontSizeItemNode + + private(set) var customMode: TextSelectionCustomMode = .chat + + var cancel: (() -> Void)? + var done: (() -> Void)? + + var updateUseSystemFont: ((Bool) -> Void)? + var updateCustomFontSize: ((PresentationFontSize) -> Void)? + + init(presentationThemeSettings: PresentationThemeSettings, presentationData: PresentationData) { + self.presentationThemeSettings = presentationThemeSettings + self.presentationData = presentationData + + self.switchItemNode = ItemListSwitchItemNode(type: .regular) + self.fontSizeItemNode = ThemeSettingsFontSizeItemNode() + + super.init() + + self.addSubnode(self.switchItemNode) + self.addSubnode(self.fontSizeItemNode) + self.addSubnode(self.cancelButton) + self.addSubnode(self.doneButton) + self.addSubnode(self.separatorNode) + self.addSubnode(self.topSeparatorNode) + + self.updatePresentationData(presentationData: self.presentationData) + + self.cancelButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.cancelButton.backgroundColor = strongSelf.presentationData.theme.list.itemHighlightedBackgroundColor + } else { + UIView.animate(withDuration: 0.3, animations: { + strongSelf.cancelButton.backgroundColor = .clear + }) + } + } + } + + self.doneButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.doneButton.backgroundColor = strongSelf.presentationData.theme.list.itemHighlightedBackgroundColor + } else { + UIView.animate(withDuration: 0.3, animations: { + strongSelf.doneButton.backgroundColor = .clear + }) + } + } + } + + self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) + self.doneButton.addTarget(self, action: #selector(self.donePressed), forControlEvents: .touchUpInside) + } + + func setDoneEnabled(_ enabled: Bool) { + self.doneButton.alpha = enabled ? 1.0 : 0.4 + self.doneButton.isUserInteractionEnabled = enabled + } + + func setCustomMode(_ customMode: TextSelectionCustomMode) { + self.customMode = customMode + } + + func updatePresentationData(presentationData: PresentationData) { + self.backgroundColor = presentationData.theme.rootController.tabBar.backgroundColor + self.separatorNode.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor + self.topSeparatorNode.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor + + self.cancelButton.setTitle(presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: presentationData.theme.list.itemPrimaryTextColor, for: []) + self.doneButton.setTitle(presentationData.strings.Wallpaper_Set, with: Font.regular(17.0), with: presentationData.theme.list.itemPrimaryTextColor, for: []) + } + + func updatePresentationThemeSettings(presentationThemeSettings: PresentationThemeSettings) { + self.presentationThemeSettings = presentationThemeSettings + } + + func updateLayout(width: CGFloat, bottomInset: CGFloat, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat { + var contentHeight: CGFloat = 0.0 + + let switchItem = ItemListSwitchItem(presentationData: ItemListPresentationData(self.presentationData), title: self.presentationData.strings.Appearance_TextSize_UseSystem, value: self.presentationThemeSettings.useSystemFont, disableLeadingInset: true, sectionId: 0, style: .blocks, updated: { [weak self] value in + self?.updateUseSystemFont?(value) + }) + let fontSizeItem = ThemeSettingsFontSizeItem(theme: self.presentationData.theme, fontSize: self.customMode == .chat ? self.presentationThemeSettings.fontSize : self.presentationThemeSettings.listsFontSize, enabled: !self.presentationThemeSettings.useSystemFont, disableLeadingInset: true, force: true, sectionId: 0, updated: { [weak self] value in + self?.updateCustomFontSize?(value) + }) + + switchItem.updateNode(async: { f in + f() + }, node: { + return self.switchItemNode + }, params: ListViewItemLayoutParams(width: width, leftInset: layout.intrinsicInsets.left, rightInset: layout.intrinsicInsets.right, availableHeight: 1000.0), previousItem: nil, nextItem: fontSizeItem, animation: .None, completion: { layout, apply in + self.switchItemNode.contentSize = layout.contentSize + self.switchItemNode.insets = layout.insets + transition.updateFrame(node: self.switchItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: layout.contentSize)) + contentHeight += layout.contentSize.height + apply(ListViewItemApply(isOnScreen: true)) + }) + + fontSizeItem.updateNode(async: { f in + f() + }, node: { + return self.fontSizeItemNode + }, params: ListViewItemLayoutParams(width: width, leftInset: layout.intrinsicInsets.left, rightInset: layout.intrinsicInsets.right, availableHeight: 1000.0), previousItem: switchItem, nextItem: nil, animation: .None, completion: { layout, apply in + self.fontSizeItemNode.contentSize = layout.contentSize + self.fontSizeItemNode.insets = layout.insets + transition.updateFrame(node: self.fontSizeItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: layout.contentSize)) + contentHeight += layout.contentSize.height + apply(ListViewItemApply(isOnScreen: true)) + }) + + self.cancelButton.frame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: floor(width / 2.0), height: 49.0)) + self.doneButton.frame = CGRect(origin: CGPoint(x: floor(width / 2.0), y: contentHeight), size: CGSize(width: width - floor(width / 2.0), height: 49.0)) + + contentHeight += 49.0 + + self.topSeparatorNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel)) + + let resultHeight = contentHeight + bottomInset + + self.separatorNode.frame = CGRect(origin: CGPoint(x: floor(width / 2.0), y: self.cancelButton.frame.minY), size: CGSize(width: UIScreenPixel, height: resultHeight - self.cancelButton.frame.minY)) + + return resultHeight + } + + @objc func cancelPressed() { + self.cancel?() + } + + @objc func donePressed() { + self.doneButton.isUserInteractionEnabled = false + self.done?() + } +} diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionItem.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionItem.swift new file mode 100644 index 0000000000..d714a7a6bc --- /dev/null +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionItem.swift @@ -0,0 +1,302 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import SyncCore +import TelegramPresentationData +import TelegramUIPreferences +import LegacyComponents +import ItemListUI +import PresentationDataUtils +import AppBundle + +class BubbleSettingsRadiusItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let value: Int + let disableLeadingInset: Bool + let displayIcons: Bool + let force: Bool + let enabled: Bool + let sectionId: ItemListSectionId + let updated: (Int) -> Void + let tag: ItemListItemTag? + + init(theme: PresentationTheme, value: Int, enabled: Bool = true, disableLeadingInset: Bool = false, displayIcons: Bool = true, force: Bool = false, sectionId: ItemListSectionId, updated: @escaping (Int) -> Void, tag: ItemListItemTag? = nil) { + self.theme = theme + self.value = value + self.enabled = enabled + self.disableLeadingInset = disableLeadingInset + self.displayIcons = displayIcons + self.force = force + self.sectionId = sectionId + self.updated = updated + self.tag = tag + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = BubbleSettingsRadiusItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? BubbleSettingsRadiusItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } +} + +private func generateKnobImage() -> UIImage? { + return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(width: 0.0, height: -1.0), blur: 3.5, color: UIColor(white: 0.0, alpha: 0.25).cgColor) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0))) + }) +} + +class BubbleSettingsRadiusItemNode: ListViewItemNode, ItemListItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private var sliderView: TGPhotoEditorSliderView? + private let leftIconNode: ASImageNode + private let rightIconNode: ASImageNode + private let disabledOverlayNode: ASDisplayNode + + private var item: BubbleSettingsRadiusItem? + private var layoutParams: ListViewItemLayoutParams? + + var tag: ItemListItemTag? { + return self.item?.tag + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + self.leftIconNode = ASImageNode() + self.leftIconNode.displaysAsynchronously = false + self.leftIconNode.displayWithoutProcessing = true + + self.rightIconNode = ASImageNode() + self.rightIconNode.displaysAsynchronously = false + self.rightIconNode.displayWithoutProcessing = true + + self.disabledOverlayNode = ASDisplayNode() + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.leftIconNode) + self.addSubnode(self.rightIconNode) + + self.addSubnode(self.disabledOverlayNode) + } + + override func didLoad() { + super.didLoad() + + let sliderView = TGPhotoEditorSliderView() + sliderView.enablePanHandling = true + sliderView.enablePanHandling = true + sliderView.trackCornerRadius = 1.0 + sliderView.lineSize = 2.0 + sliderView.dotSize = 5.0 + sliderView.minimumValue = 0.0 + sliderView.maximumValue = 4.0 + sliderView.startValue = 0.0 + sliderView.positionsCount = 5 + sliderView.disablesInteractiveTransitionGestureRecognizer = true + if let item = self.item, let params = self.layoutParams { + sliderView.isUserInteractionEnabled = item.enabled + + sliderView.value = CGFloat((item.value - 8) / 2) + sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor + sliderView.backColor = item.theme.list.disclosureArrowColor + sliderView.trackColor = item.enabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor + sliderView.knobImage = generateKnobImage() + + let sliderInset: CGFloat = item.displayIcons ? 38.0 : 16.0 + + sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + sliderInset, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - sliderInset * 2.0, height: 44.0)) + } + self.view.insertSubview(sliderView, belowSubview: self.disabledOverlayNode.view) + sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) + self.sliderView = sliderView + } + + func asyncLayout() -> (_ item: BubbleSettingsRadiusItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + + return { item, params, neighbors in + var updatedLeftIcon: UIImage? + var updatedRightIcon: UIImage? + + var themeUpdated = false + if currentItem?.theme !== item.theme { + themeUpdated = true + + updatedLeftIcon = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsFontMinIcon"), color: item.theme.list.itemPrimaryTextColor) + updatedRightIcon = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsFontMaxIcon"), color: item.theme.list.itemPrimaryTextColor) + } + + let contentSize: CGSize + var insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + contentSize = CGSize(width: params.width, height: 60.0) + insets = itemListNeighborsGroupedInsets(neighbors) + + if item.disableLeadingInset { + insets.top = 0.0 + insets.bottom = 0.0 + } + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + let firstTime = strongSelf.item == nil || item.force + strongSelf.item = item + strongSelf.layoutParams = params + + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + strongSelf.disabledOverlayNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4) + strongSelf.disabledOverlayNode.isHidden = item.enabled + strongSelf.disabledOverlayNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: 44.0)) + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = params.leftInset + 16.0 + bottomStripeOffset = -separatorHeight + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + + if let updatedLeftIcon = updatedLeftIcon { + strongSelf.leftIconNode.image = updatedLeftIcon + } + if let image = strongSelf.leftIconNode.image { + strongSelf.leftIconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 25.0), size: CGSize(width: image.size.width, height: image.size.height)) + } + if let updatedRightIcon = updatedRightIcon { + strongSelf.rightIconNode.image = updatedRightIcon + } + if let image = strongSelf.rightIconNode.image { + strongSelf.rightIconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 14.0 - image.size.width, y: 21.0), size: CGSize(width: image.size.width, height: image.size.height)) + } + + strongSelf.leftIconNode.isHidden = !item.displayIcons + strongSelf.rightIconNode.isHidden = !item.displayIcons + + if let sliderView = strongSelf.sliderView { + sliderView.isUserInteractionEnabled = item.enabled + sliderView.trackColor = item.enabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor + + if themeUpdated { + sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor + sliderView.backColor = item.theme.list.disclosureArrowColor + sliderView.knobImage = generateKnobImage() + } + + let value: CGFloat = CGFloat((item.value - 8) / 2) + if firstTime { + sliderView.value = value + } + + let sliderInset: CGFloat = item.displayIcons ? 38.0 : 16.0 + sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + sliderInset, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - sliderInset * 2.0, height: 44.0)) + } + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + @objc func sliderValueChanged() { + guard let sliderView = self.sliderView else { + return + } + let value = Int(sliderView.value) * 2 + 8 + self.item?.updated(value) + } +} diff --git a/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift b/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift index 4ad1e1a7d6..31f9540413 100644 --- a/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift +++ b/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift @@ -138,6 +138,7 @@ func uploadCustomWallpaper(context: AccountContext, wallpaper: WallpaperGalleryE context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data) context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) + let autoNightModeTriggered = context.sharedContext.currentPresentationData.with {$0 }.autoNightModeTriggered let accountManager = context.sharedContext.accountManager let account = context.account let updateWallpaper: (TelegramWallpaper) -> Void = { wallpaper in @@ -155,8 +156,20 @@ func uploadCustomWallpaper(context: AccountContext, wallpaper: WallpaperGalleryE let _ = (updatePresentationThemeSettingsInteractively(accountManager: accountManager, { current in var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers - themeSpecificChatWallpapers[current.theme.index] = wallpaper - return PresentationThemeSettings(chatWallpaper: wallpaper, theme: current.theme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) + let themeReference: PresentationThemeReference + if autoNightModeTriggered { + themeReference = current.automaticThemeSwitchSetting.theme + } else { + themeReference = current.theme + } + let accentColor = current.themeSpecificAccentColors[themeReference.index] + if let accentColor = accentColor, accentColor.baseColor == .custom { + themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = wallpaper + } else { + themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = nil + themeSpecificChatWallpapers[themeReference.index] = wallpaper + } + return current.withUpdatedThemeSpecificChatWallpapers(themeSpecificChatWallpapers) })).start() } diff --git a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift index 180b9f52c6..f85e96e5ce 100644 --- a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift +++ b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift @@ -14,15 +14,18 @@ import PresentationDataUtils import LegacyMediaPickerUI import WallpaperResources import AccountContext +import MediaResources private final class EditThemeControllerArguments { let context: AccountContext let updateState: ((EditThemeControllerState) -> EditThemeControllerState) -> Void + let openColors: () -> Void let openFile: () -> Void - init(context: AccountContext, updateState: @escaping ((EditThemeControllerState) -> EditThemeControllerState) -> Void, openFile: @escaping () -> Void) { + init(context: AccountContext, updateState: @escaping ((EditThemeControllerState) -> EditThemeControllerState) -> Void, openColors: @escaping () -> Void, openFile: @escaping () -> Void) { self.context = context self.updateState = updateState + self.openColors = openColors self.openFile = openFile } } @@ -50,7 +53,8 @@ private enum EditThemeControllerEntry: ItemListNodeEntry { case slug(PresentationTheme, PresentationStrings, String, String, Bool) case slugInfo(PresentationTheme, String) case chatPreviewHeader(PresentationTheme, String) - case chatPreview(PresentationTheme, PresentationTheme, TelegramWallpaper, PresentationFontSize, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, [ChatPreviewMessageItem]) + case chatPreview(PresentationTheme, PresentationTheme, TelegramWallpaper, PresentationFontSize, PresentationChatBubbleCorners, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, [ChatPreviewMessageItem]) + case changeColors(PresentationTheme, String) case uploadTheme(PresentationTheme, String) case uploadInfo(PresentationTheme, String) @@ -58,7 +62,7 @@ private enum EditThemeControllerEntry: ItemListNodeEntry { switch self { case .title, .slug, .slugInfo: return EditThemeControllerSection.info.rawValue - case .chatPreviewHeader, .chatPreview, .uploadTheme, .uploadInfo: + case .chatPreviewHeader, .chatPreview, .changeColors, .uploadTheme, .uploadInfo: return EditThemeControllerSection.chatPreview.rawValue } } @@ -75,10 +79,12 @@ private enum EditThemeControllerEntry: ItemListNodeEntry { return 3 case .chatPreview: return 4 - case .uploadTheme: + case .changeColors: return 5 - case .uploadInfo: + case .uploadTheme: return 6 + case .uploadInfo: + return 7 } } @@ -108,8 +114,14 @@ private enum EditThemeControllerEntry: ItemListNodeEntry { } else { return false } - case let .chatPreview(lhsTheme, lhsComponentTheme, lhsWallpaper, lhsFontSize, lhsStrings, lhsTimeFormat, lhsNameOrder, lhsItems): - if case let .chatPreview(rhsTheme, rhsComponentTheme, rhsWallpaper, rhsFontSize, rhsStrings, rhsTimeFormat, rhsNameOrder, rhsItems) = rhs, lhsComponentTheme === rhsComponentTheme, lhsTheme === rhsTheme, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsStrings === rhsStrings, lhsTimeFormat == rhsTimeFormat, lhsNameOrder == rhsNameOrder, lhsItems == rhsItems { + case let .chatPreview(lhsTheme, lhsComponentTheme, lhsWallpaper, lhsFontSize, lhsChatBubbleCorners, lhsStrings, lhsTimeFormat, lhsNameOrder, lhsItems): + if case let .chatPreview(rhsTheme, rhsComponentTheme, rhsWallpaper, rhsFontSize, rhsChatBubbleCorners, rhsStrings, rhsTimeFormat, rhsNameOrder, rhsItems) = rhs, lhsComponentTheme === rhsComponentTheme, lhsTheme === rhsTheme, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsChatBubbleCorners == rhsChatBubbleCorners, lhsStrings === rhsStrings, lhsTimeFormat == rhsTimeFormat, lhsNameOrder == rhsNameOrder, lhsItems == rhsItems { + return true + } else { + return false + } + case let .changeColors(lhsTheme, lhsText): + if case let .changeColors(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -133,11 +145,11 @@ private enum EditThemeControllerEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! EditThemeControllerArguments switch self { case let .title(theme, strings, title, text, _): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(), text: text, placeholder: title, type: .regular(capitalization: true, autocorrection: false), returnKeyType: .default, clearType: .onFocus, tag: EditThemeEntryTag.title, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: text, placeholder: title, type: .regular(capitalization: true, autocorrection: false), returnKeyType: .default, clearType: .onFocus, tag: EditThemeEntryTag.title, sectionId: self.section, textUpdated: { value in arguments.updateState { current in var state = current state.title = value @@ -147,7 +159,7 @@ private enum EditThemeControllerEntry: ItemListNodeEntry { }) case let .slug(theme, strings, title, text, enabled): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: "t.me/addtheme/", textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: title, type: .username, clearType: .onFocus, enabled: enabled, tag: EditThemeEntryTag.slug, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "t.me/addtheme/", textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: title, type: .username, clearType: .onFocus, enabled: enabled, tag: EditThemeEntryTag.slug, sectionId: self.section, textUpdated: { value in arguments.updateState { current in var state = current state.slug = value @@ -157,23 +169,27 @@ private enum EditThemeControllerEntry: ItemListNodeEntry { }) case let .slugInfo(theme, text): - return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .chatPreviewHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) - case let .chatPreview(theme, componentTheme, wallpaper, fontSize, strings, dateTimeFormat, nameDisplayOrder, items): - return ThemeSettingsChatPreviewItem(context: arguments.context, theme: theme, componentTheme: componentTheme, strings: strings, sectionId: self.section, fontSize: fontSize, wallpaper: wallpaper, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, messageItems: items) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .chatPreview(theme, componentTheme, wallpaper, fontSize, chatBubbleCorners, strings, dateTimeFormat, nameDisplayOrder, items): + return ThemeSettingsChatPreviewItem(context: arguments.context, theme: theme, componentTheme: componentTheme, strings: strings, sectionId: self.section, fontSize: fontSize, chatBubbleCorners: chatBubbleCorners, wallpaper: wallpaper, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, messageItems: items) + case let .changeColors(theme, text): + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.openColors() + }) case let .uploadTheme(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openFile() }) case let .uploadInfo(theme, text): - return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) } } } public enum EditThemeControllerMode: Equatable { - case create + case create(PresentationTheme?, TelegramThemeSettings?) case edit(PresentationCloudTheme) } @@ -185,7 +201,7 @@ private struct EditThemeControllerState: Equatable { var updating: Bool } -private func editThemeControllerEntries(presentationData: PresentationData, state: EditThemeControllerState, previewTheme: PresentationTheme) -> [EditThemeControllerEntry] { +private func editThemeControllerEntries(presentationData: PresentationData, state: EditThemeControllerState, previewTheme: PresentationTheme, hasSettings: Bool) -> [EditThemeControllerEntry] { var entries: [EditThemeControllerEntry] = [] var isCreate = false @@ -237,23 +253,43 @@ private func editThemeControllerEntries(presentationData: PresentationData, stat entries.append(.slugInfo(presentationData.theme, infoText)) entries.append(.chatPreviewHeader(presentationData.theme, presentationData.strings.EditTheme_Preview.uppercased())) - entries.append(.chatPreview(presentationData.theme, previewTheme, previewTheme.chat.defaultWallpaper, presentationData.fontSize, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, [ChatPreviewMessageItem(outgoing: false, reply: (previewIncomingReplyName, previewIncomingReplyText), text: previewIncomingText), ChatPreviewMessageItem(outgoing: true, reply: nil, text: previewOutgoingText)])) + entries.append(.chatPreview(presentationData.theme, previewTheme, previewTheme.chat.defaultWallpaper, presentationData.chatFontSize, presentationData.chatBubbleCorners, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, [ChatPreviewMessageItem(outgoing: false, reply: (previewIncomingReplyName, previewIncomingReplyText), text: previewIncomingText), ChatPreviewMessageItem(outgoing: true, reply: nil, text: previewOutgoingText)])) - entries.append(.uploadTheme(presentationData.theme, uploadText)) - entries.append(.uploadInfo(presentationData.theme, uploadInfo)) + entries.append(.changeColors(presentationData.theme, presentationData.strings.EditTheme_ChangeColors)) + if !hasSettings { + entries.append(.uploadTheme(presentationData.theme, uploadText)) + entries.append(.uploadInfo(presentationData.theme, uploadInfo)) + } return entries } -public func editThemeController(context: AccountContext, mode: EditThemeControllerMode, navigateToChat: ((PeerId) -> Void)? = nil) -> ViewController { +public func editThemeController(context: AccountContext, mode: EditThemeControllerMode, navigateToChat: ((PeerId) -> Void)? = nil, completion: ((PresentationThemeReference) -> Void)? = nil) -> ViewController { let initialState: EditThemeControllerState let previewThemePromise = Promise() + let settingsPromise = Promise(nil) + let hasSettings: Bool + let presentationData = context.sharedContext.currentPresentationData.with { $0 } switch mode { - case .create: - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - initialState = EditThemeControllerState(mode: mode, title: generateThemeName(accentColor: presentationData.theme.rootController.navigationBar.buttonColor), slug: "", updatedTheme: nil, updating: false) - previewThemePromise.set(.single(presentationData.theme.withUpdated(name: "", defaultWallpaper: presentationData.chatWallpaper))) + case let .create(existingTheme, settings): + let theme: PresentationTheme + let wallpaper: TelegramWallpaper + if let existingTheme = existingTheme { + theme = existingTheme + wallpaper = theme.chat.defaultWallpaper + settingsPromise.set(.single(settings)) + hasSettings = settings != nil + } else { + theme = presentationData.theme + wallpaper = presentationData.chatWallpaper + settingsPromise.set(.single(nil)) + hasSettings = false + } + initialState = EditThemeControllerState(mode: mode, title: generateThemeName(accentColor: theme.rootController.navigationBar.buttonColor), slug: "", updatedTheme: nil, updating: false) + previewThemePromise.set(.single(theme.withUpdated(name: "", defaultWallpaper: wallpaper))) case let .edit(info): + hasSettings = info.theme.settings != nil + settingsPromise.set(.single(info.theme.settings)) if let file = info.theme.file, let path = context.sharedContext.accountManager.mediaBox.completedResourcePath(file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path)), let theme = makePresentationTheme(data: data, resolvedWallpaper: info.resolvedWallpaper) { if case let .file(file) = theme.chat.defaultWallpaper, file.id == 0 { previewThemePromise.set(cachedWallpaper(account: context.account, slug: file.slug, settings: file.settings) @@ -261,14 +297,15 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll if let wallpaper = wallpaper { return theme.withUpdated(name: nil, defaultWallpaper: wallpaper.wallpaper) } else { - return theme.withUpdated(name: nil, defaultWallpaper: .color(Int32(bitPattern: theme.chatList.backgroundColor.rgb))) + return theme.withUpdated(name: nil, defaultWallpaper: .color(theme.chatList.backgroundColor.argb)) } })) } else { previewThemePromise.set(.single(theme.withUpdated(name: nil, defaultWallpaper: info.resolvedWallpaper))) } } else { - previewThemePromise.set(.single(context.sharedContext.currentPresentationData.with { $0 }.theme)) + previewThemePromise.set(.single(presentationData.theme.withUpdated(name: "", defaultWallpaper: presentationData.chatWallpaper))) + } initialState = EditThemeControllerState(mode: mode, title: info.theme.title, slug: info.theme.slug, updatedTheme: nil, updating: false) } @@ -279,12 +316,39 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll } var presentControllerImpl: ((ViewController, Any?) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? var dismissInputImpl: (() -> Void)? var errorImpl: ((EditThemeEntryTag) -> Void)? + var generalThemeReference: PresentationThemeReference? + if case let .edit(cloudTheme) = mode { + generalThemeReference = PresentationThemeReference.cloud(cloudTheme).generalThemeReference + } + let arguments = EditThemeControllerArguments(context: context, updateState: { f in updateState(f) + }, openColors: { + let _ = (combineLatest(queue: Queue.mainQueue(), previewThemePromise.get(), settingsPromise.get()) + |> take(1)).start(next: { theme, previousSettings in + var controllerDismissImpl: (() -> Void)? + let controller = ThemeAccentColorController(context: context, mode: .edit(theme: theme, wallpaper: nil, generalThemeReference: generalThemeReference, defaultThemeReference: nil, create: false, completion: { updatedTheme, settings in + updateState { current in + var state = current + previewThemePromise.set(.single(updatedTheme)) + state.updatedTheme = updatedTheme + return state + } + if previousSettings != nil { + settingsPromise.set(.single(settings)) + } + controllerDismissImpl?() + })) + controllerDismissImpl = { [weak controller] in + controller?.dismiss() + } + pushControllerImpl?(controller) + }) }, openFile: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controller = legacyICloudFilePicker(theme: presentationData.theme, mode: .import, documentTypes: ["org.telegram.Telegram-iOS.theme"], completion: { urls in @@ -295,13 +359,13 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll |> mapToSignal { wallpaper -> Signal in if let wallpaper = wallpaper, case let .file(file) = wallpaper.wallpaper { var convertedRepresentations: [ImageRepresentationWithReference] = [] - convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource), reference: .media(media: .standalone(media: file.file), resource: file.file.resource))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) return wallpaperDatas(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: .standalone(media: file.file), representations: convertedRepresentations, alwaysShowThumbnailFirst: false, thumbnail: false, onlyFullSize: true, autoFetchFullSize: true, synchronousLoad: false) |> mapToSignal { _, fullSizeData, complete -> Signal in guard complete, let fullSizeData = fullSizeData else { return .complete() } - context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData) + context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData, synchronous: true) return .single(wallpaper.wallpaper) } } else { @@ -325,6 +389,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll return state } } + settingsPromise.set(.single(nil)) } else { presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.EditTheme_FileReadError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) @@ -374,8 +439,8 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll return state } - let _ = (previewThemePromise.get() - |> deliverOnMainQueue).start(next: { previewTheme in + let _ = (combineLatest(queue: Queue.mainQueue(), previewThemePromise.get(), settingsPromise.get()) + |> take(1)).start(next: { previewTheme, settings in let saveThemeTemplateFile: (String, LocalFileMediaResource, @escaping () -> Void) -> Void = { title, resource, completion in let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: resource.fileId), partialReference: nil, resource: resource, previewRepresentations: [], immediateThumbnailData: nil, mimeType: "application/x-tgtheme-ios", size: nil, attributes: [.FileName(fileName: "\(title).tgios-theme")]) let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil) @@ -414,8 +479,8 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll let themeThumbnailData: Data? if let theme = theme, let themeString = encodePresentationTheme(theme), let data = themeString.data(using: .utf8) { let resource = LocalFileMediaResource(fileId: arc4random64()) - context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data) + context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) themeResource = resource themeData = data @@ -445,32 +510,54 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll resolvedWallpaper = nil } + + let prepare: Signal + if let resolvedWallpaper = resolvedWallpaper, case let .file(file) = resolvedWallpaper, resolvedWallpaper.isPattern { + let resource = file.file.resource + let representation = CachedPatternWallpaperRepresentation(color: file.settings.color ?? 0xd6e2ee, bottomColor: file.settings.bottomColor, intensity: file.settings.intensity ?? 50, rotation: file.settings.rotation) + + var data: Data? + if let path = context.account.postbox.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + data = maybeData + } else if let path = context.sharedContext.accountManager.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + data = maybeData + } + + if let data = data { + context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + prepare = (context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true) + |> filter({ $0.complete }) + |> take(1) + |> castError(CreateThemeError.self) + |> mapToSignal { _ -> Signal in + return .complete() + }) + } else { + prepare = .complete() + } + } else { + prepare = .complete() + } + switch mode { case .create: if let themeResource = themeResource { - let _ = (createTheme(account: context.account, title: state.title, resource: themeResource, thumbnailData: themeThumbnailData) - |> deliverOnMainQueue).start(next: { next in + let _ = (prepare |> then(createTheme(account: context.account, title: state.title, resource: themeResource, thumbnailData: themeThumbnailData, settings: settings) + |> deliverOnMainQueue)).start(next: { next in if case let .result(resultTheme) = next { let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: resultTheme).start() - let _ = (context.sharedContext.accountManager.transaction { transaction -> Void in - transaction.updateSharedData(ApplicationSpecificSharedDataKeys.presentationThemeSettings, { entry in - let current: PresentationThemeSettings - if let entry = entry as? PresentationThemeSettings { - current = entry - } else { - current = PresentationThemeSettings.defaultSettings - } - if let resource = resultTheme.file?.resource, let data = themeData { - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data) - } - let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: resolvedWallpaper)) - var chatWallpaper = current.chatWallpaper - if let theme = theme { - chatWallpaper = resolvedWallpaper ?? theme.chat.defaultWallpaper - } - return PresentationThemeSettings(chatWallpaper: chatWallpaper, theme: themeReference, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: current.themeSpecificChatWallpapers, fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) - }) - } |> deliverOnMainQueue).start(completed: { + let _ = (updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + if let resource = resultTheme.file?.resource, let data = themeData { + context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + } + + let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: context.account.id)) + + var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers + themeSpecificChatWallpapers[themeReference.index] = nil + + return PresentationThemeSettings(theme: themeReference, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, useSystemFont: current.useSystemFont, fontSize: current.fontSize, listsFontSize: current.listsFontSize, chatBubbleSettings: current.chatBubbleSettings, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) + }) |> deliverOnMainQueue).start(completed: { if !hasCustomFile { saveThemeTemplateFile(state.title, themeResource, { dismissImpl?() @@ -489,29 +576,22 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll }) } case let .edit(info): - let _ = (updateTheme(account: context.account, accountManager: context.sharedContext.accountManager, theme: info.theme, title: state.title, slug: state.slug, resource: themeResource) - |> deliverOnMainQueue).start(next: { next in + let _ = (prepare |> then(updateTheme(account: context.account, accountManager: context.sharedContext.accountManager, theme: info.theme, title: state.title, slug: state.slug, resource: themeResource, settings: settings) + |> deliverOnMainQueue)).start(next: { next in if case let .result(resultTheme) = next { let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: resultTheme).start() - let _ = (context.sharedContext.accountManager.transaction { transaction -> Void in - transaction.updateSharedData(ApplicationSpecificSharedDataKeys.presentationThemeSettings, { entry in - let current: PresentationThemeSettings - if let entry = entry as? PresentationThemeSettings { - current = entry - } else { - current = PresentationThemeSettings.defaultSettings - } - if let resource = resultTheme.file?.resource, let data = themeData { - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data) - } - let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: resolvedWallpaper)) - var chatWallpaper = current.chatWallpaper - if let theme = theme { - chatWallpaper = resolvedWallpaper ?? theme.chat.defaultWallpaper - } - return PresentationThemeSettings(chatWallpaper: chatWallpaper, theme: themeReference, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: current.themeSpecificChatWallpapers, fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) - }) - } |> deliverOnMainQueue).start(completed: { + let _ = (updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + if let resource = resultTheme.file?.resource, let data = themeData { + context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + } + + let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: context.account.id)) + + var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers + themeSpecificChatWallpapers[themeReference.index] = nil + + return PresentationThemeSettings(theme: themeReference, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, useSystemFont: current.useSystemFont, fontSize: current.fontSize, listsFontSize: current.listsFontSize, chatBubbleSettings: current.chatBubbleSettings, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) + }) |> deliverOnMainQueue).start(completed: { if let themeResource = themeResource, !hasCustomFile { saveThemeTemplateFile(state.title, themeResource, { dismissImpl?() @@ -558,8 +638,8 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll title = presentationData.strings.EditTheme_EditTitle } } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: editThemeControllerEntries(presentationData: presentationData, state: state, previewTheme: previewTheme), style: .blocks, focusItemTag: focusItemTag, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: editThemeControllerEntries(presentationData: presentationData, state: state, previewTheme: previewTheme, hasSettings: hasSettings), style: .blocks, focusItemTag: focusItemTag, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } @@ -569,6 +649,9 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) } + pushControllerImpl = { [weak controller] c in + controller?.push(c) + } dismissImpl = { [weak controller] in controller?.view.endEditing(true) let _ = controller?.dismiss() diff --git a/submodules/SettingsUI/Sources/Themes/GenerateGradientColors.swift b/submodules/SettingsUI/Sources/Themes/GenerateGradientColors.swift new file mode 100644 index 0000000000..edd2dce252 --- /dev/null +++ b/submodules/SettingsUI/Sources/Themes/GenerateGradientColors.swift @@ -0,0 +1,338 @@ +import UIKit +import Display + +private let colorPairs: [(UInt32, UInt32)] = [ + (0xbdc3c7, 0x2c3e50), + (0xee9ca7, 0xffdde1), + (0x2193b0, 0x6dd5ed), + (0xb92b27, 0x1565c0), + (0x373b44, 0x4286f4), + (0xff0099, 0x493240), + (0x8e2de2, 0x4a00e0), + (0x1f4037, 0x99f2c8), + (0xf953c6, 0xb91d73), + (0xc31432, 0x240b36), + (0xf12711, 0xf5af19), + (0x659999, 0xf4791f), + (0xdd3e54, 0x6be585), + (0x8360c3, 0x2ebf91), + (0x544a7d, 0xffd452), + (0x009fff, 0xec2f4b), + (0x654ea3, 0xeaafc8), + (0xa8ff78, 0x78ffd6), + (0xed213a, 0x93291e), + (0xfdc830, 0xf37335), + (0x00b4db, 0x0083b0), + (0xffefba, 0xffffff), + (0x005aa7, 0xfffde4), + (0xda4453, 0x89216b), + (0x636363, 0xa2ab58), + (0xad5389, 0x3c1053), + (0xa8c0ff, 0x3f2b96), + (0x333333, 0xdd1818), + (0x4e54c8, 0x8f94fb), + (0xbc4e9c, 0xf80759), + (0x3e5151, 0xdecba4), + (0x11998e, 0x38ef7d), + (0x108dc7, 0xef8e38), + (0xfc5c7d, 0x6a82fb), + (0xfc466b, 0x3f5efb), + (0xc94b4b, 0x4b134f), + (0x23074d, 0xcc5333), + (0xfffbd5, 0xb20a2c), + (0x00b09b, 0x96c93d), + (0xd3cce3, 0xe9e4f0), + (0x3c3b3f, 0x605c3c), + (0xcac531, 0xf3f9a7), + (0x800080, 0xffc0cb), + (0x00f260, 0x0575e6), + (0xfc4a1a, 0xf7b733), + (0x74ebd5, 0xacb6e5), + (0x6d6027, 0xd3cbb8), + (0xe1eec3, 0xf05053), + (0x22c1c3, 0xfdbb2d), + (0xff9966, 0xff5e62), + (0x7f00ff, 0xe100ff), + (0xc9d6ff, 0xe2e2e2), + (0x396afc, 0x2948ff), + (0xd9a7c7, 0xfffcdc), + (0x642b73, 0xc6426e), + (0x1c92d2, 0xf2fcfe), + (0x000000, 0x0f9b0f), + (0x36d1dc, 0x5b86e5), + (0xcb356b, 0xbd3f32), + (0x283c86, 0x45a247), + (0xef3b36, 0xffffff), + (0xc0392b, 0x8e44ad), + (0x159957, 0x155799), + (0x000046, 0x1cb5e0), + (0x007991, 0x78ffd6), + (0x56ccf2, 0x2f80ed), + (0xf2994a, 0xf2c94c), + (0xeb5757, 0x000000), + (0xe44d26, 0xf16529), + (0x4ac29a, 0xbdfff3), + (0xb2fefa, 0x0ed2f7), + (0x30e8bf, 0xff8235), + (0xd66d75, 0xe29587), + (0x20002c, 0xcbb4d4), + (0xc33764, 0x1d2671), + (0xf7971e, 0xffd200), + (0x34e89e, 0x0f3443), + (0x6190e8, 0xa7bfe8), + (0x44a08d, 0x093637), + (0x200122, 0x6f0000), + (0x0575e6, 0x021b79), + (0x4568dc, 0xb06ab3), + (0x43c6ac, 0x191654), + (0x093028, 0x237a57), + (0x43c6ac, 0xf8ffae), + (0xffafbd, 0xffc3a0), + (0xf0f2f0, 0x000c40), + (0xe8cbc0, 0x636fa4), + (0xdce35b, 0x45b649), + (0xc0c0aa, 0x1cefff), + (0xdbe6f6, 0xc5796d), + (0x3494e6, 0xec6ead), + (0x67b26f, 0x4ca2cd), + (0xf3904f, 0x3b4371), + (0xee0979, 0xff6a00), + (0x41295a, 0x2f0743), + (0xf4c4f3, 0xfc67fa), + (0x00c3ff, 0xffff1c), + (0xff7e5f, 0xfeb47b), + (0xfffc00, 0xffffff), + (0xff00cc, 0x333399), + (0xde6161, 0x2657eb), + (0xef32d9, 0x89fffd), + (0x3a6186, 0x89253e), + (0x4ecdc4, 0x556270), + (0xa1ffce, 0xfaffd1), + (0xbe93c5, 0x7bc6cc), + (0xbdc3c7, 0x2c3e50), + (0xffd89b, 0x19547b), + (0x808080, 0x3fada8), + (0xfceabb, 0xf8b500), + (0xf85032, 0xe73827), + (0xf79d00, 0x64f38c), + (0x56ab2f, 0xa8e063), + (0x000428, 0x004e92), + (0x42275a, 0x734b6d), + (0x141e30, 0x243b55), + (0x2c3e50, 0xfd746c), + (0x2c3e50, 0x4ca1af), + (0xe96443, 0x904e95), + (0x0b486b, 0xf56217), + (0x3a7bd5, 0x3a6073), + (0x00d2ff, 0x928dab), + (0x2196f3, 0xf44336), + (0xff5f6d, 0xffc371), + (0xff4b1f, 0xff9068), + (0x16bffd, 0xcb3066), + (0xeecda3, 0xef629f), + (0x1d4350, 0xa43931), + (0xf7ff00, 0xdb36a4), + (0xff4b1f, 0x1fddff), + (0xba5370, 0xf4e2d8), + (0x4ca1af, 0xc4e0e5), + (0x000000, 0x434343), + (0x4b79a1, 0x283e51), + (0x834d9b, 0xd04ed6), + (0x0099f7, 0xf11712), + (0x2980b9, 0x2c3e50), + (0x5a3f37, 0x2c7744), + (0x4da0b0, 0xd39d38), + (0x5614b0, 0xdbd65c), + (0x2f7336, 0xaa3a38), + (0x1e3c72, 0x2a5298), + (0x114357, 0xf29492), + (0xfd746c, 0xff9068), + (0xeacda3, 0xd6ae7b), + (0x6a3093, 0xa044ff), + (0x457fca, 0x5691c8), + (0xb24592, 0xf15f79), + (0xc02425, 0xf0cb35), + (0x403a3e, 0xbe5869), + (0xc2e59c, 0x64b3f4), + (0xffb75e, 0xed8f03), + (0x8e0e00, 0x1f1c18), + (0x76b852, 0x8dc26f), + (0x673ab7, 0x512da8), + (0x00c9ff, 0x92fe9d), + (0xf46b45, 0xeea849), + (0x005c97, 0x363795), + (0xe53935, 0xe35d5b), + (0xfc00ff, 0x00dbde), + (0x2c3e50, 0x3498db), + (0xccccb2, 0x757519), + (0x304352, 0xd7d2cc), + (0xee9ca7, 0xffdde1), + (0xba8b02, 0x181818), + (0x525252, 0x3d72b4), + (0x004ff9, 0xfff94c), + (0x6a9113, 0x141517), + (0xf1f2b5, 0x135058), + (0xd1913c, 0xffd194), + (0x7b4397, 0xdc2430), + (0x8e9eab, 0xeef2f3), + (0x136a8a, 0x267871), + (0x00bf8f, 0x001510), + (0xff0084, 0x33001b), + (0x6441a5, 0x2a0845), + (0xffb347, 0xffcc33), + (0x43cea2, 0x185a9d), + (0xffa17f, 0x00223e), + (0x360033, 0x0b8793), + (0x948e99, 0x2e1437), + (0x1e130c, 0x9a8478), + (0xd38312, 0xa83279), + (0x73c8a9, 0x373b44), + (0xabbaab, 0xffffff), + (0xfdfc47, 0x24fe41), + (0x83a4d4, 0xb6fbff), + (0x485563, 0x29323c), + (0x52c234, 0x061700), + (0xfe8c00, 0xf83600), + (0x00c6ff, 0x0072ff), + (0x70e1f5, 0xffd194), + (0x556270, 0xff6b6b), + (0x9d50bb, 0x6e48aa), + (0x780206, 0x061161), + (0xb3ffab, 0x12fff7), + (0xaaffa9, 0x11ffbd), + (0x000000, 0xe74c3c), + (0xf0c27b, 0x4b1248), + (0xff4e50, 0xf9d423), + (0xadd100, 0x7b920a), + (0xfbd3e9, 0xbb377d), + (0x606c88, 0x3f4c6b), + (0xc9ffbf, 0xffafbd), + (0x649173, 0xdbd5a4), + (0xb993d6, 0x8ca6db), + (0x870000, 0x190a05), + (0x00d2ff, 0x3a7bd5), + (0xd3959b, 0xbfe6ba), + (0xdad299, 0xb0dab9), + (0xf2709c, 0xff9472), + (0xe6dada, 0x274046), + (0x5d4157, 0xa8caba), + (0xddd6f3, 0xfaaca8), + (0x616161, 0x9bc5c3), + (0x50c9c3, 0x96deda), + (0x215f00, 0xe4e4d9), + (0xc21500, 0xffc500), + (0xefefbb, 0xd4d3dd), + (0xffeeee, 0xddefbb), + (0x666600, 0x999966), + (0xde6262, 0xffb88c), + (0xe9d362, 0x333333), + (0xd53369, 0xcbad6d), + (0xa73737, 0x7a2828), + (0xf857a6, 0xff5858), + (0x4b6cb7, 0x182848), + (0xfc354c, 0x0abfbc), + (0x414d0b, 0x727a17), + (0xe43a15, 0xe65245), + (0xc04848, 0x480048), + (0x5f2c82, 0x49a09d), + (0xec6f66, 0xf3a183), + (0x7474bf, 0x348ac7), + (0xece9e6, 0xffffff), + (0xdae2f8, 0xd6a4a4), + (0xed4264, 0xffedbc), + (0xdc2424, 0x4a569d), + (0x24c6dc, 0x514a9d), + (0x283048, 0x859398), + (0x3d7eaa, 0xffe47a), + (0x1cd8d2, 0x93edc7), + (0x232526, 0x414345), + (0x757f9a, 0xd7dde8), + (0x5c258d, 0x4389a2), + (0x134e5e, 0x71b280), + (0x2bc0e4, 0xeaecc6), + (0x085078, 0x85d8ce), + (0x4776e6, 0x8e54e9), + (0x614385, 0x516395), + (0x1f1c2c, 0x928dab), + (0x16222a, 0x3a6073), + (0xff8008, 0xffc837), + (0x1d976c, 0x93f9b9), + (0xeb3349, 0xf45c43), + (0xdd5e89, 0xf7bb97), + (0x4cb8c4, 0x3cd3ad), + (0x1d2b64, 0xf8cdda), + (0xff512f, 0xf09819), + (0x1a2980, 0x26d0ce), + (0xaa076b, 0x61045f), + (0xff512f, 0xdd2476), + (0xf09819, 0xedde5d), + (0x403b4a, 0xe7e9bb), + (0xe55d87, 0x5fc3e4), + (0x003973, 0xe5e5be), + (0x3ca55c, 0xb5ac49), + (0x348f50, 0x56b4d3), + (0xda22ff, 0x9733ee), + (0xede574, 0xe1f5c4), + (0xd31027, 0xea384d), + (0x16a085, 0xf4d03f), + (0x603813, 0xb29f94), + (0xe52d27, 0xb31217), + (0xff6e7f, 0xbfe9ff), + (0x314755, 0x26a0da), + (0x2b5876, 0x4e4376), + (0xe65c00, 0xf9d423), + (0x2193b0, 0x6dd5ed), + (0xcc2b5e, 0x753a88), + (0xec008c, 0xfc6767), + (0x1488cc, 0x2b32b2), + (0x00467f, 0xa5cc82), + (0x076585, 0xffffff), + (0xbbd2c5, 0x536976), + (0x9796f0, 0xfbc7d4), + (0xb79891, 0x94716b), + (0x536976, 0x292e49), + (0xacb6e5, 0x86fde8), + (0xffe000, 0x799f0c), + (0x00416a, 0xe4e5e6), + (0xffe259, 0xffa751), + (0x799f0c, 0xacbb78), + (0x334d50, 0xcbcaa5), + (0xf7f8f8, 0xacbb78), + (0xffe000, 0x799f0c), + (0x00416a, 0xe4e5e6) +] + +func generateGradientColors(color: UIColor) -> (UIColor, UIColor) { + var nearest: (colors: (lhs: UInt32, rhs: UInt32), distance: Int32)? + for (lhs, rhs) in colorPairs { + let lhsDistance = color.distance(to: UIColor(rgb: lhs)) + let rhsDistance = color.distance(to: UIColor(rgb: rhs)) + if let currentNearest = nearest { + if lhsDistance < currentNearest.distance || rhsDistance < currentNearest.distance { + if lhsDistance < rhsDistance { + nearest = ((lhs, rhs), lhsDistance) + } else { + nearest = ((rhs, lhs), rhsDistance) + } + } + } else { + if lhsDistance < rhsDistance { + nearest = ((lhs, rhs), lhsDistance) + } else { + nearest = ((rhs, lhs), rhsDistance) + } + } + } + + if let colors = nearest?.colors { + var colorHsb = color.hsb + var similarColorHsb = UIColor(rgb: colors.0).hsb + var complementingColorHsb = UIColor(rgb: colors.1).hsb + + var correction = (similarColorHsb.0 > 0.0 ? colorHsb.0 / similarColorHsb.0 : 1.0, similarColorHsb.1 > 0.0 ? colorHsb.1 / similarColorHsb.1 : 1.0, similarColorHsb.2 > 0.0 ? colorHsb.2 / similarColorHsb.2 : 1.0) + var correctedComplementingColor = UIColor(hue: min(1.0, complementingColorHsb.0 * correction.0), saturation: min(1.0, complementingColorHsb.1 * correction.1), brightness: min(1.0, complementingColorHsb.2 * correction.2), alpha: 1.0) + return (color, correctedComplementingColor) + } else { + return (color, color) + } +} diff --git a/submodules/SettingsUI/Sources/Themes/ThemeNameGenerator.swift b/submodules/SettingsUI/Sources/Themes/GenerateThemeName.swift similarity index 98% rename from submodules/SettingsUI/Sources/Themes/ThemeNameGenerator.swift rename to submodules/SettingsUI/Sources/Themes/GenerateThemeName.swift index f2c391e63d..7c54bdf1f7 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeNameGenerator.swift +++ b/submodules/SettingsUI/Sources/Themes/GenerateThemeName.swift @@ -103,7 +103,7 @@ private let colors: [UInt32: String] = [ 0x54a5f8: "Blue" ] -private let adjectives = [ +private let adjectives: [String] = [ "Ancient", "Antique", "Autumn", @@ -213,7 +213,7 @@ private let adjectives = [ "Winsome" ] -private let subjectives = [ +private let subjectives: [String] = [ "Ambrosia", "Attack", "Avalanche", @@ -301,7 +301,7 @@ func generateThemeName(accentColor: UIColor) -> String { var nearest: (color: UInt32, distance: Int32)? for (color, _) in colors { let distance = accentColor.distance(to: UIColor(rgb: color)) - if let currentNearest = nearest { + if let currentNearest = nearest { if distance < currentNearest.distance { nearest = (color, distance) } @@ -312,7 +312,6 @@ func generateThemeName(accentColor: UIColor) -> String { if let color = nearest?.color, let colorName = colors[color]?.capitalized { if arc4random() % 2 == 0 { - return "\((adjectives.randomElement() ?? "").capitalized) \(colorName)" } else { return "\(colorName) \((subjectives.randomElement() ?? "").capitalized)" diff --git a/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift b/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift index c7badf0e10..f1fb72e4cf 100644 --- a/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift +++ b/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift @@ -30,8 +30,8 @@ private func whiteColorImage(theme: PresentationTheme, color: UIColor) -> Signal } final class SettingsThemeWallpaperNode: ASDisplayNode { - private var wallpaper: TelegramWallpaper? - private var color: UIColor? + var wallpaper: TelegramWallpaper? + private var arguments: PatternWallpaperArguments? let buttonNode = HighlightTrackingButtonNode() let backgroundNode = ASDisplayNode() @@ -39,7 +39,7 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { private let statusNode: RadialStatusNode var pressed: (() -> Void)? - + init(overlayBackgroundColor: UIColor = UIColor(white: 0.0, alpha: 0.3)) { self.imageNode.contentAnimations = [.subsequentUpdates] @@ -63,6 +63,10 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { self.statusNode.transitionToState(state, animated: animated, completion: {}) } + func setOverlayBackgroundColor(_ color: UIColor) { + self.statusNode.backgroundNodeColor = color + } + func setWallpaper(context: AccountContext, wallpaper: TelegramWallpaper, selected: Bool, size: CGSize, cornerRadius: CGFloat = 0.0, synchronousLoad: Bool = false) { self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) @@ -87,7 +91,7 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { apply() case let .color(color): let theme = context.sharedContext.currentPresentationData.with { $0 }.theme - let uiColor = UIColor(rgb: UInt32(bitPattern: color)) + let uiColor = UIColor(rgb: color) if uiColor.distance(to: theme.list.itemBlocksBackgroundColor) < 200 { self.imageNode.isHidden = false self.backgroundNode.isHidden = true @@ -97,14 +101,19 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { } else { self.imageNode.isHidden = true self.backgroundNode.isHidden = false - self.backgroundNode.backgroundColor = UIColor(rgb: UInt32(bitPattern: color)) + self.backgroundNode.backgroundColor = UIColor(rgb: color) } - + case let .gradient(topColor, bottomColor, _): + self.imageNode.isHidden = false + self.backgroundNode.isHidden = true + self.imageNode.setSignal(gradientImage([UIColor(rgb: topColor), UIColor(rgb: bottomColor)])) + let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: CGSize(), boundingSize: size, intrinsicInsets: UIEdgeInsets())) + apply() case let .image(representations, _): self.imageNode.isHidden = false self.backgroundNode.isHidden = true - let convertedRepresentations: [ImageRepresentationWithReference] = representations.map({ ImageRepresentationWithReference(representation: $0, reference: .wallpaper(resource: $0.resource)) }) + let convertedRepresentations: [ImageRepresentationWithReference] = representations.map({ ImageRepresentationWithReference(representation: $0, reference: .wallpaper(wallpaper: nil, resource: $0.resource)) }) self.imageNode.setSignal(wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, thumbnail: true, autoFetchFullSize: true, synchronousLoad: synchronousLoad)) let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: largestImageRepresentation(representations)!.dimensions.cgSize.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets())) @@ -113,23 +122,30 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { self.imageNode.isHidden = false let convertedRepresentations : [ImageRepresentationWithReference] = file.file.previewRepresentations.map { - ImageRepresentationWithReference(representation: $0, reference: .wallpaper(resource: $0.resource)) + ImageRepresentationWithReference(representation: $0, reference: .wallpaper(wallpaper: .slug(file.slug), resource: $0.resource)) } let imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError> - if file.isPattern { + if wallpaper.isPattern { self.backgroundNode.isHidden = false + var patternColors: [UIColor] = [] var patternColor = UIColor(rgb: 0xd6e2ee, alpha: 0.5) var patternIntensity: CGFloat = 0.5 if let color = file.settings.color { if let intensity = file.settings.intensity { patternIntensity = CGFloat(intensity) / 100.0 } - patternColor = UIColor(rgb: UInt32(bitPattern: color), alpha: patternIntensity) + patternColor = UIColor(rgb: color, alpha: patternIntensity) + patternColors.append(patternColor) + + if let bottomColor = file.settings.bottomColor { + patternColors.append(UIColor(rgb: bottomColor, alpha: patternIntensity)) + } } + self.backgroundNode.backgroundColor = patternColor - self.color = patternColor + self.arguments = PatternWallpaperArguments(colors: patternColors, rotation: file.settings.rotation) imageSignal = patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, mode: .thumbnail, autoFetchFullSize: true) } else { self.backgroundNode.isHidden = true @@ -139,12 +155,12 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { self.imageNode.setSignal(imageSignal, attemptSynchronously: synchronousLoad) let dimensions = file.file.dimensions ?? PixelDimensions(width: 100, height: 100) - let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: dimensions.cgSize.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: self.color)) + let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: dimensions.cgSize.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets(), custom: self.arguments)) apply() } } else if let wallpaper = self.wallpaper { switch wallpaper { - case .builtin, .color: + case .builtin, .color, .gradient: let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: CGSize(), boundingSize: size, intrinsicInsets: UIEdgeInsets())) apply() case let .image(representations, _): @@ -152,7 +168,7 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { apply() case let .file(file): let dimensions = file.file.dimensions ?? PixelDimensions(width: 100, height: 100) - let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: dimensions.cgSize.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets(), emptyColor: self.color)) + let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: dimensions.cgSize.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets(), custom: self.arguments)) apply() } } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorController.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorController.swift index e3470bac21..21c9950124 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorController.swift @@ -9,94 +9,609 @@ import SyncCore import TelegramPresentationData import TelegramUIPreferences import AccountContext +import PresentationDataUtils +import MediaResources -private let colors: [Int32] = [0x007aff, 0x00c2ed, 0x29b327, 0xeb6ca4, 0xf08200, 0x9472ee, 0xd33213, 0xedb400, 0x6d839e] +private let randomBackgroundColors: [Int32] = [0x007aff, 0x00c2ed, 0x29b327, 0xeb6ca4, 0xf08200, 0x9472ee, 0xd33213, 0xedb400, 0x6d839e] + +extension TelegramThemeSettings { + convenience init(baseTheme: TelegramBaseTheme, accentColor: UIColor, messageColors: (top: UIColor, bottom: UIColor?)?, wallpaper: TelegramWallpaper?) { + var messageColorsValues: (UInt32, UInt32)? + if let colors = messageColors { + messageColorsValues = (colors.0.argb, colors.1?.argb ?? colors.0.argb) + } + self.init(baseTheme: baseTheme, accentColor: accentColor.argb, messageColors: messageColorsValues, wallpaper: wallpaper) + } +} + +enum ThemeAccentColorControllerMode { + case colors(themeReference: PresentationThemeReference, create: Bool) + case background(themeReference: PresentationThemeReference) + case edit(theme: PresentationTheme, wallpaper: TelegramWallpaper?, generalThemeReference: PresentationThemeReference?, defaultThemeReference: PresentationThemeReference?, create: Bool, completion: (PresentationTheme, TelegramThemeSettings?) -> Void) + + var themeReference: PresentationThemeReference? { + switch self { + case let .colors(themeReference, _), let .background(themeReference): + return themeReference + case let .edit(_, _, _, defaultThemeReference, _, _): + return defaultThemeReference + default: + return nil + } + } +} final class ThemeAccentColorController: ViewController { private let context: AccountContext - private let currentTheme: PresentationThemeReference - private let initialColor: UIColor - private let initialTheme: PresentationTheme + private let mode: ThemeAccentColorControllerMode + private let section: ThemeColorSection + private let initialBackgroundColor: UIColor? + private var presentationData: PresentationData + private var initialAccentColor: PresentationThemeAccentColor? private var controllerNode: ThemeAccentColorControllerNode { return self.displayNode as! ThemeAccentColorControllerNode } - private var presentationData: PresentationData + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + + private let segmentedTitleView: ThemeColorSegmentedTitleView - init(context: AccountContext, currentTheme: PresentationThemeReference, currentColor: UIColor?) { + private var applyDisposable = MetaDisposable() + + var completion: (() -> Void)? + + init(context: AccountContext, mode: ThemeAccentColorControllerMode) { self.context = context - self.currentTheme = currentTheme - - var color: UIColor - if let currentColor = currentColor { - color = currentColor - } - else if let randomColor = colors.randomElement() { - color = UIColor(rgb: UInt32(bitPattern: randomColor)) - } else { - color = defaultDayAccentColor - } - self.initialColor = color - self.initialTheme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: currentTheme, accentColor: color, serviceBackgroundColor: defaultServiceBackgroundColor, baseColor: nil, preview: true) ?? defaultPresentationTheme - + self.mode = mode self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationTheme: self.initialTheme, presentationStrings: self.presentationData.strings)) + var section: ThemeColorSection = .accent + if case .background = mode { + section = .background + } + self.section = section - self.title = self.presentationData.strings.AccentColor_Title + self.segmentedTitleView = ThemeColorSegmentedTitleView(theme: self.presentationData.theme, strings: self.presentationData.strings, selectedSection: section) + if case .background = mode { + self.initialBackgroundColor = randomBackgroundColors.randomElement().flatMap { UIColor(rgb: UInt32(bitPattern: $0)) } + } else { + self.initialBackgroundColor = nil + } + + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationTheme: self.presentationData.theme, presentationStrings: self.presentationData.strings)) + + self.navigationPresentation = .modal + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + self.segmentedTitleView.sectionUpdated = { [weak self] section in + if let strongSelf = self { + strongSelf.controllerNode.updateSection(section) + } + } + + self.segmentedTitleView.shouldUpdateSection = { [weak self] section, f in + guard let strongSelf = self else { + f(false) + return + } + guard section == .background else { + f(true) + return + } + + if strongSelf.controllerNode.requiresWallpaperChange { + let controller = textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Theme_Colors_ColorWallpaperWarning, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + f(false) + }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Theme_Colors_ColorWallpaperWarningProceed, action: { + f(true) + })]) + strongSelf.present(controller, in: .window(.root)) + } else { + f(true) + } + } + + if case .background = mode { + self.title = self.presentationData.strings.Wallpaper_Title + } else { + self.navigationItem.titleView = self.segmentedTitleView + } + self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + self.applyDisposable.dispose() + } + override func loadDisplayNode() { super.loadDisplayNode() - self.displayNode = ThemeAccentColorControllerNode(context: self.context, currentTheme: self.currentTheme, color: self.initialColor, theme: self.initialTheme, dismiss: { [weak self] in + let theme: PresentationTheme + let initialWallpaper: TelegramWallpaper + if case let .edit(editedTheme, walpaper, _, _, _, _) = self.mode { + theme = editedTheme + initialWallpaper = walpaper ?? editedTheme.chat.defaultWallpaper + } else { + theme = self.presentationData.theme + initialWallpaper = self.presentationData.chatWallpaper + } + + self.displayNode = ThemeAccentColorControllerNode(context: self.context, mode: self.mode, theme: theme, wallpaper: initialWallpaper, dismiss: { [weak self] in if let strongSelf = self { strongSelf.dismiss() } - }, apply: { [weak self] in + }, apply: { [weak self] state, serviceBackgroundColor in if let strongSelf = self { let context = strongSelf.context - let _ = (updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in - var themeSpecificAccentColors = current.themeSpecificAccentColors - let color = PresentationThemeAccentColor(baseColor: .custom, value: Int32(bitPattern: strongSelf.controllerNode.color)) - themeSpecificAccentColors[current.theme.index] = color + let initialAccentColor = strongSelf.initialAccentColor + let autoNightModeTriggered = strongSelf.presentationData.autoNightModeTriggered + + var coloredWallpaper: TelegramWallpaper? + if let backgroundColors = state.backgroundColors { + let color = backgroundColors.0.argb + let bottomColor = backgroundColors.1.flatMap { $0.argb } - var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers - - let theme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: strongSelf.currentTheme, accentColor: UIColor(rgb: strongSelf.controllerNode.color), serviceBackgroundColor: defaultServiceBackgroundColor, baseColor: color.baseColor) ?? defaultPresentationTheme - var chatWallpaper = current.chatWallpaper - if let wallpaper = current.themeSpecificChatWallpapers[current.theme.index], wallpaper.hasWallpaper { + if let patternWallpaper = state.patternWallpaper { + coloredWallpaper = patternWallpaper.withUpdatedSettings(WallpaperSettings(motion: state.motion, color: color, bottomColor: bottomColor, intensity: state.patternIntensity, rotation: state.rotation)) + } else if let bottomColor = bottomColor { + coloredWallpaper = .gradient(color, bottomColor, WallpaperSettings(motion: state.motion, rotation: state.rotation)) } else { - chatWallpaper = theme.chat.defaultWallpaper - themeSpecificChatWallpapers[current.theme.index] = chatWallpaper + coloredWallpaper = .color(color) + } + } + + + let apply: Signal + + let prepareWallpaper: Signal + if let patternWallpaper = state.patternWallpaper, case let .file(file) = patternWallpaper, let backgroundColors = state.backgroundColors { + let resource = file.file.resource + let representation = CachedPatternWallpaperRepresentation(color: backgroundColors.0.argb, bottomColor: backgroundColors.1.flatMap { $0.argb }, intensity: state.patternIntensity, rotation: state.rotation) + + var data: Data? + if let path = strongSelf.context.account.postbox.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + data = maybeData + } else if let path = strongSelf.context.sharedContext.accountManager.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + data = maybeData } - return PresentationThemeSettings(chatWallpaper: chatWallpaper, theme: strongSelf.currentTheme, themeSpecificAccentColors: themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) - }) |> deliverOnMainQueue).start(completed: { [weak self] in - if let strongSelf = self { - strongSelf.dismiss() + if let data = data { + strongSelf.context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + prepareWallpaper = (strongSelf.context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true) + |> filter({ $0.complete }) + |> take(1) + |> castError(CreateThemeError.self) + |> mapToSignal { _ -> Signal in + return .complete() + }) + } else { + prepareWallpaper = .complete() } - }) + } else { + prepareWallpaper = .complete() + } + + if case let .edit(theme, initialWallpaper, generalThemeReference, themeReference, _, completion) = strongSelf.mode { + let _ = (prepareWallpaper + |> deliverOnMainQueue).start(completed: { [weak self] in + let updatedTheme: PresentationTheme + + var settings: TelegramThemeSettings? + var hasSettings = false + var baseTheme: TelegramBaseTheme? + + if case let .cloud(theme) = generalThemeReference, let settings = theme.theme.settings { + hasSettings = true + baseTheme = settings.baseTheme + } else if case let .builtin(theme) = generalThemeReference { + hasSettings = true + baseTheme = theme.baseTheme + } + + if let themeReference = generalThemeReference { + updatedTheme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: themeReference, accentColor: state.accentColor, backgroundColors: state.backgroundColors, bubbleColors: state.messagesColors, wallpaper: state.initialWallpaper ?? coloredWallpaper, serviceBackgroundColor: serviceBackgroundColor) ?? defaultPresentationTheme + } else { + updatedTheme = customizePresentationTheme(theme, editing: false, accentColor: state.accentColor, backgroundColors: state.backgroundColors, bubbleColors: state.messagesColors, wallpaper: state.initialWallpaper ?? coloredWallpaper) + } + + if hasSettings, let baseTheme = baseTheme { + var messageColors: (Int32, Int32)? + if let colors = state.messagesColors { + messageColors = (Int32(bitPattern: colors.0.argb), Int32(bitPattern: colors.1?.argb ?? colors.0.argb)) + } + + settings = TelegramThemeSettings(baseTheme: baseTheme, accentColor: state.accentColor, messageColors: state.messagesColors, wallpaper: coloredWallpaper) + } + + completion(updatedTheme, settings) + }) + } else if case let .colors(theme, create) = strongSelf.mode { + var baseTheme: TelegramBaseTheme + var telegramTheme: TelegramTheme? + if case let .cloud(theme) = theme, let settings = theme.theme.settings { + telegramTheme = theme.theme + baseTheme = settings.baseTheme + } else if case let .builtin(theme) = theme { + baseTheme = theme.baseTheme + } else { + baseTheme = .classic + } + + let wallpaper = state.initialWallpaper ?? coloredWallpaper + + let settings = TelegramThemeSettings(baseTheme: baseTheme, accentColor: state.accentColor, messageColors: state.messagesColors, wallpaper: wallpaper) + let baseThemeReference = PresentationThemeReference.builtin(PresentationBuiltinThemeReference(baseTheme: baseTheme)) + + let apply: Signal + if create { + apply = (prepareWallpaper |> then(createTheme(account: context.account, title: generateThemeName(accentColor: state.accentColor), resource: nil, thumbnailData: nil, settings: settings))) + |> mapToSignal { next -> Signal in + if case let .result(resultTheme) = next { + let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: resultTheme).start() + return updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: wallpaper, creatorAccountId: context.account.id)) + + var updatedTheme = current.theme + var updatedAutomaticThemeSwitchSetting = current.automaticThemeSwitchSetting + if autoNightModeTriggered { + updatedAutomaticThemeSwitchSetting.theme = themeReference + } else { + updatedTheme = themeReference + } + + var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers + themeSpecificChatWallpapers[themeReference.index] = nil + + var themeSpecificAccentColors = current.themeSpecificAccentColors + themeSpecificAccentColors[baseThemeReference.index] = PresentationThemeAccentColor(themeIndex: themeReference.index) + + return PresentationThemeSettings(theme: updatedTheme, themeSpecificAccentColors: themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, useSystemFont: current.useSystemFont, fontSize: current.fontSize, listsFontSize: current.listsFontSize, chatBubbleSettings: current.chatBubbleSettings, automaticThemeSwitchSetting: updatedAutomaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) + }) + |> castError(CreateThemeError.self) + } else { + return .complete() + } + } + } else if let theme = telegramTheme { + apply = (prepareWallpaper |> then(updateTheme(account: context.account, accountManager: context.sharedContext.accountManager, theme: theme, title: theme.title, slug: theme.slug, resource: nil, settings: settings))) + |> mapToSignal { next -> Signal in + if case let .result(resultTheme) = next { + let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: resultTheme).start() + return updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: wallpaper, creatorAccountId: context.account.id)) + + var updatedTheme = current.theme + var updatedAutomaticThemeSwitchSetting = current.automaticThemeSwitchSetting + if autoNightModeTriggered { + updatedAutomaticThemeSwitchSetting.theme = themeReference + } else { + updatedTheme = themeReference + } + + var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers + themeSpecificChatWallpapers[themeReference.index] = nil + + var themeSpecificAccentColors = current.themeSpecificAccentColors + themeSpecificAccentColors[baseThemeReference.index] = PresentationThemeAccentColor(themeIndex: themeReference.index) + + return PresentationThemeSettings(theme: updatedTheme, themeSpecificAccentColors: themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, useSystemFont: current.useSystemFont, fontSize: current.fontSize, listsFontSize: current.listsFontSize, chatBubbleSettings: current.chatBubbleSettings, automaticThemeSwitchSetting: updatedAutomaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) + }) + |> castError(CreateThemeError.self) + } else { + return .complete() + } + } + } else { + apply = .complete() + } + + let disposable = strongSelf.applyDisposable + var cancelImpl: (() -> Void)? + let progress = Signal { [weak self] subscriber in + let controller = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + self?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.35, queue: Queue.mainQueue()) + + let progressDisposable = progress.start() + cancelImpl = { + if let strongSelf = self { + strongSelf.controllerNode.dismissed = false + } + disposable.set(nil) + } + disposable.set((apply + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + |> deliverOnMainQueue).start(completed: { + if let strongSelf = self { + strongSelf.completion?() + strongSelf.dismiss() + } + })) + } else if case .background = strongSelf.mode { + let autoNightModeTriggered = strongSelf.presentationData.autoNightModeTriggered + let _ = (updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager) { current in + var updated = current + let themeReference: PresentationThemeReference + if autoNightModeTriggered { + themeReference = current.automaticThemeSwitchSetting.theme + } else { + themeReference = current.theme + } + updated.themeSpecificChatWallpapers[themeReference.index] = coloredWallpaper + return updated + } |> deliverOnMainQueue).start(completed: { + if let strongSelf = self { + strongSelf.completion?() + strongSelf.dismiss() + } + }) + } } - }) + }, ready: self._ready) self.controllerNode.themeUpdated = { [weak self] theme in if let strongSelf = self { strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationTheme: theme, presentationStrings: strongSelf.presentationData.strings)) + strongSelf.segmentedTitleView.theme = theme + } + } + self.controllerNode.requestSectionUpdate = { [weak self] section in + if let strongSelf = self { + strongSelf.segmentedTitleView.setIndex(section.rawValue, animated: true) } } - self.displayNodeDidLoad() - } - - private func updateStrings() { + let _ = (combineLatest( + self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings]) |> take(1), + telegramWallpapers(postbox: context.account.postbox, network: context.account.network) |> take(1) + ) + |> deliverOnMainQueue).start(next: { [weak self] sharedData, wallpapers in + guard let strongSelf = self else { + return + } + let settings = (sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings] as? PresentationThemeSettings) ?? PresentationThemeSettings.defaultSettings + + let accentColor: UIColor + var initialWallpaper: TelegramWallpaper? + var backgroundColors: (UIColor, UIColor?)? + var patternWallpaper: TelegramWallpaper? + var patternIntensity: Int32 = 50 + var motion = false + let messageColors: (UIColor, UIColor?)? + var defaultMessagesColor: UIColor? + var rotation: Int32 = 0 + + func extractWallpaperParameters(_ wallpaper: TelegramWallpaper?) { + guard let wallpaper = wallpaper else { + return + } + if case let .file(file) = wallpaper, wallpaper.isPattern { + var patternColor = UIColor(rgb: 0xd6e2ee, alpha: 0.4) + var bottomColor: UIColor? + if let color = file.settings.color { + if let intensity = file.settings.intensity { + patternIntensity = intensity + } + patternColor = UIColor(rgb: color) + if let bottomColorValue = file.settings.bottomColor { + bottomColor = UIColor(rgb: bottomColorValue) + } + } + patternWallpaper = wallpaper + backgroundColors = (patternColor, bottomColor) + motion = file.settings.motion + rotation = file.settings.rotation ?? 0 + } else if case let .color(color) = wallpaper { + backgroundColors = (UIColor(rgb: color), nil) + } else if case let .gradient(topColor, bottomColor, settings) = wallpaper { + backgroundColors = (UIColor(rgb: topColor), UIColor(rgb: bottomColor)) + motion = settings.motion + rotation = settings.rotation ?? 0 + } else { + backgroundColors = nil + } + } + + if let themeReference = strongSelf.mode.themeReference { + var wallpaper: TelegramWallpaper + + func extractBuiltinWallpaper(_ currentWallpaper: TelegramWallpaper) { + if case let .builtin(settings) = currentWallpaper { + var defaultPatternWallpaper: TelegramWallpaper? + + for wallpaper in wallpapers { + if case let .file(file) = wallpaper, file.slug == "JqSUrO0-mFIBAAAAWwTvLzoWGQI" { + defaultPatternWallpaper = wallpaper + break + } + } + + if let defaultPatternWallpaper = defaultPatternWallpaper { + wallpaper = defaultPatternWallpaper.withUpdatedSettings(WallpaperSettings(blur: settings.blur, motion: settings.motion, color: 0xd6e2ee, bottomColor: nil, intensity: 40, rotation: nil)) + } + } + } + + if case .colors(_, true) = strongSelf.mode { + let themeSpecificAccentColor = settings.themeSpecificAccentColors[themeReference.index] + accentColor = themeSpecificAccentColor?.color ?? defaultDayAccentColor + + if let accentColor = themeSpecificAccentColor, let customWallpaper = settings.themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] { + wallpaper = customWallpaper + } else if let customWallpaper = settings.themeSpecificChatWallpapers[themeReference.index] { + wallpaper = customWallpaper + } else { + let theme = makePresentationTheme(mediaBox: strongSelf.context.sharedContext.accountManager.mediaBox, themeReference: themeReference, accentColor: themeSpecificAccentColor?.color, wallpaper: themeSpecificAccentColor?.wallpaper) ?? defaultPresentationTheme + wallpaper = theme.chat.defaultWallpaper + } + + extractBuiltinWallpaper(wallpaper) + + if !wallpaper.isColorOrGradient { + initialWallpaper = wallpaper + } + + if let initialBackgroundColor = strongSelf.initialBackgroundColor { + backgroundColors = (initialBackgroundColor, nil) + } else { + extractWallpaperParameters(wallpaper) + } + + if let bubbleColors = settings.themeSpecificAccentColors[themeReference.index]?.customBubbleColors { + if let bottomColor = bubbleColors.1 { + messageColors = (bubbleColors.0, bottomColor) + } else { + messageColors = (bubbleColors.0, nil) + } + } else { + if let themeReference = strongSelf.mode.themeReference, themeReference == .builtin(.dayClassic), settings.themeSpecificAccentColors[themeReference.index] == nil { + messageColors = (UIColor(rgb: 0xe1ffc7), nil) + } else { + messageColors = nil + } + } + } else { + let presentationTheme = makePresentationTheme(mediaBox: strongSelf.context.sharedContext.accountManager.mediaBox, themeReference: themeReference)! + if case let .cloud(theme) = themeReference, let themeSettings = theme.theme.settings { + accentColor = UIColor(argb: themeSettings.accentColor) + + if let customWallpaper = settings.themeSpecificChatWallpapers[themeReference.index] { + wallpaper = customWallpaper + } else { + wallpaper = presentationTheme.chat.defaultWallpaper + } + extractWallpaperParameters(wallpaper) + if !wallpaper.isColorOrGradient { + initialWallpaper = wallpaper + } + + if let colors = themeSettings.messageColors { + let topMessageColor = UIColor(argb: colors.top) + let bottomMessageColor = UIColor(argb: colors.bottom) + if topMessageColor.argb == bottomMessageColor.argb { + messageColors = (topMessageColor, nil) + } else { + messageColors = (topMessageColor, bottomMessageColor) + } + } else { + messageColors = nil + } + } else if case .builtin = themeReference { + let themeSpecificAccentColor = settings.themeSpecificAccentColors[themeReference.index] + accentColor = themeSpecificAccentColor?.color ?? defaultDayAccentColor + + if let accentColor = themeSpecificAccentColor, let customWallpaper = settings.themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] { + wallpaper = customWallpaper + } else if let customWallpaper = settings.themeSpecificChatWallpapers[themeReference.index] { + wallpaper = customWallpaper + } else { + let theme = makePresentationTheme(mediaBox: strongSelf.context.sharedContext.accountManager.mediaBox, themeReference: themeReference, accentColor: nil, wallpaper: themeSpecificAccentColor?.wallpaper) ?? defaultPresentationTheme + wallpaper = theme.chat.defaultWallpaper + } + + extractBuiltinWallpaper(wallpaper) + + if !wallpaper.isColorOrGradient { + initialWallpaper = wallpaper + } + + if let initialBackgroundColor = strongSelf.initialBackgroundColor { + backgroundColors = (initialBackgroundColor, nil) + } else { + extractWallpaperParameters(wallpaper) + } + + if let bubbleColors = settings.themeSpecificAccentColors[themeReference.index]?.customBubbleColors { + if let bottomColor = bubbleColors.1 { + messageColors = (bubbleColors.0, bottomColor) + } else { + messageColors = (bubbleColors.0, nil) + } + } else { + if let themeReference = strongSelf.mode.themeReference, themeReference == .builtin(.dayClassic), settings.themeSpecificAccentColors[themeReference.index] == nil { + messageColors = (UIColor(rgb: 0xe1ffc7), nil) + } else { + messageColors = nil + } + } + } else { + let themeSpecificAccentColor = settings.themeSpecificAccentColors[themeReference.index] + + let theme = makePresentationTheme(mediaBox: strongSelf.context.sharedContext.accountManager.mediaBox, themeReference: themeReference)! + + accentColor = theme.rootController.navigationBar.accentTextColor + + let wallpaper = theme.chat.defaultWallpaper + extractWallpaperParameters(wallpaper) + + if !wallpaper.isColorOrGradient { + initialWallpaper = wallpaper + } + + let topMessageColor = theme.chat.message.outgoing.bubble.withWallpaper.fill + let bottomMessageColor = theme.chat.message.outgoing.bubble.withWallpaper.gradientFill + + if topMessageColor.argb == bottomMessageColor.argb { + messageColors = (topMessageColor, nil) + } else { + messageColors = (topMessageColor, bottomMessageColor) + } + } + } + } else if case let .edit(theme, wallpaper, _, themeReference, _, _) = strongSelf.mode { + accentColor = theme.rootController.navigationBar.accentTextColor + + let wallpaper = wallpaper ?? theme.chat.defaultWallpaper + extractWallpaperParameters(wallpaper) + + if !wallpaper.isColorOrGradient { + initialWallpaper = wallpaper + } + + let topMessageColor = theme.chat.message.outgoing.bubble.withWallpaper.fill + let bottomMessageColor = theme.chat.message.outgoing.bubble.withWallpaper.gradientFill + + if topMessageColor.argb == bottomMessageColor.argb { + messageColors = (topMessageColor, nil) + } else { + messageColors = (topMessageColor, bottomMessageColor) + } + } else { + accentColor = defaultDayAccentColor + backgroundColors = nil + messageColors = nil + } + + let initialState = ThemeColorState(section: strongSelf.section, accentColor: accentColor, initialWallpaper: initialWallpaper, backgroundColors: backgroundColors, patternWallpaper: patternWallpaper, patternIntensity: patternIntensity, motion: motion, defaultMessagesColor: defaultMessagesColor, messagesColors: messageColors, rotation: rotation) + + strongSelf.controllerNode.updateState({ _ in + return initialState + }, animated: false) + }) + self.displayNodeDidLoad() } override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index c8f4087e97..57916acc46 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -10,6 +10,8 @@ import TelegramPresentationData import TelegramUIPreferences import ChatListUI import AccountContext +import WallpaperResources +import PresentationDataUtils private func generateMaskImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in @@ -25,46 +27,208 @@ private func generateMaskImage(color: UIColor) -> UIImage? { context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 80.0), options: CGGradientDrawingOptions()) }) } + +enum ThemeColorSection: Int { + case accent + case background + case messages +} + +struct ThemeColorState { + fileprivate var section: ThemeColorSection? + fileprivate var colorPanelCollapsed: Bool + fileprivate var displayPatternPanel: Bool + + var accentColor: UIColor + var initialWallpaper: TelegramWallpaper? + var backgroundColors: (UIColor, UIColor?)? + + fileprivate var preview: Bool + fileprivate var previousPatternWallpaper: TelegramWallpaper? + var patternWallpaper: TelegramWallpaper? + var patternIntensity: Int32 + var motion: Bool + + var defaultMessagesColor: UIColor? + var messagesColors: (UIColor, UIColor?)? + + var rotation: Int32 + + init() { + self.section = nil + self.colorPanelCollapsed = false + self.displayPatternPanel = false + self.accentColor = .clear + self.initialWallpaper = nil + self.backgroundColors = nil + self.preview = false + self.previousPatternWallpaper = nil + self.patternWallpaper = nil + self.patternIntensity = 50 + self.motion = false + self.defaultMessagesColor = nil + self.messagesColors = nil + self.rotation = 0 + } + + init(section: ThemeColorSection, accentColor: UIColor, initialWallpaper: TelegramWallpaper?, backgroundColors: (UIColor, UIColor?)?, patternWallpaper: TelegramWallpaper?, patternIntensity: Int32, motion: Bool, defaultMessagesColor: UIColor?, messagesColors: (UIColor, UIColor?)?, rotation: Int32 = 0) { + self.section = section + self.colorPanelCollapsed = false + self.displayPatternPanel = false + self.accentColor = accentColor + self.initialWallpaper = initialWallpaper + self.backgroundColors = backgroundColors + self.preview = false + self.previousPatternWallpaper = nil + self.patternWallpaper = patternWallpaper + self.patternIntensity = patternIntensity + self.motion = motion + self.defaultMessagesColor = defaultMessagesColor + self.messagesColors = messagesColors + self.rotation = rotation + } + + func isEqual(to otherState: ThemeColorState) -> Bool { + if self.accentColor != otherState.accentColor { + return false + } + if self.preview != otherState.preview { + return false + } + if self.patternWallpaper != otherState.patternWallpaper { + return false + } + if self.patternIntensity != otherState.patternIntensity { + return false + } + if self.rotation != otherState.rotation { + return false + } + if let lhsBackgroundColors = self.backgroundColors, let rhsBackgroundColors = otherState.backgroundColors { + if lhsBackgroundColors.0 != rhsBackgroundColors.0 { + return false + } + if let lhsSecondColor = lhsBackgroundColors.1, let rhsSecondColor = rhsBackgroundColors.1 { + if lhsSecondColor != rhsSecondColor { + return false + } + } else if (lhsBackgroundColors.1 == nil) != (rhsBackgroundColors.1 == nil) { + return false + } + } else if (self.backgroundColors == nil) != (otherState.backgroundColors == nil) { + return false + } + if let lhsMessagesColors = self.messagesColors, let rhsMessagesColors = otherState.messagesColors { + if lhsMessagesColors.0 != rhsMessagesColors.0 { + return false + } + if let lhsSecondColor = lhsMessagesColors.1, let rhsSecondColor = rhsMessagesColors.1 { + if lhsSecondColor != rhsSecondColor { + return false + } + } else if (lhsMessagesColors.1 == nil) != (rhsMessagesColors.1 == nil) { + return false + } + } else if (self.messagesColors == nil) != (otherState.messagesColors == nil) { + return false + } + return true + } +} + +private func calcPatternColors(for state: ThemeColorState) -> [UIColor] { + if let backgroundColors = state.backgroundColors { + let patternIntensity = CGFloat(state.patternIntensity) / 100.0 + let topPatternColor = backgroundColors.0.withAlphaComponent(patternIntensity) + if let bottomColor = backgroundColors.1 { + let bottomPatternColor = bottomColor.withAlphaComponent(patternIntensity) + return [topPatternColor, bottomPatternColor] + } else { + return [topPatternColor, topPatternColor] + } + } else { + let patternColor = UIColor(rgb: 0xd6e2ee, alpha: 0.5) + return [patternColor, patternColor] + } +} final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate { private let context: AccountContext private var theme: PresentationTheme - private let currentTheme: PresentationThemeReference + private let mode: ThemeAccentColorControllerMode private var presentationData: PresentationData + private let ready: Promise + + private let queue = Queue() + + private var state: ThemeColorState private let referenceTimestamp: Int32 private let scrollNode: ASScrollNode private let pageControlBackgroundNode: ASDisplayNode private let pageControlNode: PageControlNode - + private var motionButtonNode: WallpaperOptionButtonNode + private var patternButtonNode: WallpaperOptionButtonNode private let chatListBackgroundNode: ASDisplayNode private var chatNodes: [ListViewItemNode]? private let maskNode: ASImageNode - - private let chatBackgroundNode: WallpaperBackgroundNode + private let backgroundContainerNode: ASDisplayNode + private let backgroundWrapperNode: ASDisplayNode + private let immediateBackgroundNode: ASImageNode + private let signalBackgroundNode: TransformImageNode + private let messagesContainerNode: ASDisplayNode + private var dateHeaderNode: ListViewItemHeaderNode? private var messageNodes: [ListViewItemNode]? - - private var colorPanelNode: WallpaperColorPanelNode + private let colorPanelNode: WallpaperColorPanelNode + private let patternPanelNode: WallpaperPatternPanelNode private let toolbarNode: WallpaperGalleryToolbarNode + private var serviceColorDisposable: Disposable? + private var stateDisposable: Disposable? + private let statePromise = Promise() + private let themePromise = Promise() + private var wallpaper: TelegramWallpaper + private var serviceBackgroundColor: UIColor? + private let serviceBackgroundColorPromise = Promise() + private var wallpaperDisposable = MetaDisposable() + + private var currentBackgroundColors: (UIColor, UIColor?, Int32?)? + private var currentBackgroundPromise = Promise<(UIColor, UIColor?)?>() + + private var patternWallpaper: TelegramWallpaper? + private var patternArguments: PatternWallpaperArguments? + private var patternArgumentsPromise = Promise() + private var patternArgumentsDisposable: Disposable? + + var themeUpdated: ((PresentationTheme) -> Void)? + var requestSectionUpdate: ((ThemeColorSection) -> Void)? + + var dismissed = false + private var validLayout: (ContainerViewLayout, CGFloat, CGFloat)? - private var serviceColorDisposable: Disposable? - private var colorDisposable: Disposable? - private let colorValue = ValuePromise(ignoreRepeated: true) - - var themeUpdated: ((PresentationTheme) -> Void)? - var color: UInt32 { - return self.colorPanelNode.color.rgb + var requiresWallpaperChange: Bool { + switch self.wallpaper { + case .image, .builtin: + return true + case let .file(file): + return !self.wallpaper.isPattern + default: + return false + } } - init(context: AccountContext, currentTheme: PresentationThemeReference, color: UIColor, theme: PresentationTheme, dismiss: @escaping () -> Void, apply: @escaping () -> Void) { + init(context: AccountContext, mode: ThemeAccentColorControllerMode, theme: PresentationTheme, wallpaper: TelegramWallpaper, dismiss: @escaping () -> Void, apply: @escaping (ThemeColorState, UIColor?) -> Void, ready: Promise) { self.context = context - self.currentTheme = currentTheme + self.mode = mode + self.state = ThemeColorState() self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.theme = theme - self.colorValue.set(color) + self.wallpaper = self.presentationData.chatWallpaper + let bubbleCorners = self.presentationData.chatBubbleCorners + + self.ready = ready let calendar = Calendar(identifier: .gregorian) var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: Date()) @@ -76,22 +240,36 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate self.scrollNode = ASScrollNode() self.pageControlBackgroundNode = ASDisplayNode() self.pageControlBackgroundNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.3) - self.pageControlBackgroundNode.cornerRadius = 8.0 + self.pageControlBackgroundNode.cornerRadius = 10.5 - self.pageControlNode = PageControlNode(dotColor: self.theme.chatList.unreadBadgeActiveBackgroundColor, inactiveDotColor: self.presentationData.theme.list.pageIndicatorInactiveColor) + self.pageControlNode = PageControlNode(dotSpacing: 7.0, dotColor: .white, inactiveDotColor: UIColor.white.withAlphaComponent(0.4)) + + self.motionButtonNode = WallpaperOptionButtonNode(title: self.presentationData.strings.WallpaperPreview_Motion, value: .check(false)) + self.patternButtonNode = WallpaperOptionButtonNode(title: self.presentationData.strings.WallpaperPreview_Pattern, value: .check(false)) self.chatListBackgroundNode = ASDisplayNode() - self.chatBackgroundNode = WallpaperBackgroundNode() - self.chatBackgroundNode.displaysAsynchronously = false - if case .color = self.presentationData.chatWallpaper { - } else { - self.chatBackgroundNode.image = chatControllerBackgroundImage(theme: theme, wallpaper: self.presentationData.chatWallpaper, mediaBox: context.sharedContext.accountManager.mediaBox, knockoutMode: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper) - self.chatBackgroundNode.motionEnabled = self.presentationData.chatWallpaper.settings?.motion ?? false - } + + self.backgroundContainerNode = ASDisplayNode() + self.backgroundContainerNode.clipsToBounds = true + self.backgroundWrapperNode = ASDisplayNode() + self.immediateBackgroundNode = ASImageNode() + self.signalBackgroundNode = TransformImageNode() + self.signalBackgroundNode.displaysAsynchronously = false + + self.messagesContainerNode = ASDisplayNode() + self.messagesContainerNode.clipsToBounds = true + self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) self.colorPanelNode = WallpaperColorPanelNode(theme: self.theme, strings: self.presentationData.strings) - self.colorPanelNode.color = color - self.toolbarNode = WallpaperGalleryToolbarNode(theme: self.theme, strings: self.presentationData.strings) + self.patternPanelNode = WallpaperPatternPanelNode(context: self.context, theme: self.theme, strings: self.presentationData.strings) + + let doneButtonType: WallpaperGalleryToolbarDoneButtonType + if case .edit(_, _, _, _, true, _) = self.mode { + doneButtonType = .proceed + } else { + doneButtonType = .set + } + self.toolbarNode = WallpaperGalleryToolbarNode(theme: self.theme, strings: self.presentationData.strings, doneButtonType: doneButtonType) self.maskNode = ASImageNode() self.maskNode.displaysAsynchronously = false @@ -105,13 +283,8 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate }) self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor - self.chatListBackgroundNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor - if case let .color(value) = self.presentationData.theme.chat.defaultWallpaper { - self.chatBackgroundNode.backgroundColor = UIColor(rgb: UInt32(bitPattern: value)) - } - self.pageControlNode.isUserInteractionEnabled = false self.pageControlNode.pagesCount = 2 @@ -119,85 +292,332 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate self.chatListBackgroundNode.addSubnode(self.maskNode) self.addSubnode(self.pageControlBackgroundNode) self.addSubnode(self.pageControlNode) + self.addSubnode(self.motionButtonNode) + self.addSubnode(self.patternButtonNode) self.addSubnode(self.colorPanelNode) + self.addSubnode(self.patternPanelNode) self.addSubnode(self.toolbarNode) self.scrollNode.addSubnode(self.chatListBackgroundNode) - self.scrollNode.addSubnode(self.chatBackgroundNode) + self.scrollNode.addSubnode(self.backgroundContainerNode) + self.scrollNode.addSubnode(self.messagesContainerNode) - self.colorPanelNode.colorChanged = { [weak self] color, ended in + self.backgroundContainerNode.addSubnode(self.backgroundWrapperNode) + self.backgroundWrapperNode.addSubnode(self.immediateBackgroundNode) + self.backgroundWrapperNode.addSubnode(self.signalBackgroundNode) + + self.signalBackgroundNode.imageUpdated = { [weak self] _ in if let strongSelf = self { - strongSelf.colorValue.set(color) + strongSelf.ready.set(.single(true)) + strongSelf.signalBackgroundNode.contentAnimations = [] } } - self.toolbarNode.cancel = { - dismiss() - } - self.toolbarNode.done = { - apply() + self.motionButtonNode.addTarget(self, action: #selector(self.toggleMotion), forControlEvents: .touchUpInside) + self.patternButtonNode.addTarget(self, action: #selector(self.togglePattern), forControlEvents: .touchUpInside) + + self.colorPanelNode.colorAdded = { [weak self] in + if let strongSelf = self { + strongSelf.signalBackgroundNode.contentAnimations = [.subsequentUpdates] + } } - self.colorDisposable = (self.colorValue.get() - |> deliverOn(Queue.concurrentDefaultQueue()) - |> map { color -> PresentationTheme in - let theme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: currentTheme, accentColor: color, serviceBackgroundColor: defaultServiceBackgroundColor, baseColor: nil, preview: true) ?? defaultPresentationTheme - - let wallpaper = context.sharedContext.currentPresentationData.with { $0 }.chatWallpaper - let _ = PresentationResourcesChat.principalGraphics(mediaBox: context.account.postbox.mediaBox, knockoutWallpaper: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: theme, wallpaper: wallpaper, gradientBubbles: context.sharedContext.immediateExperimentalUISettings.gradientBubbles) - - return theme - } - |> deliverOnMainQueue).start(next: { [weak self] theme in + self.colorPanelNode.colorRemoved = { [weak self] in if let strongSelf = self { + strongSelf.signalBackgroundNode.contentAnimations = [.subsequentUpdates] + } + } + + self.colorPanelNode.colorsChanged = { [weak self] firstColor, secondColor, ended in + if let strongSelf = self, let section = strongSelf.state.section { + strongSelf.updateState({ current in + var updated = current + updated.preview = !ended + switch section { + case .accent: + if let firstColor = firstColor { + updated.accentColor = firstColor + } + case .background: + if let firstColor = firstColor { + updated.backgroundColors = (firstColor, secondColor) + } else { + updated.backgroundColors = nil + } + case .messages: + if let firstColor = firstColor { + updated.messagesColors = (firstColor, secondColor) + } else { + updated.messagesColors = nil + } + } + return updated + }) + } + } + + self.colorPanelNode.colorSelected = { [weak self] in + if let strongSelf = self, strongSelf.state.colorPanelCollapsed { + strongSelf.updateState({ current in + var updated = current + updated.colorPanelCollapsed = false + return updated + }, animated: true) + } + } + + self.colorPanelNode.rotate = { [weak self] in + if let strongSelf = self { + strongSelf.updateState({ current in + var updated = current + var newRotation = updated.rotation + 45 + if newRotation >= 360 { + newRotation = 0 + } + updated.rotation = newRotation + return updated + }, animated: true) + } + } + + self.patternPanelNode.patternChanged = { [weak self] wallpaper, intensity, preview in + if let strongSelf = self { + strongSelf.updateState({ current in + var updated = current + updated.patternWallpaper = wallpaper + updated.patternIntensity = intensity ?? 50 + updated.preview = preview + return updated + }, animated: true) + } + } + + self.toolbarNode.cancel = { [weak self] in + if let strongSelf = self { + if strongSelf.state.displayPatternPanel { + strongSelf.updateState({ current in + var updated = current + updated.displayPatternPanel = false + updated.patternWallpaper = nil + return updated + }, animated: true) + } else { + dismiss() + } + } + } + + self.toolbarNode.done = { [weak self] in + if let strongSelf = self { + if strongSelf.state.displayPatternPanel { + strongSelf.updateState({ current in + var updated = current + updated.displayPatternPanel = false + return updated + }, animated: true) + } else { + if !strongSelf.dismissed { + strongSelf.dismissed = true + apply(strongSelf.state, strongSelf.serviceBackgroundColor) + } + } + } + } + + self.stateDisposable = (self.statePromise.get() + |> deliverOn(self.queue) + |> mapToThrottled { next -> Signal in + return .single(next) |> then(.complete() |> delay(0.0166667, queue: self.queue)) + } + |> map { [weak self] state -> (PresentationTheme?, (TelegramWallpaper, UIImage?, Signal<(TransformImageArguments) -> DrawingContext?, NoError>?, (() -> Void)?), UIColor, (UIColor, UIColor?)?, PatternWallpaperArguments, Bool) in + let accentColor = state.accentColor + var backgroundColors = state.backgroundColors + let messagesColors = state.messagesColors + + var wallpaper: TelegramWallpaper + var wallpaperImage: UIImage? + var wallpaperSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + + var singleBackgroundColor: UIColor? + + var updateOnlyWallpaper = false + if state.section == .background && state.preview { + updateOnlyWallpaper = true + } + + if let backgroundColors = backgroundColors { + if let patternWallpaper = state.patternWallpaper, case let .file(file) = patternWallpaper { + let color = backgroundColors.0.argb + let bottomColor = backgroundColors.1.flatMap { $0.argb } + wallpaper = patternWallpaper.withUpdatedSettings(WallpaperSettings(motion: state.motion, color: color, bottomColor: bottomColor, intensity: state.patternIntensity, rotation: state.rotation)) + + let dimensions = file.file.dimensions ?? PixelDimensions(width: 100, height: 100) + var convertedRepresentations: [ImageRepresentationWithReference] = [] + for representation in file.file.previewRepresentations { + convertedRepresentations.append(ImageRepresentationWithReference(representation: representation, reference: .wallpaper(wallpaper: .slug(file.slug), resource: representation.resource))) + } + convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) + + wallpaperSignal = patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, mode: .screen, autoFetchFullSize: true) + } else if let bottomColor = backgroundColors.1 { + wallpaper = .gradient(backgroundColors.0.argb, bottomColor.argb, WallpaperSettings(rotation: state.rotation)) + wallpaperSignal = gradientImage([backgroundColors.0, bottomColor], rotation: state.rotation) + } else { + wallpaper = .color(backgroundColors.0.argb) + } + } else if let themeReference = mode.themeReference, case let .builtin(theme) = themeReference, state.initialWallpaper == nil { + var suggestedWallpaper: TelegramWallpaper + switch theme { + case .dayClassic: + let topColor = accentColor.withMultiplied(hue: 1.010, saturation: 0.414, brightness: 0.957) + let bottomColor = accentColor.withMultiplied(hue: 1.019, saturation: 0.867, brightness: 0.965) + suggestedWallpaper = .gradient(topColor.argb, bottomColor.argb, WallpaperSettings()) + wallpaperSignal = gradientImage([topColor, bottomColor], rotation: state.rotation) + backgroundColors = (topColor, bottomColor) + case .nightAccent: + let color = accentColor.withMultiplied(hue: 1.024, saturation: 0.573, brightness: 0.18) + suggestedWallpaper = .color(color.argb) + backgroundColors = (color, nil) + default: + suggestedWallpaper = .builtin(WallpaperSettings()) + } + wallpaper = suggestedWallpaper + } else { + wallpaper = state.initialWallpaper ?? .builtin(WallpaperSettings()) + wallpaperImage = chatControllerBackgroundImage(theme: nil, wallpaper: wallpaper, mediaBox: context.sharedContext.accountManager.mediaBox, knockoutMode: false) + } + + let serviceBackgroundColor = serviceColor(for: (wallpaper, wallpaperImage)) + let updatedTheme: PresentationTheme? + + if !updateOnlyWallpaper { + if let themeReference = mode.themeReference { + updatedTheme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: themeReference, accentColor: accentColor, backgroundColors: backgroundColors, bubbleColors: messagesColors, serviceBackgroundColor: serviceBackgroundColor, preview: true) ?? defaultPresentationTheme + } else if case let .edit(theme, _, _, _, _, _) = mode { + updatedTheme = customizePresentationTheme(theme, editing: false, accentColor: accentColor, backgroundColors: backgroundColors, bubbleColors: messagesColors) + } else { + updatedTheme = theme + } + + let _ = PresentationResourcesChat.principalGraphics(mediaBox: context.account.postbox.mediaBox, knockoutWallpaper: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: updatedTheme!, wallpaper: wallpaper, bubbleCorners: bubbleCorners) + } else { + updatedTheme = nil + } + + let patternArguments = PatternWallpaperArguments(colors: calcPatternColors(for: state), rotation: wallpaper.settings?.rotation ?? 0, preview: state.preview) + + var wallpaperApply: (() -> Void)? + if let strongSelf = self, wallpaper.isPattern, let (layout, _, _) = strongSelf.validLayout { + let makeImageLayout = strongSelf.signalBackgroundNode.asyncLayout() + wallpaperApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: wallpaper.dimensions ?? layout.size, boundingSize: layout.size, intrinsicInsets: UIEdgeInsets(), custom: patternArguments)) + } + + return (updatedTheme, (wallpaper, wallpaperImage, wallpaperSignal, wallpaperApply), serviceBackgroundColor, backgroundColors, patternArguments, state.preview) + } + |> deliverOnMainQueue).start(next: { [weak self] theme, wallpaperImageAndSignal, serviceBackgroundColor, backgroundColors, patternArguments, preview in + guard let strongSelf = self else { + return + } + let (wallpaper, wallpaperImage, wallpaperSignal, wallpaperApply) = wallpaperImageAndSignal + + if let theme = theme { strongSelf.theme = theme strongSelf.themeUpdated?(theme) - + strongSelf.themePromise.set(.single(theme)) strongSelf.colorPanelNode.updateTheme(theme) strongSelf.toolbarNode.updateThemeAndStrings(theme: theme, strings: strongSelf.presentationData.strings) + strongSelf.chatListBackgroundNode.backgroundColor = theme.chatList.backgroundColor strongSelf.maskNode.image = generateMaskImage(color: theme.chatList.backgroundColor) + } + + strongSelf.serviceBackgroundColor = serviceBackgroundColor + strongSelf.serviceBackgroundColorPromise.set(.single(serviceBackgroundColor)) + + if case let .color(value) = wallpaper { + strongSelf.backgroundColor = UIColor(rgb: value) + strongSelf.immediateBackgroundNode.backgroundColor = UIColor(rgb: value) + strongSelf.immediateBackgroundNode.image = nil + strongSelf.signalBackgroundNode.isHidden = true + strongSelf.signalBackgroundNode.contentAnimations = [] + strongSelf.signalBackgroundNode.reset() + strongSelf.patternWallpaper = nil + strongSelf.ready.set(.single(true) ) + } else if let wallpaperImage = wallpaperImage { + strongSelf.immediateBackgroundNode.image = wallpaperImage + strongSelf.signalBackgroundNode.isHidden = true + strongSelf.signalBackgroundNode.contentAnimations = [] + strongSelf.signalBackgroundNode.reset() + strongSelf.patternWallpaper = nil + strongSelf.ready.set(.single(true) ) + } else if let wallpaperSignal = wallpaperSignal { + strongSelf.signalBackgroundNode.contentMode = .scaleToFill + strongSelf.signalBackgroundNode.isHidden = false - if case let .color(value) = theme.chat.defaultWallpaper { - strongSelf.backgroundColor = UIColor(rgb: UInt32(bitPattern: value)) - strongSelf.chatListBackgroundNode.backgroundColor = UIColor(rgb: UInt32(bitPattern: value)) - strongSelf.chatBackgroundNode.backgroundColor = UIColor(rgb: UInt32(bitPattern: value)) - } - - if let (layout, navigationBarHeight, messagesBottomInset) = strongSelf.validLayout { - strongSelf.pageControlNode.dotColor = theme.chatList.unreadBadgeActiveBackgroundColor - strongSelf.pageControlNode.inactiveDotColor = theme.list.pageIndicatorInactiveColor - strongSelf.updateChatsLayout(layout: layout, topInset: navigationBarHeight, transition: .immediate) - strongSelf.updateMessagesLayout(layout: layout, bottomInset: messagesBottomInset, transition: .immediate) + if case let .file(file) = wallpaper, let (layout, _, _) = strongSelf.validLayout { + wallpaperApply?() + + if let previousWallpaper = strongSelf.patternWallpaper, case let .file(previousFile) = previousWallpaper, file.id == previousFile.id { + } else { + strongSelf.signalBackgroundNode.setSignal(wallpaperSignal) + strongSelf.patternWallpaper = wallpaper + } + } else { + strongSelf.signalBackgroundNode.setSignal(wallpaperSignal) + strongSelf.patternWallpaper = nil } } + strongSelf.wallpaper = wallpaper + strongSelf.patternArguments = patternArguments + + if !preview { + if let backgroundColors = backgroundColors { + strongSelf.currentBackgroundColors = (backgroundColors.0, backgroundColors.1, strongSelf.state.rotation) + } else { + strongSelf.currentBackgroundColors = nil + } + strongSelf.patternPanelNode.backgroundColors = strongSelf.currentBackgroundColors + } + + if let _ = theme, let (layout, navigationBarHeight, messagesBottomInset) = strongSelf.validLayout { + strongSelf.updateChatsLayout(layout: layout, topInset: navigationBarHeight, transition: .immediate) + strongSelf.updateMessagesLayout(layout: layout, bottomInset: messagesBottomInset, transition: .immediate) + } }) - - self.serviceColorDisposable = (chatServiceBackgroundColor(wallpaper: self.presentationData.chatWallpaper, mediaBox: context.account.postbox.mediaBox) + + self.serviceColorDisposable = (((self.themePromise.get() + |> mapToSignal { theme -> Signal in + return chatServiceBackgroundColor(wallpaper: wallpaper, mediaBox: context.account.postbox.mediaBox) + }) + |> take(1) + |> then(self.serviceBackgroundColorPromise.get())) |> deliverOnMainQueue).start(next: { [weak self] color in if let strongSelf = self { - if strongSelf.presentationData.chatWallpaper.hasWallpaper { - strongSelf.pageControlBackgroundNode.backgroundColor = color - } else { - strongSelf.pageControlBackgroundNode.backgroundColor = .clear - } + strongSelf.patternPanelNode.serviceBackgroundColor = color + strongSelf.pageControlBackgroundNode.backgroundColor = color + strongSelf.patternButtonNode.buttonColor = color + strongSelf.motionButtonNode.buttonColor = color } }) } deinit { - self.colorDisposable?.dispose() + self.stateDisposable?.dispose() self.serviceColorDisposable?.dispose() + self.wallpaperDisposable.dispose() + self.patternArgumentsDisposable?.dispose() } override func didLoad() { super.didLoad() + self.scrollNode.view.bounces = false self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.isPagingEnabled = true self.scrollNode.view.delegate = self self.pageControlNode.setPage(0.0) self.colorPanelNode.view.disablesInteractiveTransitionGestureRecognizer = true + self.patternPanelNode.view.disablesInteractiveTransitionGestureRecognizer = true } func scrollViewDidScroll(_ scrollView: UIScrollView) { @@ -207,13 +627,149 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate } } + func updateState(_ f: (ThemeColorState) -> ThemeColorState, animated: Bool = false) { + let previousState = self.state + self.state = f(self.state) + + var needsLayout = false + var animationCurve = ContainedViewLayoutTransitionCurve.easeInOut + var animationDuration: Double = 0.3 + + let visibleStateChange = !previousState.isEqual(to: self.state) + if visibleStateChange { + self.statePromise.set(.single(self.state)) + } + + let colorPanelCollapsed = self.state.colorPanelCollapsed + + if (previousState.patternWallpaper != nil) != (self.state.patternWallpaper != nil) { + self.patternButtonNode.setSelected(self.state.patternWallpaper != nil, animated: animated) + } + + if previousState.motion != self.state.motion { + self.motionButtonNode.setSelected(self.state.motion, animated: animated) + self.setMotionEnabled(self.state.motion, animated: animated) + } + + let sectionChanged = previousState.section != self.state.section + if sectionChanged, let section = self.state.section { + self.view.endEditing(true) + + let firstColor: UIColor? + let secondColor: UIColor? + var defaultColor: UIColor? + switch section { + case .accent: + firstColor = self.state.accentColor ?? defaultDayAccentColor + secondColor = nil + case .background: + if let themeReference = self.mode.themeReference, case let .builtin(theme) = themeReference { + switch theme { + case .dayClassic: + defaultColor = self.state.accentColor.withMultiplied(hue: 1.019, saturation: 0.867, brightness: 0.965) + case .nightAccent: + defaultColor = self.state.accentColor.withMultiplied(hue: 1.024, saturation: 0.573, brightness: 0.18) + default: + break + } + } + if let backgroundColors = self.state.backgroundColors { + firstColor = backgroundColors.0 + secondColor = backgroundColors.1 + } else if previousState.initialWallpaper != nil, let image = self.immediateBackgroundNode.image { + firstColor = averageColor(from: image) + secondColor = nil + } else { + firstColor = nil + secondColor = nil + } + case .messages: + if let defaultMessagesColor = self.state.defaultMessagesColor { + defaultColor = defaultMessagesColor + } else if let themeReference = self.mode.themeReference, case let .builtin(theme) = themeReference, theme == .nightAccent { + defaultColor = self.state.accentColor.withMultiplied(hue: 1.019, saturation: 0.731, brightness: 0.59) + } else { + defaultColor = self.state.accentColor + } + if let messagesColors = self.state.messagesColors { + firstColor = messagesColors.0 + secondColor = messagesColors.1 + } else { + firstColor = nil + secondColor = nil + } + } + + self.colorPanelNode.updateState({ _ in + return WallpaperColorPanelNodeState(selection: colorPanelCollapsed ? .none : .first, firstColor: firstColor, defaultColor: defaultColor, secondColor: secondColor, secondColorAvailable: self.state.section != .accent, rotateAvailable: self.state.section == .background, rotation: self.state.rotation ?? 0, preview: false, simpleGradientGeneration: self.state.section == .messages) + }, animated: animated) + + needsLayout = true + } + + if previousState.colorPanelCollapsed != self.state.colorPanelCollapsed { + animationCurve = .spring + animationDuration = 0.45 + needsLayout = true + + self.colorPanelNode.updateState({ current in + var updated = current + updated.selection = colorPanelCollapsed ? .none : .first + return updated + }, animated: animated) + } + + if previousState.displayPatternPanel != self.state.displayPatternPanel { + let cancelButtonType: WallpaperGalleryToolbarCancelButtonType + let doneButtonType: WallpaperGalleryToolbarDoneButtonType + if self.state.displayPatternPanel { + doneButtonType = .apply + cancelButtonType = .discard + } else { + if case .edit(_, _, _, _, true, _) = self.mode { + doneButtonType = .proceed + } else { + doneButtonType = .set + } + cancelButtonType = .cancel + } + + self.toolbarNode.cancelButtonType = cancelButtonType + self.toolbarNode.doneButtonType = doneButtonType + + animationCurve = .easeInOut + animationDuration = 0.3 + needsLayout = true + } + + if (previousState.patternWallpaper == nil) != (self.state.patternWallpaper == nil) { + needsLayout = true + } + + if needsLayout, let (layout, navigationBarHeight, _) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: animated ? .animated(duration: animationDuration, curve: animationCurve) : .immediate) + } + } + + func updateSection(_ section: ThemeColorSection) { + self.updateState({ current in + var updated = current + if section == .background { + updated.initialWallpaper = nil + } + updated.section = section + updated.displayPatternPanel = false + return updated + }, animated: true) + } + private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) { var items: [ChatListItem] = [] - let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _ in }, togglePeerSelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, activateChatPreview: { _, _, gesture in + let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, activateChatPreview: { _, _, gesture in gesture?.cancel() }) - let chatListPresentationData = ChatListPresentationData(theme: self.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) + let chatListPresentationData = ChatListPresentationData(theme: self.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) let peers = SimpleDictionary() let messages = SimpleDictionary() @@ -227,17 +783,17 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate let timestamp = self.referenceTimestamp let timestamp1 = timestamp + 120 - items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex(id: MessageId(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer1.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp1, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: selfPeer, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer1), combinedReadState: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, PeerReadState.idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: 0, markedUnread: false))]), notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) + items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex(id: MessageId(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer1.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp1, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: selfPeer, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer1), combinedReadState: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, PeerReadState.idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: 0, markedUnread: false))]), notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) let presenceTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + 60 * 60) let timestamp2 = timestamp + 3660 - items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer2.id, namespace: 0, id: 0), timestamp: timestamp2)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer2.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp2, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer2, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer2), combinedReadState: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, PeerReadState.idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: 1, markedUnread: false))]), notificationSettings: nil, presence: TelegramUserPresence(status: .present(until: presenceTimestamp), lastActivity: presenceTimestamp), summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) + items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer2.id, namespace: 0, id: 0), timestamp: timestamp2)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer2.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp2, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer2, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer2), combinedReadState: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, PeerReadState.idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: 1, markedUnread: false))]), notificationSettings: nil, presence: TelegramUserPresence(status: .present(until: presenceTimestamp), lastActivity: presenceTimestamp), summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) let timestamp3 = timestamp + 3200 - items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer3.id, namespace: 0, id: 0), timestamp: timestamp3)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer3.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp3, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer3Author, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer3), combinedReadState: nil, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) + items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer3.id, namespace: 0, id: 0), timestamp: timestamp3)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer3.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp3, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer3Author, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer3), combinedReadState: nil, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) let timestamp4 = timestamp + 3000 - items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer4.id, namespace: 0, id: 0), timestamp: timestamp4)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer4.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp4, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer4, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer4), combinedReadState: nil, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) + items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer4.id, namespace: 0, id: 0), timestamp: timestamp4)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer4.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp4, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer4, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer4), combinedReadState: nil, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) let params = ListViewItemLayoutParams(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) if let chatNodes = self.chatNodes { @@ -281,6 +837,8 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate } private func updateMessagesLayout(layout: ContainerViewLayout, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + let headerItem = self.context.sharedContext.makeChatMessageDateHeaderItem(context: self.context, timestamp: self.referenceTimestamp, theme: self.theme, strings: self.presentationData.strings, wallpaper: self.wallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder) + var items: [ListViewItem] = [] let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: 1) let otherPeerId = self.context.account.peerId @@ -289,24 +847,52 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) - let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3) - messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + var sampleMessages: [Message] = [] - let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message1, theme: self.theme, strings: self.presentationData.strings, wallpaper: self.theme.chat.defaultWallpaper, fontSize: self.presentationData.fontSize, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil)) + let message1 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_4_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + sampleMessages.append(message1) - let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message2, theme: self.theme, strings: self.presentationData.strings, wallpaper: self.theme.chat.defaultWallpaper, fontSize: self.presentationData.fontSize, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil)) + let message2 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_5_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + sampleMessages.append(message2) + + let message3 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66002, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_6_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + sampleMessages.append(message3) + + let message4 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66003, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_7_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + sampleMessages.append(message4) + + let message5 = Message(stableId: 5, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 5), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66004, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + messages[message5.id] = message5 + sampleMessages.append(message5) let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: MemoryBuffer(data: Data(base64Encoded: waveformBase64)!))] let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) - let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message3, theme: self.theme, strings: self.presentationData.strings, wallpaper: self.theme.chat.defaultWallpaper, fontSize: self.presentationData.fontSize, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local))) + let message6 = Message(stableId: 6, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 6), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66005, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: []) + sampleMessages.append(message6) - let message4 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message4, theme: self.theme, strings: self.presentationData.strings, wallpaper: self.theme.chat.defaultWallpaper, fontSize: self.presentationData.fontSize, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil)) + let message7 = Message(stableId: 7, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 7), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66006, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: message5.id)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + sampleMessages.append(message7) + + let message8 = Message(stableId: 8, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 8), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66007, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + sampleMessages.append(message8) + + items = sampleMessages.reversed().map { message in + let item = self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message, theme: self.theme, strings: self.presentationData.strings, wallpaper: self.wallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: !message.media.isEmpty ? FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local) : nil, tapMessage: { [weak self] message in + if message.flags.contains(.Incoming) { + self?.updateSection(.accent) + self?.requestSectionUpdate?(.accent) + } else { + self?.updateSection(.messages) + self?.requestSectionUpdate?(.messages) + } + }, clickThroughMessage: { [weak self] in + self?.updateSection(.background) + self?.requestSectionUpdate?(.background) + }) + return item + } let params = ListViewItemLayoutParams(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) if let messageNodes = self.messageNodes { @@ -320,7 +906,6 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate itemNode.contentSize = layout.contentSize itemNode.insets = layout.insets itemNode.frame = nodeFrame - itemNode.isUserInteractionEnabled = false apply(ListViewItemApply(isOnScreen: true)) }) @@ -333,57 +918,242 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate itemNode = node apply().1(ListViewItemApply(isOnScreen: true)) }) - itemNode!.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) - itemNode!.isUserInteractionEnabled = false + itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) messageNodes.append(itemNode!) - self.chatBackgroundNode.addSubnode(itemNode!) + self.messagesContainerNode.addSubnode(itemNode!) } self.messageNodes = messageNodes } - + + var bottomOffset: CGFloat = 9.0 + bottomInset if let messageNodes = self.messageNodes { - var bottomOffset: CGFloat = layout.size.height - bottomInset - 9.0 for itemNode in messageNodes { - transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset - itemNode.frame.height), size: itemNode.frame.size)) - bottomOffset -= itemNode.frame.height + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: itemNode.frame.size)) + bottomOffset += itemNode.frame.height itemNode.updateFrame(itemNode.frame, within: layout.size) } } + + let dateHeaderNode: ListViewItemHeaderNode + if let currentDateHeaderNode = self.dateHeaderNode { + dateHeaderNode = currentDateHeaderNode + headerItem.updateNode(dateHeaderNode, previous: nil, next: headerItem) + } else { + dateHeaderNode = headerItem.node() + dateHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + self.messagesContainerNode.addSubnode(dateHeaderNode) + self.dateHeaderNode = dateHeaderNode + } + + transition.updateFrame(node: dateHeaderNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: CGSize(width: layout.size.width, height: headerItem.height))) + dateHeaderNode.updateLayout(size: self.messagesContainerNode.frame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let isFirstLayout = self.validLayout == nil + let bounds = CGRect(origin: CGPoint(), size: layout.size) + + let chatListPreviewAvailable = self.state.section == .accent + self.scrollNode.frame = bounds + self.scrollNode.view.contentSize = CGSize(width: bounds.width * 2.0, height: bounds.height) + self.scrollNode.view.isScrollEnabled = chatListPreviewAvailable + + var messagesTransition = transition + if !chatListPreviewAvailable && self.scrollNode.view.contentOffset.x > 0.0 { + var bounds = self.scrollNode.bounds + bounds.origin.x = 0.0 + transition.updateBounds(node: scrollNode, bounds: bounds) + messagesTransition = .immediate + self.pageControlNode.setPage(0.0) + } let toolbarHeight = 49.0 + layout.intrinsicInsets.bottom - self.chatListBackgroundNode.frame = CGRect(x: bounds.width, y: 0.0, width: bounds.width, height: bounds.height) - self.chatBackgroundNode.frame = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height) - - self.scrollNode.view.contentSize = CGSize(width: bounds.width * 2.0, height: bounds.height) - transition.updateFrame(node: self.toolbarNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: 49.0 + layout.intrinsicInsets.bottom))) self.toolbarNode.updateLayout(size: CGSize(width: layout.size.width, height: 49.0), layout: layout, transition: transition) var bottomInset = toolbarHeight let standardInputHeight = layout.deviceMetrics.keyboardHeight(inLandscape: false) - let height = max(standardInputHeight, layout.inputHeight ?? 0.0) - bottomInset + 47.0 - let colorPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomInset - height), size: CGSize(width: layout.size.width, height: height)) - bottomInset += height + let inputFieldPanelHeight: CGFloat = 47.0 + let colorPanelHeight = max(standardInputHeight, layout.inputHeight ?? 0.0) - bottomInset + inputFieldPanelHeight + + var colorPanelOffset: CGFloat = 0.0 + if self.state.colorPanelCollapsed { + colorPanelOffset = colorPanelHeight - inputFieldPanelHeight + } + let colorPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomInset - colorPanelHeight + colorPanelOffset), size: CGSize(width: layout.size.width, height: colorPanelHeight)) + bottomInset += (colorPanelHeight - colorPanelOffset) + + if bottomInset + navigationBarHeight > bounds.height { + return + } transition.updateFrame(node: self.colorPanelNode, frame: colorPanelFrame) - self.colorPanelNode.updateLayout(size: colorPanelFrame.size, keyboardHeight: layout.inputHeight ?? 0.0, transition: transition) + self.colorPanelNode.updateLayout(size: colorPanelFrame.size, transition: transition) - let messagesBottomInset = bottomInset + 36.0 + var patternPanelAlpha: CGFloat = self.state.displayPatternPanel ? 1.0 : 0.0 + var patternPanelFrame = colorPanelFrame + transition.updateFrame(node: self.patternPanelNode, frame: patternPanelFrame) + self.patternPanelNode.updateLayout(size: patternPanelFrame.size, transition: transition) + self.patternPanelNode.isUserInteractionEnabled = self.state.displayPatternPanel + transition.updateAlpha(node: self.patternPanelNode, alpha: patternPanelAlpha) + + self.chatListBackgroundNode.frame = CGRect(x: bounds.width, y: 0.0, width: bounds.width, height: bounds.height) + + transition.updateFrame(node: self.messagesContainerNode, frame: CGRect(x: 0.0, y: navigationBarHeight, width: bounds.width, height: bounds.height - bottomInset - navigationBarHeight)) + + let backgroundSize = CGSize(width: bounds.width, height: bounds.height - (colorPanelHeight - colorPanelOffset)) + transition.updateFrame(node: self.backgroundContainerNode, frame: CGRect(origin: CGPoint(), size: backgroundSize)) + + let makeImageLayout = self.signalBackgroundNode.asyncLayout() + let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: self.patternWallpaper?.dimensions ?? layout.size, boundingSize: layout.size, intrinsicInsets: UIEdgeInsets(), custom: self.patternArguments)) + let _ = imageApply() + + transition.updatePosition(node: self.backgroundWrapperNode, position: CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0)) + + transition.updateBounds(node: self.backgroundWrapperNode, bounds: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.immediateBackgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.signalBackgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + let displayOptionButtons = self.state.section == .background + var messagesBottomInset: CGFloat = 0.0 + + if displayOptionButtons { + messagesBottomInset = 46.0 + } else if chatListPreviewAvailable { + messagesBottomInset = 37.0 + } self.updateChatsLayout(layout: layout, topInset: navigationBarHeight, transition: transition) - self.updateMessagesLayout(layout: layout, bottomInset: messagesBottomInset, transition: transition) + self.updateMessagesLayout(layout: layout, bottomInset: messagesBottomInset, transition: messagesTransition) self.validLayout = (layout, navigationBarHeight, messagesBottomInset) + let pageControlAlpha: CGFloat = chatListPreviewAvailable ? 1.0 : 0.0 let pageControlSize = self.pageControlNode.measure(CGSize(width: bounds.width, height: 100.0)) - let pageControlFrame = CGRect(origin: CGPoint(x: floor((bounds.width - pageControlSize.width) / 2.0), y: layout.size.height - bottomInset - 27.0), size: pageControlSize) - self.pageControlNode.frame = pageControlFrame - self.pageControlBackgroundNode.frame = CGRect(x: pageControlFrame.minX - 11.0, y: pageControlFrame.minY - 12.0, width: pageControlFrame.width + 22.0, height: 30.0) + let pageControlFrame = CGRect(origin: CGPoint(x: floor((bounds.width - pageControlSize.width) / 2.0), y: layout.size.height - bottomInset - 28.0), size: pageControlSize) + transition.updateFrame(node: self.pageControlNode, frame: pageControlFrame) + transition.updateFrame(node: self.pageControlBackgroundNode, frame: CGRect(x: pageControlFrame.minX - 7.0, y: pageControlFrame.minY - 7.0, width: pageControlFrame.width + 14.0, height: 21.0)) + transition.updateAlpha(node: self.pageControlNode, alpha: pageControlAlpha) + transition.updateAlpha(node: self.pageControlBackgroundNode, alpha: pageControlAlpha) transition.updateFrame(node: self.maskNode, frame: CGRect(x: 0.0, y: layout.size.height - bottomInset - 80.0, width: bounds.width, height: 80.0)) + + let patternButtonSize = self.patternButtonNode.measure(layout.size) + let motionButtonSize = self.motionButtonNode.measure(layout.size) + let maxButtonWidth = max(patternButtonSize.width, motionButtonSize.width) + let buttonSize = CGSize(width: maxButtonWidth, height: 30.0) + + let leftButtonFrame = CGRect(origin: CGPoint(x: floor(layout.size.width / 2.0 - buttonSize.width - 10.0), y: layout.size.height - bottomInset - 44.0), size: buttonSize) + let centerButtonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonSize.width) / 2.0), y: layout.size.height - bottomInset - 44.0), size: buttonSize) + let rightButtonFrame = CGRect(origin: CGPoint(x: ceil(layout.size.width / 2.0 + 10.0), y: layout.size.height - bottomInset - 44.0), size: buttonSize) + + var hasMotion: Bool = self.state.patternWallpaper != nil || self.state.displayPatternPanel + + var patternAlpha: CGFloat = displayOptionButtons ? 1.0 : 0.0 + var motionAlpha: CGFloat = displayOptionButtons && hasMotion ? 1.0 : 0.0 + + var patternFrame = hasMotion ? leftButtonFrame : centerButtonFrame + var motionFrame = hasMotion ? rightButtonFrame : centerButtonFrame + + transition.updateFrame(node: self.patternButtonNode, frame: patternFrame) + transition.updateAlpha(node: self.patternButtonNode, alpha: patternAlpha) + + transition.updateFrame(node: self.motionButtonNode, frame: motionFrame) + transition.updateAlpha(node: self.motionButtonNode, alpha: motionAlpha) + + if isFirstLayout { + self.setMotionEnabled(self.state.motion, animated: false) + } + } + + @objc private func toggleMotion() { + self.updateState({ current in + var updated = current + updated.motion = !updated.motion + return updated + }, animated: true) + } + + @objc private func togglePattern() { + self.view.endEditing(true) + + let wallpaper = self.state.previousPatternWallpaper ?? self.patternPanelNode.wallpapers.first + let backgroundColors = self.currentBackgroundColors + + var appeared = false + self.updateState({ current in + var updated = current + if updated.patternWallpaper != nil { + updated.previousPatternWallpaper = updated.patternWallpaper + updated.patternWallpaper = nil + updated.displayPatternPanel = false + } else { + updated.colorPanelCollapsed = false + updated.displayPatternPanel = true + if current.patternWallpaper == nil, let wallpaper = wallpaper { + updated.patternWallpaper = wallpaper + if updated.backgroundColors == nil { + if let backgroundColors = backgroundColors { + updated.backgroundColors = (backgroundColors.0, backgroundColors.1) + } else { + updated.backgroundColors = nil + } + } + appeared = true + } + } + return updated + }, animated: true) + + if appeared { + self.patternPanelNode.didAppear(initialWallpaper: wallpaper, intensity: self.state.patternIntensity) + } + } + + private let motionAmount: CGFloat = 32.0 + private func setMotionEnabled(_ enabled: Bool, animated: Bool) { + guard let (layout, _, _) = self.validLayout else { + return + } + + if enabled { + let horizontal = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis) + horizontal.minimumRelativeValue = motionAmount + horizontal.maximumRelativeValue = -motionAmount + + let vertical = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis) + vertical.minimumRelativeValue = motionAmount + vertical.maximumRelativeValue = -motionAmount + + let group = UIMotionEffectGroup() + group.motionEffects = [horizontal, vertical] + self.backgroundWrapperNode.view.addMotionEffect(group) + + let scale = (layout.size.width + motionAmount * 2.0) / layout.size.width + if animated { + self.backgroundWrapperNode.transform = CATransform3DMakeScale(scale, scale, 1.0) + self.backgroundWrapperNode.layer.animateScale(from: 1.0, to: scale, duration: 0.2) + } else { + self.backgroundWrapperNode.transform = CATransform3DMakeScale(scale, scale, 1.0) + } + } else { + let position = self.backgroundWrapperNode.layer.presentation()?.position + + for effect in self.backgroundWrapperNode.view.motionEffects { + self.backgroundWrapperNode.view.removeMotionEffect(effect) + } + + let scale = (layout.size.width + motionAmount * 2.0) / layout.size.width + if animated { + self.backgroundWrapperNode.transform = CATransform3DIdentity + self.backgroundWrapperNode.layer.animateScale(from: scale, to: 1.0, duration: 0.2) + if let position = position { + self.backgroundWrapperNode.layer.animatePosition(from: position, to: self.backgroundWrapperNode.layer.position, duration: 0.2) + } + } else { + self.backgroundWrapperNode.transform = CATransform3DIdentity + } + } } } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift index aa52f61314..13136e0a15 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift @@ -68,7 +68,7 @@ private enum ThemeAutoNightSettingsControllerEntry: ItemListNodeEntry { case settingInfo(PresentationTheme, String) case themeHeader(PresentationTheme, String) - case themeItem(PresentationTheme, PresentationStrings, [PresentationThemeReference], PresentationThemeReference, [Int64: PresentationThemeAccentColor]) + case themeItem(PresentationTheme, PresentationStrings, [PresentationThemeReference], [PresentationThemeReference], PresentationThemeReference, [Int64: PresentationThemeAccentColor], [Int64: TelegramWallpaper]) var section: ItemListSectionId { switch self { @@ -186,8 +186,8 @@ private enum ThemeAutoNightSettingsControllerEntry: ItemListNodeEntry { } else { return false } - case let .themeItem(lhsTheme, lhsStrings, lhsThemes, lhsCurrentTheme, lhsThemeAccentColors): - if case let .themeItem(rhsTheme, rhsStrings, rhsThemes, rhsCurrentTheme, rhsThemeAccentColors) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsThemes == rhsThemes, lhsCurrentTheme == rhsCurrentTheme, lhsThemeAccentColors == rhsThemeAccentColors { + case let .themeItem(lhsTheme, lhsStrings, lhsThemes, lhsAllThemes, lhsCurrentTheme, lhsThemeAccentColors, lhsThemeChatWallpapers): + if case let .themeItem(rhsTheme, rhsStrings, rhsThemes, rhsAllThemes, rhsCurrentTheme, rhsThemeAccentColors, rhsThemeChatWallpapers) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsThemes == rhsThemes, lhsAllThemes == rhsAllThemes, lhsCurrentTheme == rhsCurrentTheme, lhsThemeAccentColors == rhsThemeAccentColors, lhsThemeChatWallpapers == rhsThemeChatWallpapers { return true } else { return false @@ -199,41 +199,41 @@ private enum ThemeAutoNightSettingsControllerEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ThemeAutoNightSettingsControllerArguments switch self { case let .modeSystem(theme, title, value): - return ItemListCheckboxItem(theme: theme, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateMode(.system) }) case let .modeDisabled(theme, title, value): - return ItemListCheckboxItem(theme: theme, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateMode(.none) }) case let .modeTimeBased(theme, title, value): - return ItemListCheckboxItem(theme: theme, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateMode(.timeBased) }) case let .modeBrightness(theme, title, value): - return ItemListCheckboxItem(theme: theme, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateMode(.brightness) }) case let .settingsHeader(theme, title): - return ItemListSectionHeaderItem(theme: theme, text: title, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .timeBasedAutomaticLocation(theme, title, value): - return ItemListSwitchItem(theme: theme, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.updateTimeBasedAutomatic(value) }) case let .timeBasedAutomaticLocationValue(theme, title, value): - return ItemListDisclosureItem(theme: theme, icon: nil, title: title, titleColor: .accent, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { + return ItemListDisclosureItem(presentationData: presentationData, icon: nil, title: title, titleColor: .accent, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { arguments.updateTimeBasedAutomaticLocation() }) case let .timeBasedManualFrom(theme, title, value): - return ItemListDisclosureItem(theme: theme, icon: nil, title: title, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, icon: nil, title: title, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openTimeBasedManual(.from) }) case let .timeBasedManualTo(theme, title, value): - return ItemListDisclosureItem(theme: theme, icon: nil, title: title, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, icon: nil, title: title, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openTimeBasedManual(.to) }) case let .brightnessValue(theme, value): @@ -241,11 +241,11 @@ private enum ThemeAutoNightSettingsControllerEntry: ItemListNodeEntry { arguments.updateAutomaticBrightness(Double(value) / 100.0) }) case let .settingInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .themeHeader(theme, title): - return ItemListSectionHeaderItem(theme: theme, text: title, sectionId: self.section) - case let .themeItem(theme, strings, themes, currentTheme, themeSpecificAccentColors): - return ThemeSettingsThemeItem(context: arguments.context, theme: theme, strings: strings, sectionId: self.section, themes: themes, displayUnsupported: false, themeSpecificAccentColors: themeSpecificAccentColors, currentTheme: currentTheme, updatedTheme: { theme in + return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) + case let .themeItem(theme, strings, themes, allThemes, currentTheme, themeSpecificAccentColors, themeSpecificChatWallpapers): + return ThemeSettingsThemeItem(context: arguments.context, theme: theme, strings: strings, sectionId: self.section, themes: themes, allThemes: allThemes, displayUnsupported: false, themeSpecificAccentColors: themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, currentTheme: currentTheme, updatedTheme: { theme in arguments.updateTheme(theme) }, contextAction: nil) } @@ -313,7 +313,16 @@ private func themeAutoNightSettingsControllerEntries(theme: PresentationTheme, s break case .system, .timeBased, .brightness: entries.append(.themeHeader(theme, strings.AutoNightTheme_PreferredTheme)) - entries.append(.themeItem(theme, strings, availableThemes, switchSetting.theme, settings.themeSpecificAccentColors)) + + let generalThemes: [PresentationThemeReference] = availableThemes.filter { reference in + if case let .cloud(theme) = reference { + return theme.theme.settings == nil + } else { + return true + } + } + + entries.append(.themeItem(theme, strings, generalThemes, availableThemes, switchSetting.theme, settings.themeSpecificAccentColors, settings.themeSpecificChatWallpapers)) } return entries @@ -523,7 +532,7 @@ public func themeAutoNightSettingsController(context: AccountContext) -> ViewCon } })) }, updateTheme: { theme in - guard let presentationTheme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: theme, accentColor: nil, serviceBackgroundColor: .black, baseColor: nil) else { + guard let presentationTheme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: theme) else { return } @@ -541,7 +550,7 @@ public func themeAutoNightSettingsController(context: AccountContext) -> ViewCon |> mapToSignal { resolvedWallpaper -> Signal in var updatedTheme = theme if case let .cloud(info) = theme { - updatedTheme = .cloud(PresentationCloudTheme(theme: info.theme, resolvedWallpaper: resolvedWallpaper)) + updatedTheme = .cloud(PresentationCloudTheme(theme: info.theme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: info.theme.isCreator ? context.account.id : nil)) } updateSettings { settings in @@ -563,13 +572,13 @@ public func themeAutoNightSettingsController(context: AccountContext) -> ViewCon let settings = (sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings] as? PresentationThemeSettings) ?? PresentationThemeSettings.defaultSettings let defaultThemes: [PresentationThemeReference] = [.builtin(.night), .builtin(.nightAccent)] - let cloudThemes: [PresentationThemeReference] = cloudThemes.map { .cloud(PresentationCloudTheme(theme: $0, resolvedWallpaper: nil)) } + let cloudThemes: [PresentationThemeReference] = cloudThemes.map { .cloud(PresentationCloudTheme(theme: $0, resolvedWallpaper: nil, creatorAccountId: $0.isCreator ? context.account.id : nil)) } var availableThemes = defaultThemes availableThemes.append(contentsOf: cloudThemes) - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.AutoNightTheme_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: themeAutoNightSettingsControllerEntries(theme: presentationData.theme, strings: presentationData.strings, settings: settings, switchSetting: stagingSettings ?? settings.automaticThemeSwitchSetting, availableThemes: availableThemes, dateTimeFormat: presentationData.dateTimeFormat), style: .blocks, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.AutoNightTheme_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeAutoNightSettingsControllerEntries(theme: presentationData.theme, strings: presentationData.strings, settings: settings, switchSetting: stagingSettings ?? settings.automaticThemeSwitchSetting, availableThemes: availableThemes, dateTimeFormat: presentationData.dateTimeFormat), style: .blocks, animateChanges: false) return (controllerState, (listState, arguments)) } @@ -578,6 +587,7 @@ public func themeAutoNightSettingsController(context: AccountContext) -> ViewCon } let controller = ItemListController(context: context, state: signal) + controller.alwaysSynchronous = true presentControllerImpl = { [weak controller] c in controller?.present(c, in: .window(.root)) } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightTimeSelectionActionSheet.swift b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightTimeSelectionActionSheet.swift index 0b98f3a872..7bdbbc2c7f 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightTimeSelectionActionSheet.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightTimeSelectionActionSheet.swift @@ -22,11 +22,11 @@ final class ThemeAutoNightTimeSelectionActionSheet: ActionSheetController { let theme = presentationData.theme let strings = presentationData.strings - super.init(theme: ActionSheetControllerTheme(presentationTheme: theme)) + super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationTheme: presentationData.theme) + strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) } }) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeColorPresets.swift b/submodules/SettingsUI/Sources/Themes/ThemeColorPresets.swift new file mode 100644 index 0000000000..5b74af426a --- /dev/null +++ b/submodules/SettingsUI/Sources/Themes/ThemeColorPresets.swift @@ -0,0 +1,32 @@ +import Foundation +import Postbox +import SyncCore +import TelegramUIPreferences + +private func patternWallpaper(slug: String, topColor: UInt32, bottomColor: UInt32?, intensity: Int32?, rotation: Int32?) -> TelegramWallpaper { + return TelegramWallpaper.file(id: 0, accessHash: 0, isCreator: false, isDefault: true, isPattern: true, isDark: false, slug: slug, file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], immediateThumbnailData: nil, mimeType: "", size: nil, attributes: []), settings: WallpaperSettings(color: topColor, bottomColor: bottomColor, intensity: intensity ?? 50, rotation: rotation)) +} + +var dayClassicColorPresets: [PresentationThemeAccentColor] = [ + PresentationThemeAccentColor(index: 106, baseColor: .preset, accentColor: 0xfff55783, bubbleColors: (0xffd6f5ff, 0xffc9fdfe), wallpaper: patternWallpaper(slug: "p-pXcflrmFIBAAAAvXYQk-mCwZU", topColor: 0xfffce3ec, bottomColor: 0xfffec8ff, intensity: 50, rotation: 45)), + PresentationThemeAccentColor(index: 102, baseColor: .preset, accentColor: 0xffff5fa9, bubbleColors: (0xfffff4d7, nil), wallpaper: patternWallpaper(slug: "51nnTjx8mFIBAAAAaFGJsMIvWkk", topColor: 0xfff6b594, bottomColor: 0xffebf6cd, intensity: 46, rotation: 45)), + PresentationThemeAccentColor(index: 104, baseColor: .preset, accentColor: 0xff5a9e29, bubbleColors: (0xfffff8df, 0xffdcf8c6), wallpaper: patternWallpaper(slug: "R3j69wKskFIBAAAAoUdXWCKMzCM", topColor: 0xffede6dd, bottomColor: 0xffffd59e, intensity: 50, rotation: nil)), + PresentationThemeAccentColor(index: 101, baseColor: .preset, accentColor: 0xff7e5fe5, bubbleColors: (0xfff5e2ff, nil), wallpaper: patternWallpaper(slug: "nQcFYJe1mFIBAAAAcI95wtIK0fk", topColor: 0xfffcccf4, bottomColor: 0xffae85f0, intensity: 54, rotation: nil)), + PresentationThemeAccentColor(index: 107, baseColor: .preset, accentColor: 0xff2cb9ed, bubbleColors: (0xffadf7b5, 0xfffcff8b), wallpaper: patternWallpaper(slug: "CJNyxPMgSVAEAAAAvW9sMwc51cw", topColor: 0xff1a2d1a, bottomColor: 0xff5f6f54, intensity: 50, rotation: 225)), + PresentationThemeAccentColor(index: 103, baseColor: .preset, accentColor: 0xff199972, bubbleColors: (0xfffffec7, nil), wallpaper: patternWallpaper(slug: "fqv01SQemVIBAAAApND8LDRUhRU", topColor: 0xffc1e7cb, bottomColor: nil, intensity: 50, rotation: nil)), + PresentationThemeAccentColor(index: 105, baseColor: .preset, accentColor: 0x0ff09eee, bubbleColors: (0xff94fff9, 0xffccffc7), wallpaper: patternWallpaper(slug: "p-pXcflrmFIBAAAAvXYQk-mCwZU", topColor: 0xffffbca6, bottomColor: 0xffff63bd, intensity: 57, rotation: 225)) +] + +var dayColorPresets: [PresentationThemeAccentColor] = [ + PresentationThemeAccentColor(index: 101, baseColor: .preset, accentColor: 0x007aff, bubbleColors: (0x007aff, 0xff53f4), wallpaper: nil), + PresentationThemeAccentColor(index: 102, baseColor: .preset, accentColor: 0x00b09b, bubbleColors: (0xaee946, 0x00b09b), wallpaper: nil), + PresentationThemeAccentColor(index: 103, baseColor: .preset, accentColor: 0xd33213, bubbleColors: (0xf9db00, 0xd33213), wallpaper: nil), + PresentationThemeAccentColor(index: 104, baseColor: .preset, accentColor: 0xea8ced, bubbleColors: (0xea8ced, 0x00c2ed), wallpaper: nil) +] + +var nightColorPresets: [PresentationThemeAccentColor] = [ + PresentationThemeAccentColor(index: 101, baseColor: .preset, accentColor: 0x007aff, bubbleColors: (0x007aff, 0xff53f4), wallpaper: nil), + PresentationThemeAccentColor(index: 102, baseColor: .preset, accentColor: 0x00b09b, bubbleColors: (0xaee946, 0x00b09b), wallpaper: nil), + PresentationThemeAccentColor(index: 103, baseColor: .preset, accentColor: 0xd33213, bubbleColors: (0xf9db00, 0xd33213), wallpaper: nil), + PresentationThemeAccentColor(index: 104, baseColor: .preset, accentColor: 0xea8ced, bubbleColors: (0xea8ced, 0x00c2ed), wallpaper: nil) +] diff --git a/submodules/SettingsUI/Sources/Themes/ThemeColorSegmentedTitleView.swift b/submodules/SettingsUI/Sources/Themes/ThemeColorSegmentedTitleView.swift new file mode 100644 index 0000000000..37cdcf7299 --- /dev/null +++ b/submodules/SettingsUI/Sources/Themes/ThemeColorSegmentedTitleView.swift @@ -0,0 +1,67 @@ +import Foundation +import UIKit +import SegmentedControlNode +import TelegramPresentationData + +final class ThemeColorSegmentedTitleView: UIView { + private let segmentedControlNode: SegmentedControlNode + + var theme: PresentationTheme { + didSet { + self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.theme)) + } + } + + var index: Int { + get { + return self.segmentedControlNode.selectedIndex + } + set { + self.segmentedControlNode.selectedIndex = newValue + } + } + + func setIndex(_ index: Int, animated: Bool) { + self.segmentedControlNode.setSelectedIndex(index, animated: animated) + } + + var sectionUpdated: ((ThemeColorSection) -> Void)? + var shouldUpdateSection: ((ThemeColorSection, @escaping (Bool) -> Void) -> Void)? + + init(theme: PresentationTheme, strings: PresentationStrings, selectedSection: ThemeColorSection) { + self.theme = theme + + let sections = [strings.Theme_Colors_Accent, strings.Theme_Colors_Background, strings.Theme_Colors_Messages] + self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: theme), items: sections.map { SegmentedControlItem(title: $0) }, selectedIndex: selectedSection.rawValue) + + super.init(frame: CGRect()) + + self.segmentedControlNode.selectedIndexChanged = { [weak self] index in + if let section = ThemeColorSection(rawValue: index) { + self?.sectionUpdated?(section) + } + } + + self.segmentedControlNode.selectedIndexShouldChange = { [weak self] index, f in + if let section = ThemeColorSection(rawValue: index) { + self?.shouldUpdateSection?(section, f) + } else { + f(false) + } + } + + self.addSubnode(self.segmentedControlNode) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + let size = self.bounds.size + let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: size.width + 20.0), transition: .immediate) + self.segmentedControlNode.frame = CGRect(origin: CGPoint(x: floor((size.width - controlSize.width) / 2.0), y: floor((size.height - controlSize.height) / 2.0)), size: controlSize) + } +} diff --git a/submodules/SettingsUI/Sources/Themes/ThemeColorsGridController.swift b/submodules/SettingsUI/Sources/Themes/ThemeColorsGridController.swift index 3115fdd986..99d25dbc70 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeColorsGridController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeColorsGridController.swift @@ -8,9 +8,10 @@ import SyncCore import SwiftSignalKit import LegacyComponents import TelegramPresentationData +import TelegramUIPreferences import AccountContext -private func availableColors() -> [Int32] { +private func availableColors() -> [UInt32] { return [ 0xffffff, 0xd4dfea, @@ -48,7 +49,7 @@ private func availableColors() -> [Int32] { ] } -private func randomColor() -> Int32 { +private func randomColor() -> UInt32 { let colors = availableColors() return colors[1 ..< colors.count - 1].randomElement() ?? 0x000000 } @@ -127,13 +128,44 @@ final class ThemeColorsGridController: ViewController { } }, presentColorPicker: { [weak self] in if let strongSelf = self { - let controller = WallpaperGalleryController(context: strongSelf.context, source: .customColor(randomColor())) - controller.apply = { _, _, _ in - if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController { - let _ = navigationController.popViewController(animated: true) + let _ = (strongSelf.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings]) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] sharedData in + guard let strongSelf = self else { + return } - } - self?.present(controller, in: .window(.root), blockInteraction: true) + let settings = (sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings] as? PresentationThemeSettings) ?? PresentationThemeSettings.defaultSettings + + let autoNightModeTriggered = strongSelf.presentationData.autoNightModeTriggered + let themeReference: PresentationThemeReference + if autoNightModeTriggered { + themeReference = settings.automaticThemeSwitchSetting.theme + } else { + themeReference = settings.theme + } + + let controller = ThemeAccentColorController(context: strongSelf.context, mode: .background(themeReference: themeReference)) + controller.completion = { [weak self] in + if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController { + var controllers = navigationController.viewControllers + controllers = controllers.filter { controller in + if controller is ThemeColorsGridController { + return false + } + return true + } + navigationController.setViewControllers(controllers, animated: false) + controllers = controllers.filter { controller in + if controller is ThemeAccentColorController { + return false + } + return true + } + navigationController.setViewControllers(controllers, animated: true) + } + } + strongSelf.push(controller) + }) } }) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeColorsGridControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeColorsGridControllerNode.swift index a4274e606c..e1b471550c 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeColorsGridControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeColorsGridControllerNode.swift @@ -88,7 +88,7 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { private var disposable: Disposable? - init(context: AccountContext, presentationData: PresentationData, colors: [Int32], present: @escaping (ViewController, Any?) -> Void, pop: @escaping () -> Void, presentColorPicker: @escaping () -> Void) { + init(context: AccountContext, presentationData: PresentationData, colors: [UInt32], present: @escaping (ViewController, Any?) -> Void, pop: @escaping () -> Void, presentColorPicker: @escaping () -> Void) { self.context = context self.presentationData = presentationData self.present = present @@ -108,7 +108,7 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor self.customColorItemNode = ItemListActionItemNode() - self.customColorItem = ItemListActionItem(theme: presentationData.theme, title: presentationData.strings.WallpaperColors_SetCustomColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { + self.customColorItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.WallpaperColors_SetCustomColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { presentColorPicker() }) @@ -221,7 +221,7 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { self.leftOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor self.rightOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor - self.customColorItem = ItemListActionItem(theme: presentationData.theme, title: presentationData.strings.WallpaperColors_SetCustomColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in + self.customColorItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.WallpaperColors_SetCustomColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in self?.presentColorPicker() }) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridController.swift b/submodules/SettingsUI/Sources/Themes/ThemeGridController.swift index cc65fc5383..9444ef01e6 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeGridController.swift @@ -167,7 +167,7 @@ final class ThemeGridController: ViewController { } }, deleteWallpapers: { [weak self] wallpapers, completed in if let strongSelf = self { - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Wallpaper_DeleteConfirmation(Int32(wallpapers.count)), color: .destructive, action: { [weak self, weak actionSheet] in @@ -181,13 +181,16 @@ final class ThemeGridController: ViewController { if wallpaper == strongSelf.presentationData.chatWallpaper { let presentationData = strongSelf.presentationData let _ = (updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in - var fallbackWallpaper = presentationData.theme.chat.defaultWallpaper - if case let .cloud(info) = current.theme, let resolvedWallpaper = info.resolvedWallpaper { - fallbackWallpaper = resolvedWallpaper - } var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers - themeSpecificChatWallpapers[current.theme.index] = nil - return PresentationThemeSettings(chatWallpaper: fallbackWallpaper, theme: current.theme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) + let themeReference: PresentationThemeReference + if presentationData.autoNightModeTriggered { + themeReference = current.automaticThemeSwitchSetting.theme + } else { + themeReference = current.theme + } + themeSpecificChatWallpapers[themeReference.index] = nil + themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: current.themeSpecificAccentColors[themeReference.index])] = nil + return current.withUpdatedThemeSpecificChatWallpapers(themeSpecificChatWallpapers) })).start() break } @@ -211,7 +214,7 @@ final class ThemeGridController: ViewController { actionSheet.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) @@ -224,7 +227,7 @@ final class ThemeGridController: ViewController { } }, resetWallpapers: { [weak self] in if let strongSelf = self { - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) let items: [ActionSheetItem] = [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Wallpaper_ResetWallpapersConfirmation, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() @@ -237,22 +240,8 @@ final class ThemeGridController: ViewController { let _ = resetWallpapers(account: strongSelf.context.account).start(completed: { [weak self, weak controller] in let presentationData = strongSelf.presentationData - let _ = (strongSelf.context.sharedContext.accountManager.transaction { transaction -> Void in - transaction.updateSharedData(ApplicationSpecificSharedDataKeys.presentationThemeSettings, { entry in - let current: PresentationThemeSettings - if let entry = entry as? PresentationThemeSettings { - current = entry - } else { - current = PresentationThemeSettings.defaultSettings - } - var fallbackWallpaper = presentationData.theme.chat.defaultWallpaper - if case let .cloud(info) = current.theme, let resolvedWallpaper = info.resolvedWallpaper { - fallbackWallpaper = resolvedWallpaper - } - var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers - themeSpecificChatWallpapers[current.theme.index] = nil - return PresentationThemeSettings(chatWallpaper: fallbackWallpaper, theme: current.theme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: [:], fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) - }) + let _ = updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in + return current.withUpdatedThemeSpecificChatWallpapers([:]) }).start() let _ = (telegramWallpapers(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network) @@ -268,7 +257,7 @@ final class ThemeGridController: ViewController { ] actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) @@ -311,7 +300,7 @@ final class ThemeGridController: ViewController { var options: [String] = [] if isPattern { if let color = settings.color { - options.append("bg_color=\(UIColor(rgb: UInt32(bitPattern: color)).hexString)") + options.append("bg_color=\(UIColor(rgb: color).hexString)") } if let intensity = settings.intensity { options.append("intensity=\(intensity)") @@ -324,7 +313,7 @@ final class ThemeGridController: ViewController { } item = slug + optionsString case let .color(color): - item = "\(UIColor(rgb: UInt32(bitPattern: color)).hexString)" + item = "\(UIColor(rgb: color).hexString)" default: break } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeGridControllerNode.swift index 2cc1a23405..75476d20eb 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeGridControllerNode.swift @@ -15,35 +15,7 @@ import PresentationDataUtils import AccountContext import SearchBarNode import SearchUI - -private func areWallpapersEqual(_ lhs: TelegramWallpaper, _ rhs: TelegramWallpaper) -> Bool { - switch lhs { - case .builtin: - if case .builtin = rhs { - return true - } else { - return false - } - case let .color(color): - if case .color(color) = rhs { - return true - } else { - return false - } - case let .image(representations, _): - if case .image(representations, _) = rhs { - return true - } else { - return false - } - case let .file(_, _, _, _, _, _, lhsSlug, _, lhsSettings): - if case let .file(_, _, _, _, _, _, rhsSlug, _, rhsSettings) = rhs, lhsSlug == rhsSlug, lhsSettings.color == rhsSettings.color && lhsSettings.intensity == rhsSettings.intensity { - return true - } else { - return false - } - } -} +import WallpaperResources struct ThemeGridControllerNodeState: Equatable { let editing: Bool @@ -102,15 +74,19 @@ private struct ThemeGridControllerEntry: Comparable, Identifiable { case .builtin: return 0 case let .color(color): - return (Int64(1) << 32) | Int64(bitPattern: UInt64(UInt32(bitPattern: color))) + return (Int64(1) << 32) | Int64(bitPattern: UInt64(color)) + case let .gradient(topColor, bottomColor, _): + var hash: UInt32 = topColor + hash = hash &* 31 &+ bottomColor + return (Int64(2) << 32) | Int64(hash) case let .file(id, _, _, _, _, _, _, _, settings): var hash: Int = id.hashValue hash = hash &* 31 &+ (settings.color?.hashValue ?? 0) hash = hash &* 31 &+ (settings.intensity?.hashValue ?? 0) - return (Int64(2) << 32) | Int64(hash) + return (Int64(3) << 32) | Int64(hash) case let .image(representations, _): if let largest = largestImageRepresentation(representations) { - return (Int64(3) << 32) | Int64(largest.resource.id.hashValue) + return (Int64(4) << 32) | Int64(largest.resource.id.hashValue) } else { return 0 } @@ -266,21 +242,21 @@ final class ThemeGridControllerNode: ASDisplayNode { self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor self.colorItemNode = ItemListActionItemNode() - self.colorItem = ItemListActionItem(theme: presentationData.theme, title: presentationData.strings.Wallpaper_SetColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { + self.colorItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_SetColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { presentColors() }) self.galleryItemNode = ItemListActionItemNode() - self.galleryItem = ItemListActionItem(theme: presentationData.theme, title: presentationData.strings.Wallpaper_SetCustomBackground, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { + self.galleryItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_SetCustomBackground, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { presentGallery() }) self.descriptionItemNode = ItemListTextItemNode() - self.descriptionItem = ItemListTextItem(theme: presentationData.theme, text: .plain(presentationData.strings.Wallpaper_SetCustomBackgroundInfo), sectionId: 0) + self.descriptionItem = ItemListTextItem(presentationData: ItemListPresentationData(presentationData), text: .plain(presentationData.strings.Wallpaper_SetCustomBackgroundInfo), sectionId: 0) self.resetItemNode = ItemListActionItemNode() - self.resetItem = ItemListActionItem(theme: presentationData.theme, title: presentationData.strings.Wallpaper_ResetWallpapers, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { + self.resetItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_ResetWallpapers, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { resetWallpapers() }) self.resetDescriptionItemNode = ItemListTextItemNode() - self.resetDescriptionItem = ItemListTextItem(theme: presentationData.theme, text: .plain(presentationData.strings.Wallpaper_ResetWallpapersInfo), sectionId: 0) + self.resetDescriptionItem = ItemListTextItem(presentationData: ItemListPresentationData(presentationData), text: .plain(presentationData.strings.Wallpaper_ResetWallpapersInfo), sectionId: 0) self.currentState = ThemeGridControllerNodeState(editing: false, selectedIndices: Set()) self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) @@ -376,13 +352,13 @@ final class ThemeGridControllerNode: ASDisplayNode { var isSelectedEditable = true if case .builtin = presentationData.chatWallpaper { isSelectedEditable = false - } else if areWallpapersEqual(presentationData.chatWallpaper, presentationData.theme.chat.defaultWallpaper) { + } else if presentationData.chatWallpaper.isBasicallyEqual(to: presentationData.theme.chat.defaultWallpaper) { isSelectedEditable = false } entries.insert(ThemeGridControllerEntry(index: 0, wallpaper: presentationData.chatWallpaper, isEditable: isSelectedEditable, isSelected: true), at: 0) var defaultWallpaper: TelegramWallpaper? - if !areWallpapersEqual(presentationData.chatWallpaper, presentationData.theme.chat.defaultWallpaper) { + if !presentationData.chatWallpaper.isBasicallyEqual(to: presentationData.theme.chat.defaultWallpaper) { if case .builtin = presentationData.theme.chat.defaultWallpaper { } else { defaultWallpaper = presentationData.theme.chat.defaultWallpaper @@ -407,12 +383,12 @@ final class ThemeGridControllerNode: ASDisplayNode { } for wallpaper in sortedWallpapers { - if case let .file(file) = wallpaper, deletedWallpaperSlugs.contains(file.slug) || (file.isPattern && file.settings.color == nil) { + if case let .file(file) = wallpaper, deletedWallpaperSlugs.contains(file.slug) || (wallpaper.isPattern && file.settings.color == nil) { continue } - let selected = areWallpapersEqual(presentationData.chatWallpaper, wallpaper) + let selected = presentationData.chatWallpaper.isBasicallyEqual(to: wallpaper) var isDefault = false - if let defaultWallpaper = defaultWallpaper, areWallpapersEqual(defaultWallpaper, wallpaper) { + if let defaultWallpaper = defaultWallpaper, defaultWallpaper.isBasicallyEqual(to: wallpaper) { isDefault = true } var isEditable = true @@ -531,17 +507,17 @@ final class ThemeGridControllerNode: ASDisplayNode { self.bottomBackgroundNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor - self.colorItem = ItemListActionItem(theme: presentationData.theme, title: presentationData.strings.Wallpaper_SetColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in + self.colorItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_SetColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in self?.presentColors() }) - self.galleryItem = ItemListActionItem(theme: presentationData.theme, title: presentationData.strings.Wallpaper_SetCustomBackground, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in + self.galleryItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_SetCustomBackground, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in self?.presentGallery() }) - self.descriptionItem = ItemListTextItem(theme: presentationData.theme, text: .plain(presentationData.strings.Wallpaper_SetCustomBackgroundInfo), sectionId: 0) - self.resetItem = ItemListActionItem(theme: presentationData.theme, title: presentationData.strings.Wallpaper_ResetWallpapers, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in + self.descriptionItem = ItemListTextItem(presentationData: ItemListPresentationData(presentationData), text: .plain(presentationData.strings.Wallpaper_SetCustomBackgroundInfo), sectionId: 0) + self.resetItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_ResetWallpapers, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in self?.resetWallpapers() }) - self.resetDescriptionItem = ItemListTextItem(theme: presentationData.theme, text: .plain(presentationData.strings.Wallpaper_ResetWallpapersInfo), sectionId: 0) + self.resetDescriptionItem = ItemListTextItem(presentationData: ItemListPresentationData(presentationData), text: .plain(presentationData.strings.Wallpaper_ResetWallpapersInfo), sectionId: 0) if let (layout, navigationBarHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift index fc3229a8b6..ff179cd9bf 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift @@ -762,30 +762,10 @@ final class ThemeGridSearchContentNode: SearchDisplayControllerContentNode { let topInset = navigationBarHeight transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) self.recentListNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.gridNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight + spacing, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), preloadSize: 300.0, type: .fixed(itemSize: imageSize, fillWidth: nil, lineSpacing: spacing, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchItem.swift index 67660f29d0..d76b1a36b7 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchItem.swift @@ -115,7 +115,7 @@ final class ThemeGridSearchItemNode: GridItemNode { representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource)) } if !representations.isEmpty { - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) updateImageSignal = mediaGridMessagePhoto(account: item.account, photoReference: .standalone(media: tmpImage), fullRepresentationSize: CGSize(width: 512, height: 512)) } else { updateImageSignal = .complete() diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift index cd724cf3f8..6ea9fd97cd 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift @@ -15,11 +15,14 @@ import WallpaperResources import OverlayStatusController import AppBundle import PresentationDataUtils +import UndoUI +import TelegramNotices public enum ThemePreviewSource { - case settings(PresentationThemeReference) + case settings(PresentationThemeReference, TelegramWallpaper?) case theme(TelegramTheme) case slug(String, TelegramMediaFile) + case themeSettings(String, TelegramThemeSettings) case media(AnyMediaReference) } @@ -34,6 +37,13 @@ public final class ThemePreviewController: ViewController { return self.displayNode as! ThemePreviewControllerNode } + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + + private var validLayout: ContainerViewLayout? + private var didPlayPresentationAnimation = false private var presentationData: PresentationData @@ -55,38 +65,63 @@ public final class ThemePreviewController: ViewController { self.blocksBackgroundWhenInOverlay = true self.navigationPresentation = .modal + var hasInstallsCount = false let themeName: String - if case let .theme(theme) = source { - themeName = theme.title - self.theme.set(.single(theme)) - } else if case let .slug(slug, _) = source { - self.theme.set(getTheme(account: context.account, slug: slug) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - }) - themeName = previewTheme.name.string - - self.presentationTheme.set(.single(self.previewTheme) - |> then( - self.theme.get() - |> mapToSignal { theme in - if let file = theme?.file { - return telegramThemeData(account: context.account, accountManager: context.sharedContext.accountManager, resource: file.resource) - |> mapToSignal { data -> Signal in - guard let data = data, let presentationTheme = makePresentationTheme(data: data) else { - return .complete() - } - return .single(presentationTheme) - } - } else { - return .complete() + switch source { + case let .theme(theme): + themeName = theme.title + self.theme.set(.single(theme) + |> then( + getTheme(account: context.account, slug: theme.slug) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) } + |> filter { $0 != nil } + )) + hasInstallsCount = true + case let .slug(slug, _), let .themeSettings(slug, _): + self.theme.set(getTheme(account: context.account, slug: slug) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + }) + themeName = previewTheme.name.string + + self.presentationTheme.set(.single(self.previewTheme) + |> then( + self.theme.get() + |> mapToSignal { theme in + if let file = theme?.file { + return telegramThemeData(account: context.account, accountManager: context.sharedContext.accountManager, reference: .standalone(resource: file.resource)) + |> mapToSignal { data -> Signal in + guard let data = data, let presentationTheme = makePresentationTheme(data: data) else { + return .complete() + } + return .single(presentationTheme) + } + } else { + return .complete() + } + } + )) + hasInstallsCount = true + case let .settings(themeReference, _): + if case let .cloud(theme) = themeReference { + self.theme.set(getTheme(account: context.account, slug: theme.theme.slug) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + }) + themeName = theme.theme.title + hasInstallsCount = true + } else { + self.theme.set(.single(nil)) + themeName = previewTheme.name.string } - )) - } else { - self.theme.set(.single(nil)) - themeName = previewTheme.name.string + default: + self.theme.set(.single(nil)) + themeName = previewTheme.name.string } var isPreview = false @@ -95,8 +130,9 @@ public final class ThemePreviewController: ViewController { } let titleView = CounterContollerTitleView(theme: self.previewTheme) - titleView.title = CounterContollerTitle(title: themeName, counter: isPreview ? "" : " ") + titleView.title = CounterContollerTitle(title: themeName, counter: hasInstallsCount ? " " : "") self.navigationItem.titleView = titleView + self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) self.statusBar.statusBarStyle = self.previewTheme.rootController.statusBarStyle.style self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) @@ -151,7 +187,13 @@ public final class ThemePreviewController: ViewController { if case .settings = self.source { isPreview = true } - self.displayNode = ThemePreviewControllerNode(context: self.context, previewTheme: self.previewTheme, dismiss: { [weak self] in + + var initialWallpaper: TelegramWallpaper? + if case let .settings(_, currentWallpaper) = self.source, let wallpaper = currentWallpaper { + initialWallpaper = wallpaper + } + + self.displayNode = ThemePreviewControllerNode(context: self.context, previewTheme: self.previewTheme, initialWallpaper: initialWallpaper, dismiss: { [weak self] in if let strongSelf = self { strongSelf.dismiss() } @@ -159,14 +201,16 @@ public final class ThemePreviewController: ViewController { if let strongSelf = self { strongSelf.apply() } - }, isPreview: isPreview) + }, isPreview: isPreview, ready: self._ready) self.displayNodeDidLoad() let previewTheme = self.previewTheme - if case let .file(file) = previewTheme.chat.defaultWallpaper, file.id == 0 { + if let initialWallpaper = initialWallpaper { + self.controllerNode.wallpaperPromise.set(.single(initialWallpaper)) + } else if case let .file(file) = previewTheme.chat.defaultWallpaper, file.id == 0 { self.controllerNode.wallpaperPromise.set(cachedWallpaper(account: self.context.account, slug: file.slug, settings: file.settings) - |> mapToSignal { wallpaper in - return .single(wallpaper?.wallpaper ?? .color(Int32(bitPattern: previewTheme.chatList.backgroundColor.rgb))) + |> mapToSignal { wallpaper in + return .single(wallpaper?.wallpaper ?? .color(previewTheme.chatList.backgroundColor.argb)) }) } else { self.controllerNode.wallpaperPromise.set(.single(previewTheme.chat.defaultWallpaper)) @@ -183,16 +227,16 @@ public final class ThemePreviewController: ViewController { let disposable = self.applyDisposable switch self.source { - case let .settings(reference): + case let .settings(reference, _): theme = .single(reference) - case .theme, .slug: + case .theme, .slug, .themeSettings: theme = combineLatest(self.theme.get() |> take(1), wallpaperPromise.get() |> take(1)) |> mapToSignal { theme, wallpaper -> Signal in if let theme = theme { if case let .file(file) = wallpaper, file.id != 0 { - return .single(.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: wallpaper))) + return .single(.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: wallpaper, creatorAccountId: theme.isCreator ? context.account.id : nil))) } else { - return .single(.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil))) + return .single(.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: theme.isCreator ? context.account.id : nil))) } } else { return .complete() @@ -211,19 +255,30 @@ public final class ThemePreviewController: ViewController { var resolvedWallpaper: TelegramWallpaper? - let signal = theme - |> mapToSignal { theme -> Signal in + let setup = theme + |> mapToSignal { theme -> Signal<(PresentationThemeReference, Bool), NoError> in guard let theme = theme else { return .complete() } switch theme { case let .cloud(info): resolvedWallpaper = info.resolvedWallpaper - return .single(theme) + return telegramThemes(postbox: context.account.postbox, network: context.account.network, accountManager: context.sharedContext.accountManager) + |> take(1) + |> map { themes -> Bool in + if let _ = themes.first(where: { $0.id == info.theme.id }) { + return true + } else { + return false + } + } + |> map { exists in + return (theme, exists) + } case let .local(info): return wallpaperPromise.get() |> take(1) - |> mapToSignal { currentWallpaper -> Signal in + |> mapToSignal { currentWallpaper -> Signal<(PresentationThemeReference, Bool), NoError> in if case let .file(file) = currentWallpaper, file.id != 0 { resolvedWallpaper = currentWallpaper } @@ -243,41 +298,41 @@ public final class ThemePreviewController: ViewController { return telegramThemes(postbox: context.account.postbox, network: context.account.network, accountManager: context.sharedContext.accountManager) |> take(1) - |> mapToSignal { themes -> Signal in + |> mapToSignal { themes -> Signal<(PresentationThemeReference, Bool), NoError> in let similarTheme = themes.first(where: { $0.isCreator && $0.title == info.title }) if let similarTheme = similarTheme { - return updateTheme(account: context.account, accountManager: context.sharedContext.accountManager, theme: similarTheme, title: nil, slug: nil, resource: info.resource, thumbnailData: themeThumbnailData) + return updateTheme(account: context.account, accountManager: context.sharedContext.accountManager, theme: similarTheme, title: nil, slug: nil, resource: info.resource, thumbnailData: themeThumbnailData, settings: nil) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal<(PresentationThemeReference, Bool), NoError> in guard let result = result else { let updatedTheme = PresentationLocalTheme(title: info.title, resource: info.resource, resolvedWallpaper: resolvedWallpaper) - return .single(.local(updatedTheme)) + return .single((.local(updatedTheme), true)) } if case let .result(theme) = result, let file = theme.file { context.sharedContext.accountManager.mediaBox.moveResourceData(from: info.resource.id, to: file.resource.id) - return .single(.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: resolvedWallpaper))) + return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: theme.isCreator ? context.account.id : nil)), true)) } else { return .complete() } } } else { - return createTheme(account: context.account, title: info.title, resource: info.resource, thumbnailData: themeThumbnailData) + return createTheme(account: context.account, title: info.title, resource: info.resource, thumbnailData: themeThumbnailData, settings: nil) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal<(PresentationThemeReference, Bool), NoError> in guard let result = result else { let updatedTheme = PresentationLocalTheme(title: info.title, resource: info.resource, resolvedWallpaper: resolvedWallpaper) - return .single(.local(updatedTheme)) + return .single((.local(updatedTheme), true)) } if case let .result(updatedTheme) = result, let file = updatedTheme.file { context.sharedContext.accountManager.mediaBox.moveResourceData(from: info.resource.id, to: file.resource.id) - return .single(.cloud(PresentationCloudTheme(theme: updatedTheme, resolvedWallpaper: resolvedWallpaper))) + return .single((.cloud(PresentationCloudTheme(theme: updatedTheme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: updatedTheme.isCreator ? context.account.id : nil)), true)) } else { return .complete() } @@ -286,27 +341,61 @@ public final class ThemePreviewController: ViewController { } } case .builtin: - return .single(theme) + return .single((theme, true)) } } - |> mapToSignal { theme -> Signal in - if case let .cloud(info) = theme { + |> mapToSignal { updatedTheme, existing -> Signal<(PresentationThemeReference, PresentationThemeAccentColor?, Bool, PresentationThemeReference, Bool)?, NoError> in + if case let .cloud(info) = updatedTheme { let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: info.theme).start() let _ = saveThemeInteractively(account: context.account, accountManager: context.sharedContext.accountManager, theme: info.theme).start() } - return context.sharedContext.accountManager.transaction { transaction -> Void in + + let autoNightModeTriggered = context.sharedContext.currentPresentationData.with { $0 }.autoNightModeTriggered + + return context.sharedContext.accountManager.transaction { transaction -> (PresentationThemeReference, PresentationThemeAccentColor?, Bool, PresentationThemeReference, Bool)? in + var previousDefaultTheme: (PresentationThemeReference, PresentationThemeAccentColor?, Bool, PresentationThemeReference, Bool)? transaction.updateSharedData(ApplicationSpecificSharedDataKeys.presentationThemeSettings, { entry in - let current = entry as? PresentationThemeSettings ?? PresentationThemeSettings.defaultSettings - var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers - themeSpecificChatWallpapers[theme.index] = nil - return PresentationThemeSettings(chatWallpaper: resolvedWallpaper ?? previewTheme.chat.defaultWallpaper, theme: theme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) + let currentSettings: PresentationThemeSettings + if let entry = entry as? PresentationThemeSettings { + currentSettings = entry + } else { + currentSettings = PresentationThemeSettings.defaultSettings + } + + var updatedSettings: PresentationThemeSettings + if autoNightModeTriggered { + if case .builtin = currentSettings.automaticThemeSwitchSetting.theme { + previousDefaultTheme = (currentSettings.automaticThemeSwitchSetting.theme, currentSettings.themeSpecificAccentColors[currentSettings.automaticThemeSwitchSetting.theme.index], true, updatedTheme, existing) + } + + var automaticThemeSwitchSetting = currentSettings.automaticThemeSwitchSetting + automaticThemeSwitchSetting.theme = updatedTheme + updatedSettings = currentSettings.withUpdatedAutomaticThemeSwitchSetting(automaticThemeSwitchSetting) + } else { + if case .builtin = currentSettings.theme { + previousDefaultTheme = (currentSettings.theme, currentSettings.themeSpecificAccentColors[currentSettings.theme.index], false, updatedTheme, existing) + } + + updatedSettings = currentSettings.withUpdatedTheme(updatedTheme) + } + + var themeSpecificAccentColors = updatedSettings.themeSpecificAccentColors + if case let .cloud(info) = updatedTheme, let settings = info.theme.settings { + let baseThemeReference = PresentationThemeReference.builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)) + themeSpecificAccentColors[baseThemeReference.index] = PresentationThemeAccentColor(themeIndex: updatedTheme.index) + } + + var themeSpecificChatWallpapers = updatedSettings.themeSpecificChatWallpapers + themeSpecificChatWallpapers[updatedTheme.index] = nil + return updatedSettings.withUpdatedThemeSpecificChatWallpapers(themeSpecificChatWallpapers).withUpdatedThemeSpecificAccentColors(themeSpecificAccentColors) }) + return previousDefaultTheme } } var cancelImpl: (() -> Void)? - let progressSignal = Signal { [weak self] subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + let progress = Signal { [weak self] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) self?.present(controller, in: .window(.root)) @@ -319,19 +408,67 @@ public final class ThemePreviewController: ViewController { |> runOn(Queue.mainQueue()) |> delay(0.35, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() + let progressDisposable = progress.start() cancelImpl = { disposable.set(nil) } - disposable.set((signal + disposable.set((setup |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() } } - |> deliverOnMainQueue).start(completed: {[weak self] in - if let strongSelf = self { - strongSelf.dismiss() + |> deliverOnMainQueue).start(next: { [weak self] previousDefaultTheme in + if let strongSelf = self, let layout = strongSelf.validLayout { + Queue.mainQueue().after(0.3) { + if layout.size.width >= 375.0 { + let navigationController = strongSelf.navigationController as? NavigationController + if let (previousDefaultTheme, previousAccentColor, autoNightMode, theme, existing) = previousDefaultTheme { + let _ = (ApplicationSpecificNotice.getThemeChangeTip(accountManager: strongSelf.context.sharedContext.accountManager) + |> deliverOnMainQueue).start(next: { [weak self] displayed in + guard let strongSelf = self, !displayed else { + return + } + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .actionSucceeded(title: strongSelf.presentationData.strings.Theme_ThemeChanged, text: strongSelf.presentationData.strings.Theme_ThemeChangedText, cancel: strongSelf.presentationData.strings.Undo_Undo), elevatedLayout: true, animateInAsReplacement: false, action: { value in + if value == .undo { + Queue.mainQueue().after(0.2) { + let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current -> PresentationThemeSettings in + var updated: PresentationThemeSettings + if autoNightMode { + var automaticThemeSwitchSetting = current.automaticThemeSwitchSetting + automaticThemeSwitchSetting.theme = previousDefaultTheme + updated = current.withUpdatedAutomaticThemeSwitchSetting(automaticThemeSwitchSetting) + } else { + updated = current.withUpdatedTheme(previousDefaultTheme) + } + + var themeSpecificAccentColors = current.themeSpecificAccentColors + themeSpecificAccentColors[previousDefaultTheme.index] = previousAccentColor + updated = updated.withUpdatedThemeSpecificAccentColors(themeSpecificAccentColors) + + return updated + }).start() + } + + if case let .cloud(info) = theme { + let _ = deleteThemeInteractively(account: context.account, accountManager: context.sharedContext.accountManager, theme: info.theme).start() + } + return true + } else if value == .info { + let controller = themeSettingsController(context: context) + controller.navigationPresentation = .modal + navigationController?.pushViewController(controller, animated: true) + return true + } + return false + }), in: .window(.root)) + + ApplicationSpecificNotice.markThemeChangeTipAsSeen(accountManager: strongSelf.context.sharedContext.accountManager) + }) + } + } + strongSelf.dismiss() + } } })) } @@ -339,6 +476,8 @@ public final class ThemePreviewController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) + self.validLayout = layout + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } @@ -351,7 +490,7 @@ public final class ThemePreviewController: ViewController { case let .theme(theme): subject = .url("https://t.me/addtheme/\(theme.slug)") preferredAction = .default - case let .slug(slug, _): + case let .slug(slug, _), let .themeSettings(slug, _): subject = .url("https://t.me/addtheme/\(slug)") preferredAction = .default case let .media(media): diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index b8bd4b6c89..469477a0d6 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -33,6 +33,8 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { private var previewTheme: PresentationTheme private var presentationData: PresentationData private let isPreview: Bool + + private let ready: Promise public let wallpaperPromise = Promise() @@ -49,9 +51,11 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { private let separatorNode: ASDisplayNode private let chatContainerNode: ASDisplayNode + private let messagesContainerNode: ASDisplayNode private let instantChatBackgroundNode: WallpaperBackgroundNode private let remoteChatBackgroundNode: TransformImageNode private let blurredNode: BlurredImageNode + private var dateHeaderNode: ListViewItemHeaderNode? private var messageNodes: [ListViewItemNode]? private let toolbarNode: WallpaperGalleryToolbarNode @@ -63,11 +67,15 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { private var statusDisposable: Disposable? private var fetchDisposable = MetaDisposable() - init(context: AccountContext, previewTheme: PresentationTheme, dismiss: @escaping () -> Void, apply: @escaping () -> Void, isPreview: Bool) { + private var dismissed = false + + init(context: AccountContext, previewTheme: PresentationTheme, initialWallpaper: TelegramWallpaper?, dismiss: @escaping () -> Void, apply: @escaping () -> Void, isPreview: Bool, ready: Promise) { self.context = context self.previewTheme = previewTheme self.isPreview = isPreview + self.ready = ready + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } let calendar = Calendar(identifier: .gregorian) @@ -81,34 +89,43 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.pageControlBackgroundNode = ASDisplayNode() self.pageControlBackgroundNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.3) - self.pageControlBackgroundNode.cornerRadius = 8.0 + self.pageControlBackgroundNode.cornerRadius = 10.5 - self.pageControlNode = PageControlNode(dotColor: previewTheme.chatList.unreadBadgeActiveBackgroundColor, inactiveDotColor: previewTheme.list.pageIndicatorInactiveColor) + self.pageControlNode = PageControlNode(dotSpacing: 7.0, dotColor: .white, inactiveDotColor: UIColor.white.withAlphaComponent(0.4)) self.chatListBackgroundNode = ASDisplayNode() self.chatContainerNode = ASDisplayNode() self.chatContainerNode.clipsToBounds = true + + self.messagesContainerNode = ASDisplayNode() + self.messagesContainerNode.clipsToBounds = true + self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + self.instantChatBackgroundNode = WallpaperBackgroundNode() self.instantChatBackgroundNode.displaysAsynchronously = false - self.instantChatBackgroundNode.image = chatControllerBackgroundImage(theme: previewTheme, wallpaper: previewTheme.chat.defaultWallpaper, mediaBox: context.sharedContext.accountManager.mediaBox, knockoutMode: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper) + + let wallpaper = initialWallpaper ?? previewTheme.chat.defaultWallpaper + self.instantChatBackgroundNode.image = chatControllerBackgroundImage(theme: previewTheme, wallpaper: wallpaper, mediaBox: context.sharedContext.accountManager.mediaBox, knockoutMode: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper) + if self.instantChatBackgroundNode.image != nil { + self.ready.set(.single(true)) + } + if case .gradient = wallpaper { + self.instantChatBackgroundNode.imageContentMode = .scaleToFill + } self.instantChatBackgroundNode.motionEnabled = previewTheme.chat.defaultWallpaper.settings?.motion ?? false self.instantChatBackgroundNode.view.contentMode = .scaleAspectFill self.remoteChatBackgroundNode = TransformImageNode() - self.remoteChatBackgroundNode.backgroundColor = previewTheme.chatList.backgroundColor self.remoteChatBackgroundNode.view.contentMode = .scaleAspectFill self.blurredNode = BlurredImageNode() self.blurredNode.blurView.contentMode = .scaleAspectFill - self.toolbarNode = WallpaperGalleryToolbarNode(theme: self.previewTheme, strings: self.presentationData.strings) + self.toolbarNode = WallpaperGalleryToolbarNode(theme: self.previewTheme, strings: self.presentationData.strings, doneButtonType: .set) - if case let .file(file) = previewTheme.chat.defaultWallpaper, file.id == 0 { - self.remoteChatBackgroundNode.isHidden = false + if case let .file(file) = previewTheme.chat.defaultWallpaper { self.toolbarNode.setDoneEnabled(false) - } else { - self.remoteChatBackgroundNode.isHidden = true } self.maskNode = ASImageNode() @@ -131,7 +148,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.maskNode.image = generateMaskImage(color: self.previewTheme.chatList.backgroundColor) if case let .color(value) = self.previewTheme.chat.defaultWallpaper { - self.instantChatBackgroundNode.backgroundColor = UIColor(rgb: UInt32(bitPattern: value)) + self.instantChatBackgroundNode.backgroundColor = UIColor(rgb: value) } self.pageControlNode.isUserInteractionEnabled = false @@ -150,19 +167,25 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.chatContainerNode.addSubnode(self.instantChatBackgroundNode) self.chatContainerNode.addSubnode(self.remoteChatBackgroundNode) + self.chatContainerNode.addSubnode(self.messagesContainerNode) self.addSubnode(self.separatorNode) self.toolbarNode.cancel = { dismiss() } - self.toolbarNode.done = { - apply() + self.toolbarNode.done = { [weak self] in + if let strongSelf = self { + if !strongSelf.dismissed { + strongSelf.dismissed = true + apply() + } + } } if case let .file(file) = self.previewTheme.chat.defaultWallpaper { if file.settings.blur { - self.chatContainerNode.addSubnode(self.blurredNode) + self.chatContainerNode.insertSubnode(self.blurredNode, belowSubnode: self.messagesContainerNode) } } @@ -178,6 +201,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { strongSelf.blurredNode.image = image strongSelf.blurredNode.blurView.blurRadius = 45.0 } + self?.ready.set(.single(true)) } self.colorDisposable = (self.wallpaperPromise.get() @@ -190,11 +214,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { } |> deliverOnMainQueue).start(next: { [weak self] color in if let strongSelf = self { - if strongSelf.previewTheme.chat.defaultWallpaper.hasWallpaper { - strongSelf.pageControlBackgroundNode.backgroundColor = color - } else { - strongSelf.pageControlBackgroundNode.backgroundColor = .clear - } + strongSelf.pageControlBackgroundNode.backgroundColor = color } }) @@ -209,15 +229,15 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { var convertedRepresentations: [ImageRepresentationWithReference] = [] for representation in file.file.previewRepresentations { - convertedRepresentations.append(ImageRepresentationWithReference(representation: representation, reference: MediaResourceReference.media(media: .standalone(media: file.file), resource: representation.resource))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: representation, reference: .wallpaper(wallpaper: .slug(file.slug), resource: representation.resource))) } - convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource), reference: .media(media: .standalone(media: file.file), resource: file.file.resource))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) let signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError> let fileReference = FileMediaReference.standalone(media: file.file) - if file.isPattern { + if wallpaper.isPattern { signal = patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, mode: .screen, autoFetchFullSize: false) - } else { + } else if strongSelf.instantChatBackgroundNode.image == nil { signal = wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: fileReference, representations: convertedRepresentations, alwaysShowThumbnailFirst: false, autoFetchFullSize: false) |> afterNext { next in if let _ = context.sharedContext.accountManager.mediaBox.completedResourcePath(file.file.resource) { @@ -225,11 +245,13 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data) } } + } else { + signal = .complete() } strongSelf.remoteChatBackgroundNode.setSignal(signal) - - strongSelf.fetchDisposable.set(freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file.file)).start()) + strongSelf.fetchDisposable.set(fetchedMediaResource(mediaBox: context.sharedContext.accountManager.mediaBox, reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)).start()) + let account = strongSelf.context.account let statusSignal = strongSelf.context.sharedContext.accountManager.mediaBox.resourceStatus(file.file.resource) |> take(1) @@ -248,16 +270,20 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { } }) - var patternColor: UIColor? - var patternIntensity: CGFloat = 0.5 + var patternArguments: PatternWallpaperArguments? if let color = file.settings.color { + var patternIntensity: CGFloat = 0.5 if let intensity = file.settings.intensity { patternIntensity = CGFloat(intensity) / 100.0 } - patternColor = UIColor(rgb: UInt32(bitPattern: color), alpha: patternIntensity) + var patternColors = [UIColor(rgb: color, alpha: patternIntensity)] + if let bottomColor = file.settings.bottomColor { + patternColors.append(UIColor(rgb: bottomColor, alpha: patternIntensity)) + } + patternArguments = PatternWallpaperArguments(colors: patternColors, rotation: file.settings.rotation) } - strongSelf.remoteChatBackgroundNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets(), emptyColor: patternColor))() + strongSelf.remoteChatBackgroundNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets(), custom: patternArguments))() } }) } @@ -272,6 +298,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { override func didLoad() { super.didLoad() + self.scrollNode.view.bounces = false self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.isPagingEnabled = true @@ -284,13 +311,10 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.backgroundColor = self.previewTheme.list.plainBackgroundColor - self.pageControlNode.dotColor = self.previewTheme.chatList.unreadBadgeActiveBackgroundColor - self.pageControlNode.inactiveDotColor = self.previewTheme.list.pageIndicatorInactiveColor - self.chatListBackgroundNode.backgroundColor = self.previewTheme.chatList.backgroundColor self.maskNode.image = generateMaskImage(color: self.previewTheme.chatList.backgroundColor) if case let .color(value) = self.previewTheme.chat.defaultWallpaper { - self.instantChatBackgroundNode.backgroundColor = UIColor(rgb: UInt32(bitPattern: value)) + self.instantChatBackgroundNode.backgroundColor = UIColor(rgb: value) } self.toolbarNode.updateThemeAndStrings(theme: self.previewTheme, strings: self.presentationData.strings) @@ -326,10 +350,10 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) { var items: [ChatListItem] = [] - let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _ in }, togglePeerSelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, activateChatPreview: { _, _, gesture in + let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, activateChatPreview: { _, _, gesture in gesture?.cancel() }) - let chatListPresentationData = ChatListPresentationData(theme: self.previewTheme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) + let chatListPresentationData = ChatListPresentationData(theme: self.previewTheme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) let peers = SimpleDictionary() let messages = SimpleDictionary() @@ -346,24 +370,24 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { let timestamp = self.referenceTimestamp let timestamp1 = timestamp + 120 - items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex(id: MessageId(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer1.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp1, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: selfPeer, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer1), combinedReadState: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, PeerReadState.idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: 0, markedUnread: false))]), notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) + items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex(id: MessageId(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer1.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp1, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: selfPeer, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer1), combinedReadState: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, PeerReadState.idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: 0, markedUnread: false))]), notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) let presenceTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + 60 * 60) let timestamp2 = timestamp + 3660 - items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer2.id, namespace: 0, id: 0), timestamp: timestamp2)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer2.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp2, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer2, text: "", attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer2), combinedReadState: nil, notificationSettings: nil, presence: TelegramUserPresence(status: .present(until: presenceTimestamp), lastActivity: presenceTimestamp), summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: [(peer2, .typingText)], isAd: false, ignoreUnreadBadge: false, displayAsMessage: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) + items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer2.id, namespace: 0, id: 0), timestamp: timestamp2)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer2.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp2, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer2, text: "", attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer2), combinedReadState: nil, notificationSettings: nil, presence: TelegramUserPresence(status: .present(until: presenceTimestamp), lastActivity: presenceTimestamp), summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: [(peer2, .typingText)], isAd: false, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) let timestamp3 = timestamp + 3200 - items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer3.id, namespace: 0, id: 0), timestamp: timestamp3)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer3.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp3, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer3Author, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer3), combinedReadState: nil, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) + items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer3.id, namespace: 0, id: 0), timestamp: timestamp3)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer3.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp3, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer3Author, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer3), combinedReadState: nil, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) let timestamp4 = timestamp + 3000 - items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer4.id, namespace: 0, id: 0), timestamp: timestamp4)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer4.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp4, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer4, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer4), combinedReadState: nil, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) + items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer4.id, namespace: 0, id: 0), timestamp: timestamp4)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer4.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp4, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer4, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer4), combinedReadState: nil, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) let timestamp5 = timestamp + 1000 - items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer5.id, namespace: 0, id: 0), timestamp: timestamp5)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer4.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp5, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer5, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer5), combinedReadState: nil, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) + items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer5.id, namespace: 0, id: 0), timestamp: timestamp5)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer4.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp5, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer5, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer5), combinedReadState: nil, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) - items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer6.id, namespace: 0, id: 0), timestamp: timestamp - 360)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer6.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp - 360, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer6, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer6), combinedReadState: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, PeerReadState.idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: 1, markedUnread: false))]), notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) + items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer6.id, namespace: 0, id: 0), timestamp: timestamp - 360)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer6.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp - 360, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer6, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer6), combinedReadState: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, PeerReadState.idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: 1, markedUnread: false))]), notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) - items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer7.id, namespace: 0, id: 0), timestamp: timestamp - 420)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer7.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp - 420, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer6, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_7_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer7), combinedReadState: nil, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) + items.append(ChatListItem(presentationData: chatListPresentationData, context: self.context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: peer7.id, namespace: 0, id: 0), timestamp: timestamp - 420)), content: .peer(message: Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer7.id, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp - 420, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer6, text: self.presentationData.strings.Appearance_ThemePreview_ChatList_7_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), peer: RenderedPeer(peer: peer7), combinedReadState: nil, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)) let width: CGFloat if case .regular = layout.metrics.widthClass { @@ -418,6 +442,8 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { } private func updateMessagesLayout(layout: ContainerViewLayout, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + let headerItem = self.context.sharedContext.makeChatMessageDateHeaderItem(context: self.context, timestamp: self.referenceTimestamp, theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder) + var items: [ListViewItem] = [] let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: 1) let otherPeerId = self.context.account.peerId @@ -426,25 +452,41 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) - let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3) - messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + var sampleMessages: [Message] = [] + + let message1 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_4_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + sampleMessages.append(message1) - let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message1, theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.previewTheme.chat.defaultWallpaper, fontSize: self.presentationData.fontSize, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil)) + let message2 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_5_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + sampleMessages.append(message2) - let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message2, theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.previewTheme.chat.defaultWallpaper, fontSize: self.presentationData.fontSize, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil)) + let message3 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66002, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_6_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + sampleMessages.append(message3) + + let message4 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66003, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_7_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + sampleMessages.append(message4) + + let message5 = Message(stableId: 5, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 5), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66004, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + messages[message5.id] = message5 + sampleMessages.append(message5) let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: MemoryBuffer(data: Data(base64Encoded: waveformBase64)!))] let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) - let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message3, theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.previewTheme.chat.defaultWallpaper, fontSize: self.presentationData.fontSize, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local))) + let message6 = Message(stableId: 6, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 6), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66005, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: []) + sampleMessages.append(message6) - let message4 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message4, theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.previewTheme.chat.defaultWallpaper, fontSize: self.presentationData.fontSize, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil)) + let message7 = Message(stableId: 7, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 7), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66006, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: message5.id)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + sampleMessages.append(message7) + let message8 = Message(stableId: 8, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 8), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66007, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + sampleMessages.append(message8) + + items = sampleMessages.reversed().map { message in + self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message, theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.previewTheme.chat.defaultWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: !message.media.isEmpty ? FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local) : nil, tapMessage: nil, clickThroughMessage: nil) + } + let width: CGFloat if case .regular = layout.metrics.widthClass { width = layout.size.width / 2.0 @@ -477,22 +519,36 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { itemNode = node apply().1(ListViewItemApply(isOnScreen: true)) }) - itemNode!.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) itemNode!.isUserInteractionEnabled = false messageNodes.append(itemNode!) - self.chatContainerNode.addSubnode(itemNode!) + self.messagesContainerNode.addSubnode(itemNode!) } self.messageNodes = messageNodes } + var bottomOffset: CGFloat = 9.0 + bottomInset if let messageNodes = self.messageNodes { - var bottomOffset: CGFloat = layout.size.height - bottomInset - 9.0 for itemNode in messageNodes { - transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset - itemNode.frame.height), size: itemNode.frame.size)) - bottomOffset -= itemNode.frame.height + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: itemNode.frame.size)) + bottomOffset += itemNode.frame.height itemNode.updateFrame(itemNode.frame, within: layout.size) } } + + let dateHeaderNode: ListViewItemHeaderNode + if let currentDateHeaderNode = self.dateHeaderNode { + dateHeaderNode = currentDateHeaderNode + headerItem.updateNode(dateHeaderNode, previous: nil, next: headerItem) + } else { + dateHeaderNode = headerItem.node() + dateHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + self.messagesContainerNode.addSubnode(dateHeaderNode) + self.dateHeaderNode = dateHeaderNode + } + + transition.updateFrame(node: dateHeaderNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: CGSize(width: layout.size.width, height: headerItem.height))) + dateHeaderNode.updateLayout(size: self.messagesContainerNode.frame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -527,23 +583,25 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.pageControlBackgroundNode.isHidden = false self.separatorNode.isHidden = true - bottomInset = 66.0 + bottomInset = 38.0 } + self.messagesContainerNode.frame = self.chatContainerNode.bounds self.instantChatBackgroundNode.frame = self.chatContainerNode.bounds + self.instantChatBackgroundNode.updateLayout(size: self.instantChatBackgroundNode.bounds.size, transition: .immediate) self.remoteChatBackgroundNode.frame = self.chatContainerNode.bounds self.blurredNode.frame = self.chatContainerNode.bounds - transition.updateFrame(node: self.toolbarNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: 49.0 + layout.intrinsicInsets.bottom))) + transition.updateFrame(node: self.toolbarNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: toolbarHeight))) self.toolbarNode.updateLayout(size: CGSize(width: layout.size.width, height: 49.0), layout: layout, transition: transition) self.updateChatsLayout(layout: layout, topInset: navigationBarHeight, transition: transition) self.updateMessagesLayout(layout: layout, bottomInset: self.isPreview ? 0.0 : (toolbarHeight + bottomInset), transition: transition) let pageControlSize = self.pageControlNode.measure(CGSize(width: bounds.width, height: 100.0)) - let pageControlFrame = CGRect(origin: CGPoint(x: floor((bounds.width - pageControlSize.width) / 2.0), y: layout.size.height - toolbarHeight - 42.0), size: pageControlSize) + let pageControlFrame = CGRect(origin: CGPoint(x: floor((bounds.width - pageControlSize.width) / 2.0), y: layout.size.height - toolbarHeight - 28.0), size: pageControlSize) self.pageControlNode.frame = pageControlFrame - self.pageControlBackgroundNode.frame = CGRect(x: pageControlFrame.minX - 11.0, y: pageControlFrame.minY - 12.0, width: pageControlFrame.width + 22.0, height: 30.0) + self.pageControlBackgroundNode.frame = CGRect(x: pageControlFrame.minX - 7.0, y: pageControlFrame.minY - 7.0, width: pageControlFrame.width + 14.0, height: 21.0) transition.updateFrame(node: self.maskNode, frame: CGRect(x: 0.0, y: layout.size.height - toolbarHeight - 80.0, width: bounds.width, height: 80.0)) } } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAccentColorItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAccentColorItem.swift index 8acd578b3a..e270a7e15c 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAccentColorItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAccentColorItem.swift @@ -7,40 +7,502 @@ import TelegramCore import SyncCore import TelegramPresentationData import TelegramUIPreferences +import MergeLists import ItemListUI +import ContextUI import PresentationDataUtils -private func generateSwatchImage(theme: PresentationTheme, color: PresentationThemeAccentColor, selected: Bool) -> UIImage? { +private enum ThemeSettingsColorEntryId: Hashable { + case color(Int64) + case theme(Int64) + case picker +} + +private enum ThemeSettingsColorEntry: Comparable, Identifiable { + case color(Int, PresentationTheme, PresentationThemeReference, PresentationThemeAccentColor?, Bool) + case theme(Int, PresentationTheme, PresentationThemeReference, PresentationThemeReference, Bool) + case picker + + var stableId: ThemeSettingsColorEntryId { + switch self { + case let .color(index, _, themeReference, accentColor, _): + return .color(themeReference.index &+ Int64(accentColor?.index ?? 0)) + case let .theme(_, _, _, theme, _): + return .theme(theme.index) + case .picker: + return .picker + } + } + + static func ==(lhs: ThemeSettingsColorEntry, rhs: ThemeSettingsColorEntry) -> Bool { + switch lhs { + case let .color(lhsIndex, lhsCurrentTheme, lhsThemeReference, lhsAccentColor, lhsSelected): + if case let .color(rhsIndex, rhsCurrentTheme, rhsThemeReference, rhsAccentColor, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsCurrentTheme === rhsCurrentTheme, lhsThemeReference.index == rhsThemeReference.index, lhsAccentColor == rhsAccentColor, lhsSelected == rhsSelected { + return true + } else { + return false + } + case let .theme(lhsIndex, lhsCurrentTheme, lhsBaseThemeReference, lhsTheme, lhsSelected): + if case let .theme(rhsIndex, rhsCurrentTheme, rhsBaseThemeReference, rhsTheme, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsCurrentTheme === rhsCurrentTheme, lhsBaseThemeReference.index == rhsBaseThemeReference.index, lhsTheme == rhsTheme, lhsSelected == rhsSelected { + return true + } else { + return false + } + case .picker: + if case .picker = rhs { + return true + } else { + return false + } + } + } + + static func <(lhs: ThemeSettingsColorEntry, rhs: ThemeSettingsColorEntry) -> Bool { + switch lhs { + case .picker: + return true + case let .color(lhsIndex, _, _, _, _), let .theme(lhsIndex, _, _, _, _): + switch rhs { + case let .color(rhsIndex, _, _, _, _): + return lhsIndex < rhsIndex + case let .theme(rhsIndex, _, _, _, _): + return lhsIndex < rhsIndex + case .picker: + return false + } + } + } + + func item(action: @escaping (ThemeSettingsColorOption?, Bool) -> Void, contextAction: ((ThemeSettingsColorOption?, Bool, ASDisplayNode, ContextGesture?) -> Void)?, openColorPicker: @escaping (Bool) -> Void) -> ListViewItem { + switch self { + case let .color(_, currentTheme, themeReference, accentColor, selected): + return ThemeSettingsAccentColorIconItem(themeReference: themeReference, theme: currentTheme, color: accentColor.flatMap { .accentColor($0) }, selected: selected, action: action, contextAction: contextAction) + case let .theme(_, currentTheme, baseThemeReference, theme, selected): + return ThemeSettingsAccentColorIconItem(themeReference: baseThemeReference, theme: currentTheme, color: .theme(theme), selected: selected, action: action, contextAction: contextAction) + case .picker: + return ThemeSettingsAccentColorPickerItem(action: openColorPicker) + } + } +} + +enum ThemeSettingsColorOption: Equatable { + case accentColor(PresentationThemeAccentColor) + case theme(PresentationThemeReference) + + var accentColor: UIColor? { + switch self { + case let .accentColor(color): + return color.color + case let .theme(reference): + if case let .cloud(theme) = reference, let settings = theme.theme.settings { + return UIColor(argb: settings.accentColor) + } else { + return nil + } + } + } + + var baseColor: UIColor? { + switch self { + case let .accentColor(color): + return color.baseColor.color + case .theme: + return .clear + } + } + + var plainBubbleColors: (UIColor, UIColor)? { + switch self { + case let .accentColor(color): + return color.plainBubbleColors + case let .theme(reference): + if case let .cloud(theme) = reference, let settings = theme.theme.settings, let messageColors = settings.messageColors { + return (UIColor(argb: messageColors.top), UIColor(argb: messageColors.bottom)) + } else { + return nil + } + } + } + + var customBubbleColors: (UIColor, UIColor?)? { + switch self { + case let .accentColor(color): + return color.customBubbleColors + case let .theme(reference): + if case let .cloud(theme) = reference, let settings = theme.theme.settings, let messageColors = settings.messageColors { + let topColor = UIColor(argb: messageColors.top) + let bottomColor = UIColor(argb: messageColors.bottom) + if topColor.argb != bottomColor.argb { + return (topColor, bottomColor) + } else { + return (topColor, nil) + } + } else { + return nil + } + } + } + + var wallpaper: TelegramWallpaper? { + switch self { + case let .accentColor(color): + return color.wallpaper + case .theme: + return nil + } + } + + var index: Int64 { + switch self { + case let .accentColor(color): + return Int64(color.index) + case let .theme(reference): + return reference.index + + } + } +} + +private class ThemeSettingsAccentColorIconItem: ListViewItem { + let themeReference: PresentationThemeReference + let theme: PresentationTheme + let color: ThemeSettingsColorOption? + let selected: Bool + let action: (ThemeSettingsColorOption?, Bool) -> Void + let contextAction: ((ThemeSettingsColorOption?, Bool, ASDisplayNode, ContextGesture?) -> Void)? + + public init(themeReference: PresentationThemeReference, theme: PresentationTheme, color: ThemeSettingsColorOption?, selected: Bool, action: @escaping (ThemeSettingsColorOption?, Bool) -> Void, contextAction: ((ThemeSettingsColorOption?, Bool, ASDisplayNode, ContextGesture?) -> Void)?) { + self.themeReference = themeReference + self.theme = theme + self.color = color + self.selected = selected + self.action = action + self.contextAction = contextAction + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = ThemeSettingsAccentColorIconItemNode() + let (nodeLayout, apply) = node.asyncLayout()(self, params) + node.insets = nodeLayout.insets + node.contentSize = nodeLayout.contentSize + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in + apply(false) + }) + }) + } + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + assert(node() is ThemeSettingsAccentColorIconItemNode) + if let nodeValue = node() as? ThemeSettingsAccentColorIconItemNode { + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params) + Queue.mainQueue().async { + completion(nodeLayout, { _ in + let animated: Bool + if case .Crossfade = animation { + animated = true + } else { + animated = false + } + apply(animated) + }) + } + } + } + } + } + + public var selectable = true + public func selected(listView: ListView) { + self.action(self.color, self.selected) + } +} + +private func generateRingImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) - let fillColor = color.color - var strokeColor = color.baseColor.color - if strokeColor == .clear { - strokeColor = fillColor + context.setStrokeColor(color.cgColor) + context.setLineWidth(2.0) + context.strokeEllipse(in: bounds.insetBy(dx: 1.0, dy: 1.0)) + }) +} + +private func generateFillImage(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setFillColor(color.cgColor) + context.fillEllipse(in: bounds.insetBy(dx: 4.0, dy: 4.0)) + }) +} + +private func generateCenterImage(topColor: UIColor, bottomColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.addEllipse(in: bounds.insetBy(dx: 10.0, dy: 10.0)) + context.clip() + + let gradientColors = [topColor.cgColor, bottomColor.cgColor] as CFArray + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 10.0), end: CGPoint(x: 0.0, y: size.height - 10.0), options: CGGradientDrawingOptions()) + }) +} + +private func generateDotsImage() -> UIImage? { + return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setFillColor(UIColor.white.cgColor) + let dotSize = CGSize(width: 4.0, height: 4.0) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 11.0, y: 18.0), size: dotSize)) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 18.0, y: 18.0), size: dotSize)) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 25.0, y: 18.0), size: dotSize)) + }) +} + +private final class ThemeSettingsAccentColorIconItemNode : ListViewItemNode { + private let containerNode: ContextControllerSourceNode + private let fillNode: ASImageNode + private let ringNode: ASImageNode + private let centerNode: ASImageNode + private let dotsNode: ASImageNode + + var item: ThemeSettingsAccentColorIconItem? + + init() { + self.containerNode = ContextControllerSourceNode() + + self.fillNode = ASImageNode() + self.fillNode.displaysAsynchronously = false + self.fillNode.displayWithoutProcessing = true + + self.ringNode = ASImageNode() + self.ringNode.displaysAsynchronously = false + self.ringNode.displayWithoutProcessing = true + + self.centerNode = ASImageNode() + self.centerNode.displaysAsynchronously = false + self.centerNode.displayWithoutProcessing = true + + self.dotsNode = ASImageNode() + self.dotsNode.displaysAsynchronously = false + self.dotsNode.displayWithoutProcessing = true + self.dotsNode.image = generateDotsImage() + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.fillNode) + self.containerNode.addSubnode(self.ringNode) + self.containerNode.addSubnode(self.dotsNode) + self.containerNode.addSubnode(self.centerNode) + + self.containerNode.activated = { [weak self] gesture in + guard let strongSelf = self, let item = strongSelf.item else { + gesture.cancel() + return + } + item.contextAction?(item.color, item.selected, strongSelf.containerNode, gesture) } - if strokeColor.distance(to: theme.list.itemBlocksBackgroundColor) < 200 { - if strokeColor.distance(to: UIColor.white) < 200 { - strokeColor = UIColor(rgb: 0x999999) - } else { - strokeColor = theme.list.controlSecondaryColor + } + + override func didLoad() { + super.didLoad() + + self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + } + + func setSelected(_ selected: Bool, animated: Bool = false) { + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate + if selected { + transition.updateTransformScale(node: self.fillNode, scale: 1.0) + transition.updateTransformScale(node: self.centerNode, scale: 0.16) + transition.updateAlpha(node: self.centerNode, alpha: 0.0) + transition.updateTransformScale(node: self.dotsNode, scale: 1.0) + transition.updateAlpha(node: self.dotsNode, alpha: 1.0) + } else { + transition.updateTransformScale(node: self.fillNode, scale: 1.2) + transition.updateTransformScale(node: self.centerNode, scale: 1.0) + transition.updateAlpha(node: self.centerNode, alpha: 1.0) + transition.updateTransformScale(node: self.dotsNode, scale: 0.85) + transition.updateAlpha(node: self.dotsNode, alpha: 0.0) + } + } + + func asyncLayout() -> (ThemeSettingsAccentColorIconItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let currentItem = self.item + + return { [weak self] item, params in + var updatedAccentColor = false + var updatedSelected = false + + if currentItem == nil || currentItem?.color != item.color || currentItem?.themeReference != item.themeReference { + updatedAccentColor = true + } + if currentItem?.selected != item.selected { + updatedSelected = true + } + + let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 60.0, height: 58.0), insets: UIEdgeInsets()) + return (itemLayout, { animated in + if let strongSelf = self { + strongSelf.item = item + + if updatedAccentColor { + var fillColor = item.color?.accentColor + var strokeColor = item.color?.baseColor + if strokeColor == .clear { + strokeColor = fillColor + } + + if let color = strokeColor, color.distance(to: item.theme.list.itemBlocksBackgroundColor) < 200 { + if color.distance(to: UIColor.white) < 200 { + strokeColor = UIColor(rgb: 0x999999) + } else { + strokeColor = item.theme.list.controlSecondaryColor + } + } + + var topColor: UIColor? + var bottomColor: UIColor? + + if let colors = item.color?.plainBubbleColors { + topColor = colors.0 + bottomColor = colors.1 + } else if case .builtin(.dayClassic) = item.themeReference { + if let accentColor = item.color?.accentColor { + let hsb = accentColor.hsb + let bubbleColor = UIColor(hue: hsb.0, saturation: (hsb.1 > 0.0 && hsb.2 > 0.0) ? 0.14 : 0.0, brightness: 0.79 + hsb.2 * 0.21, alpha: 1.0) + topColor = bubbleColor + bottomColor = bubbleColor + } else { + fillColor = UIColor(rgb: 0x007ee5) + strokeColor = fillColor + topColor = UIColor(rgb: 0xe1ffc7) + bottomColor = topColor + } + } else if case .builtin(.nightAccent) = item.themeReference { + if let accentColor = item.color?.accentColor { + bottomColor = accentColor.withMultiplied(hue: 1.019, saturation: 0.731, brightness: 0.59) + topColor = bottomColor!.withMultiplied(hue: 0.966, saturation: 0.61, brightness: 0.98) + } else { + fillColor = UIColor(rgb: 0x2ea6ff) + strokeColor = fillColor + topColor = UIColor(rgb: 0x466f95) + bottomColor = topColor + } + } + + strongSelf.fillNode.image = generateFillImage(color: fillColor ?? .clear) + strongSelf.ringNode.image = generateRingImage(color: strokeColor ?? .clear) + strongSelf.centerNode.image = generateCenterImage(topColor: topColor ?? .clear, bottomColor: bottomColor ?? .clear) + } + + let center = CGPoint(x: 30.0, y: 29.0) + let bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 40.0, height: 40.0)) + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: itemLayout.contentSize) + + strongSelf.fillNode.position = center + strongSelf.ringNode.position = center + strongSelf.centerNode.position = center + strongSelf.dotsNode.position = center + + strongSelf.fillNode.bounds = bounds + strongSelf.ringNode.bounds = bounds + strongSelf.centerNode.bounds = bounds + strongSelf.dotsNode.bounds = bounds + + if updatedSelected { + strongSelf.setSelected(item.selected, animated: !updatedAccentColor && currentItem != nil) + } + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + super.animateRemoved(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + super.animateAdded(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } +} + +private class ThemeSettingsAccentColorPickerItem: ListViewItem { + let action: (Bool) -> Void + + public init(action: @escaping (Bool) -> Void) { + self.action = action + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = ThemeSettingsAccentColorPickerItemNode() + let (nodeLayout, apply) = node.asyncLayout()(self, params) + node.insets = nodeLayout.insets + node.contentSize = nodeLayout.contentSize + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in + apply(false) + }) + }) } } - - context.setFillColor(fillColor.cgColor) - context.setStrokeColor(strokeColor.cgColor) - context.setLineWidth(2.0) - - if selected { - context.fillEllipse(in: bounds.insetBy(dx: 4.0, dy: 4.0)) - context.strokeEllipse(in: bounds.insetBy(dx: 1.0, dy: 1.0)) - } else { - context.fillEllipse(in: bounds) + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + assert(node() is ThemeSettingsAccentColorPickerItemNode) + if let nodeValue = node() as? ThemeSettingsAccentColorPickerItemNode { + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params) + Queue.mainQueue().async { + completion(nodeLayout, { _ in + apply(animation.isAnimated) + }) + } + } + } } - })?.stretchableImage(withLeftCapWidth: 15, topCapHeight: 15) + } + + public var selectable = true + public func selected(listView: ListView) { + self.action(true) + } } + private func generateCustomSwatchImage() -> UIImage? { return generateImage(CGSize(width: 42.0, height: 42.0), rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) @@ -71,21 +533,104 @@ private func generateCustomSwatchImage() -> UIImage? { }) } +private final class ThemeSettingsAccentColorPickerItemNode : ListViewItemNode { + private let imageNode: ASImageNode + + var item: ThemeSettingsAccentColorPickerItem? + + init() { + self.imageNode = ASImageNode() + self.imageNode.displaysAsynchronously = false + self.imageNode.displayWithoutProcessing = true + self.imageNode.image = generateCustomSwatchImage() + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.imageNode) + } + + override func didLoad() { + super.didLoad() + + self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + } + + func asyncLayout() -> (ThemeSettingsAccentColorPickerItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let currentItem = self.item + + return { [weak self] item, params in + let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 60.0, height: 60.0), insets: UIEdgeInsets()) + return (itemLayout, { animated in + if let strongSelf = self { + strongSelf.item = item + + strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: 11.0, y: 9.0), size: CGSize(width: 42.0, height: 42.0)) + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + super.animateRemoved(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + super.animateAdded(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } +} + +enum ThemeSettingsAccentColor { + case `default` + case color(PresentationThemeBaseColor) + case preset(PresentationThemeAccentColor) + case custom(PresentationThemeAccentColor) + case theme(PresentationThemeReference) + + var index: Int64? { + switch self { + case .default: + return nil + case let .color(color): + return Int64(10 + color.rawValue) + case let .preset(color), let .custom(color): + return Int64(color.index) + case let .theme(theme): + return theme.index + } + } +} + class ThemeSettingsAccentColorItem: ListViewItem, ItemListItem { var sectionId: ItemListSectionId let theme: PresentationTheme - let colors: [PresentationThemeBaseColor] - let currentColor: PresentationThemeAccentColor - let updated: (PresentationThemeAccentColor) -> Void - let openColorPicker: () -> Void + let generalThemeReference: PresentationThemeReference + let themeReference: PresentationThemeReference + let colors: [ThemeSettingsAccentColor] + let currentColor: ThemeSettingsColorOption? + let updated: (ThemeSettingsColorOption?) -> Void + let contextAction: ((Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void)? + let openColorPicker: (Bool) -> Void let tag: ItemListItemTag? - init(theme: PresentationTheme, sectionId: ItemListSectionId, colors: [PresentationThemeBaseColor], currentColor: PresentationThemeAccentColor, updated: @escaping (PresentationThemeAccentColor) -> Void, openColorPicker: @escaping () -> Void, tag: ItemListItemTag? = nil) { + init(theme: PresentationTheme, sectionId: ItemListSectionId, generalThemeReference: PresentationThemeReference, themeReference: PresentationThemeReference, colors: [ThemeSettingsAccentColor], currentColor: ThemeSettingsColorOption?, updated: @escaping (ThemeSettingsColorOption?) -> Void, contextAction: ((Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void)?, openColorPicker: @escaping (Bool) -> Void, tag: ItemListItemTag? = nil) { self.theme = theme + self.generalThemeReference = generalThemeReference + self.themeReference = themeReference self.colors = colors self.currentColor = currentColor self.updated = updated + self.contextAction = contextAction self.openColorPicker = openColorPicker self.tag = tag self.sectionId = sectionId @@ -125,59 +670,52 @@ class ThemeSettingsAccentColorItem: ListViewItem, ItemListItem { } } -private final class ThemeSettingsAccentColorNode : ASDisplayNode { - private let iconNode: ASImageNode - private var action: (() -> Void)? - - override init() { - self.iconNode = ASImageNode() - self.iconNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 62.0, height: 62.0)) - self.iconNode.isLayerBacked = true - - super.init() - - self.addSubnode(self.iconNode) - } - - func setup(theme: PresentationTheme, color: PresentationThemeAccentColor, selected: Bool, action: @escaping () -> Void) { - self.iconNode.image = generateSwatchImage(theme: theme, color: color, selected: selected) - self.action = { - action() - } - } - - override func didLoad() { - super.didLoad() - - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - } - - @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.action?() - } - } - - override func layout() { - super.layout() +private struct ThemeSettingsAccentColorItemNodeTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let updatePosition: Bool +} - self.iconNode.frame = self.bounds +private func preparedTransition(action: @escaping (ThemeSettingsColorOption?, Bool) -> Void, contextAction: ((ThemeSettingsColorOption?, Bool, ASDisplayNode, ContextGesture?) -> Void)?, openColorPicker: @escaping (Bool) -> Void, from fromEntries: [ThemeSettingsColorEntry], to toEntries: [ThemeSettingsColorEntry], updatePosition: Bool) -> ThemeSettingsAccentColorItemNodeTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(action: action, contextAction: contextAction, openColorPicker: openColorPicker), directionHint: .Down) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(action: action, contextAction: contextAction, openColorPicker: openColorPicker), directionHint: nil) } + + return ThemeSettingsAccentColorItemNodeTransition(deletions: deletions, insertions: insertions, updates: updates, updatePosition: updatePosition) +} + +private func ensureColorVisible(listNode: ListView, accentColor: ThemeSettingsColorOption?, animated: Bool) -> Bool { + var resultNode: ThemeSettingsAccentColorIconItemNode? + listNode.forEachItemNode { node in + if resultNode == nil, let node = node as? ThemeSettingsAccentColorIconItemNode { + if node.item?.color?.index == accentColor?.index { + resultNode = node + } + } + } + if let resultNode = resultNode { + listNode.ensureItemNodeVisible(resultNode, animated: animated, overflow: 24.0) + return true + } else { + return false } } - -private let textFont = Font.regular(11.0) -private let itemSize = Font.regular(11.0) - class ThemeSettingsAccentColorItemNode: ListViewItemNode, ItemListItemNode { + private let containerNode: ASDisplayNode private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let maskNode: ASImageNode + private var snapshotView: UIView? - private let scrollNode: ASScrollNode - private var colorNodes: [ThemeSettingsAccentColorNode] = [] - private let customNode: HighlightableButtonNode + private let listNode: ListView + private var entries: [ThemeSettingsColorEntry]? + private var enqueuedTransitions: [ThemeSettingsAccentColorItemNodeTransition] = [] + private var initialized = false private var item: ThemeSettingsAccentColorItem? private var layoutParams: ListViewItemLayoutParams? @@ -187,6 +725,8 @@ class ThemeSettingsAccentColorItemNode: ListViewItemNode, ItemListItemNode { } init() { + self.containerNode = ASDisplayNode() + self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -198,38 +738,49 @@ class ThemeSettingsAccentColorItemNode: ListViewItemNode, ItemListItemNode { self.maskNode = ASImageNode() - self.scrollNode = ASScrollNode() - - self.customNode = HighlightableButtonNode() + self.listNode = ListView() + self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) super.init(layerBacked: false, dynamicBounce: false) - - self.customNode.setImage(generateCustomSwatchImage(), for: .normal) - self.customNode.addTarget(self, action: #selector(customPressed), forControlEvents: .touchUpInside) - self.addSubnode(self.scrollNode) - self.scrollNode.addSubnode(self.customNode) + self.addSubnode(self.containerNode) + self.addSubnode(self.listNode) } override func didLoad() { super.didLoad() - self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true - self.scrollNode.view.showsHorizontalScrollIndicator = false + self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true } - @objc func customPressed() { - self.item?.openColorPicker() - } - - private func scrollToNode(_ node: ThemeSettingsAccentColorNode, animated: Bool) { - let bounds = self.scrollNode.view.bounds - let frame = node.frame.insetBy(dx: -48.0, dy: 0.0) + private func enqueueTransition(_ transition: ThemeSettingsAccentColorItemNodeTransition) { + self.enqueuedTransitions.append(transition) - if frame.minX < bounds.minX || frame.maxX > bounds.maxX { - self.scrollNode.view.scrollRectToVisible(frame, animated: animated) + if let _ = self.item { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } } } + private func dequeueTransition() { + guard let item = self.item, let transition = self.enqueuedTransitions.first else { + return + } + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + var scrollToItem: ListViewScrollToItem? + if !self.initialized || transition.updatePosition { + if let index = item.colors.firstIndex(where: { $0.index == item.currentColor?.index }) { + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(-70.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down) + self.initialized = true + } + } + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + }) + } + func asyncLayout() -> (_ item: ThemeSettingsAccentColorItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let currentItem = self.item @@ -239,6 +790,11 @@ class ThemeSettingsAccentColorItemNode: ListViewItemNode, ItemListItemNode { themeUpdated = true } + var colorUpdated: Bool + if currentItem?.currentColor != item.currentColor { + colorUpdated = true + } + let contentSize: CGSize let insets: UIEdgeInsets let separatorHeight = UIScreenPixel @@ -254,22 +810,23 @@ class ThemeSettingsAccentColorItemNode: ListViewItemNode, ItemListItemNode { strongSelf.item = item strongSelf.layoutParams = params - strongSelf.scrollNode.view.contentInset = UIEdgeInsets(top: 0.0, left: params.leftInset, bottom: 0.0, right: params.rightInset) - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + if themeUpdated { + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + } if strongSelf.backgroundNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + strongSelf.containerNode.insertSubnode(strongSelf.backgroundNode, at: 0) } if strongSelf.topStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + strongSelf.containerNode.insertSubnode(strongSelf.topStripeNode, at: 1) } if strongSelf.bottomStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + strongSelf.containerNode.insertSubnode(strongSelf.bottomStripeNode, at: 2) } if strongSelf.maskNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + strongSelf.containerNode.insertSubnode(strongSelf.maskNode, at: 3) } let hasCorners = itemListHasRoundedBlockLayout(params) @@ -295,6 +852,7 @@ class ThemeSettingsAccentColorItemNode: ListViewItemNode, ItemListItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } + strongSelf.containerNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: contentSize.height) strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) @@ -302,65 +860,94 @@ class ThemeSettingsAccentColorItemNode: ListViewItemNode, ItemListItemNode { strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) - strongSelf.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layoutSize.width, height: layoutSize.height)) + var listInsets = UIEdgeInsets() + listInsets.top += params.leftInset + 4.0 + listInsets.bottom += params.rightInset + 4.0 - let nodeInset: CGFloat = 15.0 - let nodeSize = CGSize(width: 40.0, height: 40.0) - var nodeOffset = nodeInset + strongSelf.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width) + strongSelf.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0) + strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - var updated = false - var selectedNode: ThemeSettingsAccentColorNode? + var entries: [ThemeSettingsColorEntry] = [] + entries.append(.picker) - var i = 0 + var index: Int = 0 for color in item.colors { - let imageNode: ThemeSettingsAccentColorNode - if strongSelf.colorNodes.count > i { - imageNode = strongSelf.colorNodes[i] - } else { - imageNode = ThemeSettingsAccentColorNode() - strongSelf.colorNodes.append(imageNode) - strongSelf.scrollNode.addSubnode(imageNode) - updated = true + switch color { + case .default: + let selected = item.currentColor == nil + entries.append(.color(index, item.theme, item.generalThemeReference, nil, selected)) + case let .color(color): + var selected = false + if let currentColor = item.currentColor, case let .accentColor(accentColor) = currentColor { + selected = accentColor.baseColor == color + } + let accentColor: ThemeSettingsColorOption + if let currentColor = item.currentColor, selected { + accentColor = currentColor + } else { + accentColor = .accentColor(PresentationThemeAccentColor(index: 10 + color.rawValue, baseColor: color)) + } + switch accentColor { + case let .accentColor(color): + entries.append(.color(index, item.theme, item.generalThemeReference, color, selected)) + case let .theme(theme): + entries.append(.theme(index, item.theme, item.generalThemeReference, theme, selected)) + } + case let .preset(color), let .custom(color): + var selected = false + if let currentColor = item.currentColor { + selected = currentColor.index == Int64(color.index) + } + entries.append(.color(index, item.theme, item.themeReference, color, selected)) + case let .theme(theme): + var selected = false + if let currentColor = item.currentColor { + selected = currentColor.index == theme.index + } + entries.append(.theme(index, item.theme, item.generalThemeReference, theme, selected)) } - - let accentColor: PresentationThemeAccentColor - let selected = item.currentColor.baseColor == color - if selected { - accentColor = item.currentColor - selectedNode = imageNode - } else { - accentColor = PresentationThemeAccentColor(baseColor: color) - } - - imageNode.setup(theme: item.theme, color: accentColor, selected: selected, action: { [weak self, weak imageNode] in - item.updated(accentColor) - if let imageNode = imageNode { - self?.scrollToNode(imageNode, animated: true) + index += 1 + } + + let action: (ThemeSettingsColorOption?, Bool) -> Void = { [weak self] color, selected in + if let strongSelf = self, let item = strongSelf.item { + if selected { + var create = true + if let color = color { + switch color { + case let .accentColor(color): + create = color.baseColor != .custom + case let .theme(theme): + if case let .cloud(theme) = theme { + create = !theme.theme.isCreator + } + } + } + item.openColorPicker(create) + } else { + item.updated(color) } - }) - - imageNode.frame = CGRect(origin: CGPoint(x: nodeOffset, y: 10.0), size: nodeSize) - nodeOffset += nodeSize.width + 18.0 - - i += 1 + ensureColorVisible(listNode: strongSelf.listNode, accentColor: color, animated: true) + } } + let contextAction: ((ThemeSettingsColorOption?, Bool, ASDisplayNode, ContextGesture?) -> Void)? = { [weak item] color, selected, node, gesture in + if let strongSelf = self, let item = strongSelf.item { + item.contextAction?(selected, item.generalThemeReference, color, node, gesture) + } + } + let openColorPicker: (Bool) -> Void = { [weak self] create in + if let strongSelf = self, let item = strongSelf.item { + item.openColorPicker(true) + } + } + + let previousEntries = strongSelf.entries ?? [] + let updatePosition = currentItem != nil && (previousEntries.count != entries.count || (currentItem?.generalThemeReference.index != item.generalThemeReference.index)) + let transition = preparedTransition(action: action, contextAction: contextAction, openColorPicker: openColorPicker, from: previousEntries, to: entries, updatePosition: updatePosition) + strongSelf.enqueueTransition(transition) - strongSelf.customNode.frame = CGRect(origin: CGPoint(x: nodeOffset, y: 9.0), size: CGSize(width: 42.0, height: 42.0)) - - for k in (i ..< strongSelf.colorNodes.count).reversed() { - let node = strongSelf.colorNodes[k] - strongSelf.colorNodes.remove(at: k) - node.removeFromSupernode() - } - - let contentSize = CGSize(width: strongSelf.customNode.frame.maxX + nodeInset, height: strongSelf.scrollNode.frame.height) - if strongSelf.scrollNode.view.contentSize != contentSize { - strongSelf.scrollNode.view.contentSize = contentSize - } - - if updated, let selectedNode = selectedNode { - strongSelf.scrollToNode(selectedNode, animated: false) - } + strongSelf.entries = entries } }) } @@ -373,5 +960,19 @@ class ThemeSettingsAccentColorItemNode: ListViewItemNode, ItemListItemNode { override func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } + + func prepareCrossfadeTransition() { + self.snapshotView?.removeFromSuperview() + + if let snapshotView = self.containerNode.view.snapshotView(afterScreenUpdates: false) { + self.view.insertSubview(snapshotView, aboveSubview: self.containerNode.view) + self.snapshotView = snapshotView + } + } + + func animateCrossfadeTransition() { + self.snapshotView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in + self?.snapshotView?.removeFromSuperview() + }) + } } - diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift index 89a7c23cb2..47b4bbda37 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift @@ -40,18 +40,20 @@ class ThemeSettingsChatPreviewItem: ListViewItem, ItemListItem { let strings: PresentationStrings let sectionId: ItemListSectionId let fontSize: PresentationFontSize + let chatBubbleCorners: PresentationChatBubbleCorners let wallpaper: TelegramWallpaper let dateTimeFormat: PresentationDateTimeFormat let nameDisplayOrder: PresentationPersonNameOrder let messageItems: [ChatPreviewMessageItem] - init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, fontSize: PresentationFontSize, wallpaper: TelegramWallpaper, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, messageItems: [ChatPreviewMessageItem]) { + init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, wallpaper: TelegramWallpaper, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, messageItems: [ChatPreviewMessageItem]) { self.context = context self.theme = theme self.componentTheme = componentTheme self.strings = strings self.sectionId = sectionId self.fontSize = fontSize + self.chatBubbleCorners = chatBubbleCorners self.wallpaper = wallpaper self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder @@ -102,6 +104,9 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { private var messageNodes: [ListViewItemNode]? private var item: ThemeSettingsChatPreviewItem? + private var finalImage = true + + private let disposable = MetaDisposable() init() { self.backgroundNode = ASImageNode() @@ -123,18 +128,24 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { super.init(layerBacked: false, dynamicBounce: false) + self.clipsToBounds = true + self.addSubnode(self.containerNode) } + deinit { + self.disposable.dispose() + } + func asyncLayout() -> (_ item: ThemeSettingsChatPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let currentItem = self.item let currentNodes = self.messageNodes return { item, params, neighbors in - var updatedBackgroundImage: UIImage? + var updatedBackgroundSignal: Signal<(UIImage?, Bool)?, NoError>? if currentItem?.wallpaper != item.wallpaper { - updatedBackgroundImage = chatControllerBackgroundImage(theme: item.theme, wallpaper: item.wallpaper, mediaBox: item.context.sharedContext.accountManager.mediaBox, knockoutMode: item.context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper) + updatedBackgroundSignal = chatControllerBackgroundImageSignal(wallpaper: item.wallpaper, mediaBox: item.context.sharedContext.accountManager.mediaBox, accountMediaBox: item.context.account.postbox.mediaBox) } let insets: UIEdgeInsets @@ -154,7 +165,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { } let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: messageItem.outgoing ? otherPeerId : peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: messageItem.outgoing ? TelegramUser(id: otherPeerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) : nil, text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, message: message, theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil)) + items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, message: message, theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil)) } var nodes: [ListViewItemNode] = [] @@ -214,8 +225,24 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { topOffset += node.frame.size.height } - if let updatedBackgroundImage = updatedBackgroundImage { - strongSelf.backgroundNode.image = updatedBackgroundImage + if let updatedBackgroundSignal = updatedBackgroundSignal { + strongSelf.disposable.set((updatedBackgroundSignal + |> deliverOnMainQueue).start(next: { [weak self] image in + if let strongSelf = self, let (image, final) = image { + if final && !strongSelf.finalImage { + let tempLayer = CALayer() + tempLayer.frame = strongSelf.backgroundNode.bounds + tempLayer.contentsGravity = strongSelf.backgroundNode.layer.contentsGravity + tempLayer.contents = strongSelf.contents + strongSelf.layer.addSublayer(tempLayer) + tempLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak tempLayer] _ in + tempLayer?.removeFromSuperlayer() + }) + } + strongSelf.backgroundNode.image = image + strongSelf.finalImage = final + } + })) } strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor @@ -258,8 +285,9 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.backgroundNode.frame = backgroundFrame.insetBy(dx: 0.0, dy: -100.0) + strongSelf.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index 9c90bff6f7..6f8357dfba 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -11,6 +11,7 @@ import ItemListUI import PresentationDataUtils import AlertUI import PresentationDataUtils +import MediaResources import WallpaperResources import ShareController import AccountContext @@ -71,16 +72,19 @@ private final class ThemeSettingsControllerArguments { let selectTheme: (PresentationThemeReference) -> Void let selectFontSize: (PresentationFontSize) -> Void let openWallpaperSettings: () -> Void - let selectAccentColor: (PresentationThemeAccentColor) -> Void - let openAccentColorPicker: (PresentationThemeReference, PresentationThemeAccentColor?) -> Void + let selectAccentColor: (PresentationThemeAccentColor?) -> Void + let openAccentColorPicker: (PresentationThemeReference, Bool) -> Void let openAutoNightTheme: () -> Void + let openTextSize: () -> Void + let openBubbleSettings: () -> Void let toggleLargeEmoji: (Bool) -> Void let disableAnimations: (Bool) -> Void let selectAppIcon: (String) -> Void let editTheme: (PresentationCloudTheme) -> Void - let contextAction: (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void + let themeContextAction: (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void + let colorContextAction: (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void - init(context: AccountContext, selectTheme: @escaping (PresentationThemeReference) -> Void, selectFontSize: @escaping (PresentationFontSize) -> Void, openWallpaperSettings: @escaping () -> Void, selectAccentColor: @escaping (PresentationThemeAccentColor) -> Void, openAccentColorPicker: @escaping (PresentationThemeReference, PresentationThemeAccentColor?) -> Void, openAutoNightTheme: @escaping () -> Void, toggleLargeEmoji: @escaping (Bool) -> Void, disableAnimations: @escaping (Bool) -> Void, selectAppIcon: @escaping (String) -> Void, editTheme: @escaping (PresentationCloudTheme) -> Void, contextAction: @escaping (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void) { + init(context: AccountContext, selectTheme: @escaping (PresentationThemeReference) -> Void, selectFontSize: @escaping (PresentationFontSize) -> Void, openWallpaperSettings: @escaping () -> Void, selectAccentColor: @escaping (PresentationThemeAccentColor?) -> Void, openAccentColorPicker: @escaping (PresentationThemeReference, Bool) -> Void, openAutoNightTheme: @escaping () -> Void, openTextSize: @escaping () -> Void, openBubbleSettings: @escaping () -> Void, toggleLargeEmoji: @escaping (Bool) -> Void, disableAnimations: @escaping (Bool) -> Void, selectAppIcon: @escaping (String) -> Void, editTheme: @escaping (PresentationCloudTheme) -> Void, themeContextAction: @escaping (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void, colorContextAction: @escaping (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void) { self.context = context self.selectTheme = selectTheme self.selectFontSize = selectFontSize @@ -88,11 +92,14 @@ private final class ThemeSettingsControllerArguments { self.selectAccentColor = selectAccentColor self.openAccentColorPicker = openAccentColorPicker self.openAutoNightTheme = openAutoNightTheme + self.openTextSize = openTextSize + self.openBubbleSettings = openBubbleSettings self.toggleLargeEmoji = toggleLargeEmoji self.disableAnimations = disableAnimations self.selectAppIcon = selectAppIcon self.editTheme = editTheme - self.contextAction = contextAction + self.themeContextAction = themeContextAction + self.colorContextAction = colorContextAction } } @@ -126,11 +133,13 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { case themeListHeader(PresentationTheme, String) case fontSizeHeader(PresentationTheme, String) case fontSize(PresentationTheme, PresentationFontSize) - case chatPreview(PresentationTheme, PresentationTheme, TelegramWallpaper, PresentationFontSize, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, [ChatPreviewMessageItem]) + case chatPreview(PresentationTheme, TelegramWallpaper, PresentationFontSize, PresentationChatBubbleCorners, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, [ChatPreviewMessageItem]) case wallpaper(PresentationTheme, String) - case accentColor(PresentationTheme, PresentationThemeReference, String, PresentationThemeAccentColor?) + case accentColor(PresentationTheme, PresentationThemeReference, PresentationThemeReference, [PresentationThemeReference], ThemeSettingsColorOption?) case autoNightTheme(PresentationTheme, String, String) - case themeItem(PresentationTheme, PresentationStrings, [PresentationThemeReference], PresentationThemeReference, [Int64: PresentationThemeAccentColor], PresentationThemeAccentColor?) + case textSize(PresentationTheme, String, String) + case bubbleSettings(PresentationTheme, String, String) + case themeItem(PresentationTheme, PresentationStrings, [PresentationThemeReference], [PresentationThemeReference], PresentationThemeReference, [Int64: PresentationThemeAccentColor], [Int64: TelegramWallpaper], PresentationThemeAccentColor?) case iconHeader(PresentationTheme, String) case iconItem(PresentationTheme, PresentationStrings, [PresentationAppIcon], String?) case otherHeader(PresentationTheme, String) @@ -144,7 +153,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { return ThemeSettingsControllerSection.chatPreview.rawValue case .fontSizeHeader, .fontSize: return ThemeSettingsControllerSection.fontSize.rawValue - case .wallpaper, .autoNightTheme: + case .wallpaper, .autoNightTheme, .textSize, .bubbleSettings: return ThemeSettingsControllerSection.background.rawValue case .iconHeader, .iconItem: return ThemeSettingsControllerSection.icon.rawValue @@ -167,29 +176,33 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { return 5 case .autoNightTheme: return 6 - case .fontSizeHeader: + case .textSize: return 7 - case .fontSize: + case .bubbleSettings: return 8 - case .iconHeader: + case .fontSizeHeader: return 9 - case .iconItem: + case .fontSize: return 10 - case .otherHeader: + case .iconHeader: return 11 - case .largeEmoji: + case .iconItem: return 12 - case .animations: + case .otherHeader: return 13 - case .animationsInfo: + case .largeEmoji: return 14 + case .animations: + return 15 + case .animationsInfo: + return 16 } } static func ==(lhs: ThemeSettingsControllerEntry, rhs: ThemeSettingsControllerEntry) -> Bool { switch lhs { - case let .chatPreview(lhsTheme, lhsComponentTheme, lhsWallpaper, lhsFontSize, lhsStrings, lhsTimeFormat, lhsNameOrder, lhsItems): - if case let .chatPreview(rhsTheme, rhsComponentTheme, rhsWallpaper, rhsFontSize, rhsStrings, rhsTimeFormat, rhsNameOrder, rhsItems) = rhs, lhsComponentTheme === rhsComponentTheme, lhsTheme === rhsTheme, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsStrings === rhsStrings, lhsTimeFormat == rhsTimeFormat, lhsNameOrder == rhsNameOrder, lhsItems == rhsItems { + case let .chatPreview(lhsTheme, lhsWallpaper, lhsFontSize, lhsChatBubbleCorners, lhsStrings, lhsTimeFormat, lhsNameOrder, lhsItems): + if case let .chatPreview(rhsTheme, rhsWallpaper, rhsFontSize, rhsChatBubbleCorners, rhsStrings, rhsTimeFormat, rhsNameOrder, rhsItems) = rhs, lhsTheme === rhsTheme, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsChatBubbleCorners == rhsChatBubbleCorners, lhsStrings === rhsStrings, lhsTimeFormat == rhsTimeFormat, lhsNameOrder == rhsNameOrder, lhsItems == rhsItems { return true } else { return false @@ -200,8 +213,8 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { } else { return false } - case let .accentColor(lhsTheme, lhsCurrentTheme, lhsText, lhsColor): - if case let .accentColor(rhsTheme, rhsCurrentTheme, rhsText, rhsColor) = rhs, lhsTheme === rhsTheme, lhsCurrentTheme == rhsCurrentTheme, lhsText == rhsText, lhsColor == rhsColor { + case let .accentColor(lhsTheme, lhsGeneralTheme, lhsCurrentTheme, lhsThemes, lhsColor): + if case let .accentColor(rhsTheme, rhsGeneralTheme, rhsCurrentTheme, rhsThemes, rhsColor) = rhs, lhsTheme === rhsTheme, lhsCurrentTheme == rhsCurrentTheme, lhsThemes == rhsThemes, lhsColor == rhsColor { return true } else { return false @@ -212,14 +225,26 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { } else { return false } + case let .textSize(lhsTheme, lhsText, lhsValue): + if case let .textSize(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .bubbleSettings(lhsTheme, lhsText, lhsValue): + if case let .bubbleSettings(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } case let .themeListHeader(lhsTheme, lhsText): if case let .themeListHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .themeItem(lhsTheme, lhsStrings, lhsThemes, lhsCurrentTheme, lhsThemeAccentColors, lhsCurrentColor): - if case let .themeItem(rhsTheme, rhsStrings, rhsThemes, rhsCurrentTheme, rhsThemeAccentColors, rhsCurrentColor) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsThemes == rhsThemes, lhsCurrentTheme == rhsCurrentTheme, lhsThemeAccentColors == rhsThemeAccentColors, lhsCurrentColor == rhsCurrentColor { + case let .themeItem(lhsTheme, lhsStrings, lhsThemes, lhsAllThemes, lhsCurrentTheme, lhsThemeAccentColors, lhsThemeSpecificChatWallpapers, lhsCurrentColor): + if case let .themeItem(rhsTheme, rhsStrings, rhsThemes, rhsAllThemes, rhsCurrentTheme, rhsThemeAccentColors, rhsThemeSpecificChatWallpapers, rhsCurrentColor) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsThemes == rhsThemes, lhsAllThemes == rhsAllThemes, lhsCurrentTheme == rhsCurrentTheme, lhsThemeAccentColors == rhsThemeAccentColors, lhsThemeSpecificChatWallpapers == rhsThemeSpecificChatWallpapers, lhsCurrentColor == rhsCurrentColor { return true } else { return false @@ -279,26 +304,55 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ThemeSettingsControllerArguments switch self { case let .fontSizeHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .fontSize(theme, fontSize): return ThemeSettingsFontSizeItem(theme: theme, fontSize: fontSize, sectionId: self.section, updated: { value in arguments.selectFontSize(value) }, tag: ThemeSettingsEntryTag.fontSize) - case let .chatPreview(theme, componentTheme, wallpaper, fontSize, strings, dateTimeFormat, nameDisplayOrder, items): - return ThemeSettingsChatPreviewItem(context: arguments.context, theme: theme, componentTheme: componentTheme, strings: strings, sectionId: self.section, fontSize: fontSize, wallpaper: wallpaper, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, messageItems: items) + case let .chatPreview(theme, wallpaper, fontSize, chatBubbleCorners, strings, dateTimeFormat, nameDisplayOrder, items): + return ThemeSettingsChatPreviewItem(context: arguments.context, theme: theme, componentTheme: theme, strings: strings, sectionId: self.section, fontSize: fontSize, chatBubbleCorners: chatBubbleCorners, wallpaper: wallpaper, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, messageItems: items) case let .wallpaper(theme, text): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openWallpaperSettings() }) - case let .accentColor(theme, currentTheme, _, color): - var defaultColor = PresentationThemeAccentColor(baseColor: .blue) + case let .accentColor(theme, generalThemeReference, currentTheme, themes, color): + var colorItems: [ThemeSettingsAccentColor] = [] + + for theme in themes { + colorItems.append(.theme(theme)) + } + + var defaultColor: PresentationThemeAccentColor? = PresentationThemeAccentColor(baseColor: .blue) var colors = PresentationThemeBaseColor.allCases - if case let .builtin(name) = currentTheme { - if name == .night || name == .nightAccent { + colors = colors.filter { $0 != .custom && $0 != .preset && $0 != .theme } + if case let .builtin(name) = generalThemeReference { + if name == .dayClassic { + colorItems.append(.default) + defaultColor = nil + + for preset in dayClassicColorPresets { + colorItems.append(.preset(preset)) + } + } else if name == .day { + colorItems.append(.color(.blue)) + colors = colors.filter { $0 != .blue } + + for preset in dayColorPresets { + colorItems.append(.preset(preset)) + } + } else if name == .night { + colorItems.append(.color(.blue)) + colors = colors.filter { $0 != .blue } + + for preset in nightColorPresets { + colorItems.append(.preset(preset)) + } + } + if name != .day { colors = colors.filter { $0 != .black } } if name == .night { @@ -308,24 +362,51 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { colors = colors.filter { $0 != .white } } } - let currentColor = color ?? defaultColor - if currentColor.baseColor != .custom { - colors = colors.filter { $0 != .custom } + var currentColor = color ?? defaultColor.flatMap { .accentColor($0) } + if let color = currentColor, case let .accentColor(accentColor) = color, accentColor.baseColor == .theme { + var themeExists = false + if let _ = themes.first(where: { $0.index == accentColor.themeIndex }) { + themeExists = true + } + if !themeExists { + currentColor = defaultColor.flatMap { .accentColor($0) } + } } - return ThemeSettingsAccentColorItem(theme: theme, sectionId: self.section, colors: colors, currentColor: currentColor, updated: { color in - arguments.selectAccentColor(color) - }, openColorPicker: { - arguments.openAccentColorPicker(currentTheme, currentColor) + colorItems.append(contentsOf: colors.map { .color($0) }) + + return ThemeSettingsAccentColorItem(theme: theme, sectionId: self.section, generalThemeReference: generalThemeReference, themeReference: currentTheme, colors: colorItems, currentColor: currentColor, updated: { color in + if let color = color { + switch color { + case let .accentColor(color): + arguments.selectAccentColor(color) + case let .theme(theme): + arguments.selectTheme(theme) + } + } else { + arguments.selectAccentColor(nil) + } + }, contextAction: { isCurrent, theme, color, node, gesture in + arguments.colorContextAction(isCurrent, theme, color, node, gesture) + }, openColorPicker: { create in + arguments.openAccentColorPicker(currentTheme, create) }, tag: ThemeSettingsEntryTag.accentColor) case let .autoNightTheme(theme, text, value): - return ItemListDisclosureItem(theme: theme, icon: nil, title: text, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, icon: nil, title: text, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openAutoNightTheme() }) + case let .textSize(theme, text, value): + return ItemListDisclosureItem(presentationData: presentationData, icon: nil, title: text, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.openTextSize() + }) + case let .bubbleSettings(theme, text, value): + return ItemListDisclosureItem(presentationData: presentationData, icon: nil, title: text, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.openBubbleSettings() + }) case let .themeListHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) - case let .themeItem(theme, strings, themes, currentTheme, themeSpecificAccentColors, _): - return ThemeSettingsThemeItem(context: arguments.context, theme: theme, strings: strings, sectionId: self.section, themes: themes, displayUnsupported: true, themeSpecificAccentColors: themeSpecificAccentColors, currentTheme: currentTheme, updatedTheme: { theme in - if case let .cloud(theme) = theme, theme.theme.file == nil { + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .themeItem(theme, strings, themes, allThemes, currentTheme, themeSpecificAccentColors, themeSpecificChatWallpapers, _): + return ThemeSettingsThemeItem(context: arguments.context, theme: theme, strings: strings, sectionId: self.section, themes: themes, allThemes: allThemes, displayUnsupported: true, themeSpecificAccentColors: themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, currentTheme: currentTheme, updatedTheme: { theme in + if case let .cloud(theme) = theme, theme.theme.file == nil && theme.theme.settings == nil { if theme.theme.isCreator { arguments.editTheme(theme) } @@ -333,47 +414,78 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { arguments.selectTheme(theme) } }, contextAction: { theme, node, gesture in - arguments.contextAction(theme.index == currentTheme.index, theme, node, gesture) - }) + arguments.themeContextAction(theme.index == currentTheme.index, theme, node, gesture) + }, tag: ThemeSettingsEntryTag.theme) case let .iconHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .iconItem(theme, strings, icons, value): return ThemeSettingsAppIconItem(theme: theme, strings: strings, sectionId: self.section, icons: icons, currentIconName: value, updated: { iconName in arguments.selectAppIcon(iconName) }) case let .otherHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .largeEmoji(theme, title, value): - return ItemListSwitchItem(theme: theme, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleLargeEmoji(value) }, tag: ThemeSettingsEntryTag.largeEmoji) case let .animations(theme, title, value): - return ItemListSwitchItem(theme: theme, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.disableAnimations(value) }, tag: ThemeSettingsEntryTag.animations) case let .animationsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } -private func themeSettingsControllerEntries(presentationData: PresentationData, theme: PresentationTheme, themeReference: PresentationThemeReference, themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], availableThemes: [PresentationThemeReference], autoNightSettings: AutomaticThemeSwitchSetting, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, dateTimeFormat: PresentationDateTimeFormat, largeEmoji: Bool, disableAnimations: Bool, availableAppIcons: [PresentationAppIcon], currentAppIconName: String?) -> [ThemeSettingsControllerEntry] { +private func themeSettingsControllerEntries(presentationData: PresentationData, presentationThemeSettings: PresentationThemeSettings, themeReference: PresentationThemeReference, availableThemes: [PresentationThemeReference], availableAppIcons: [PresentationAppIcon], currentAppIconName: String?) -> [ThemeSettingsControllerEntry] { var entries: [ThemeSettingsControllerEntry] = [] + let strings = presentationData.strings let title = presentationData.autoNightModeTriggered ? strings.Appearance_ColorThemeNight.uppercased() : strings.Appearance_ColorTheme.uppercased() entries.append(.themeListHeader(presentationData.theme, title)) - entries.append(.chatPreview(presentationData.theme, theme, wallpaper, fontSize, presentationData.strings, dateTimeFormat, presentationData.nameDisplayOrder, [ChatPreviewMessageItem(outgoing: false, reply: (presentationData.strings.Appearance_PreviewReplyAuthor, presentationData.strings.Appearance_PreviewReplyText), text: presentationData.strings.Appearance_PreviewIncomingText), ChatPreviewMessageItem(outgoing: true, reply: nil, text: presentationData.strings.Appearance_PreviewOutgoingText)])) + entries.append(.chatPreview(presentationData.theme, presentationData.chatWallpaper, presentationData.chatFontSize, presentationData.chatBubbleCorners, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, [ChatPreviewMessageItem(outgoing: false, reply: (presentationData.strings.Appearance_PreviewReplyAuthor, presentationData.strings.Appearance_PreviewReplyText), text: presentationData.strings.Appearance_PreviewIncomingText), ChatPreviewMessageItem(outgoing: true, reply: nil, text: presentationData.strings.Appearance_PreviewOutgoingText)])) - entries.append(.themeItem(presentationData.theme, presentationData.strings, availableThemes, themeReference, themeSpecificAccentColors, themeSpecificAccentColors[themeReference.index])) + let generalThemes: [PresentationThemeReference] = availableThemes.filter { reference in + if case let .cloud(theme) = reference { + return theme.theme.settings == nil + } else { + return true + } + } - if case let .builtin(theme) = themeReference, theme != .dayClassic { - entries.append(.accentColor(presentationData.theme, themeReference, strings.Appearance_AccentColor, themeSpecificAccentColors[themeReference.index])) + let generalThemeReference: PresentationThemeReference + if case let .cloud(theme) = themeReference, let settings = theme.theme.settings { + generalThemeReference = .builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)) + } else { + generalThemeReference = themeReference + } + + entries.append(.themeItem(presentationData.theme, presentationData.strings, generalThemes, availableThemes, themeReference, presentationThemeSettings.themeSpecificAccentColors, presentationThemeSettings.themeSpecificChatWallpapers, presentationThemeSettings.themeSpecificAccentColors[themeReference.index])) + + if case let .builtin(builtinTheme) = generalThemeReference { + let colorThemes = availableThemes.filter { reference in + if case let .cloud(theme) = reference, let settings = theme.theme.settings, settings.baseTheme == builtinTheme.baseTheme { + return true + } else { + return false + } + } + + var colorOption: ThemeSettingsColorOption? + if case let .builtin(theme) = themeReference { + colorOption = presentationThemeSettings.themeSpecificAccentColors[themeReference.index].flatMap { .accentColor($0) } + } else { + colorOption = .theme(themeReference) + } + + entries.append(.accentColor(presentationData.theme, generalThemeReference, themeReference, colorThemes, colorOption)) } entries.append(.wallpaper(presentationData.theme, strings.Settings_ChatBackground)) let autoNightMode: String - switch autoNightSettings.trigger { + switch presentationThemeSettings.automaticThemeSwitchSetting.trigger { case .system: if #available(iOSApplicationExtension 13.0, iOS 13.0, *) { autoNightMode = strings.AutoNightTheme_System @@ -389,8 +501,18 @@ private func themeSettingsControllerEntries(presentationData: PresentationData, } entries.append(.autoNightTheme(presentationData.theme, strings.Appearance_AutoNightTheme, autoNightMode)) - entries.append(.fontSizeHeader(presentationData.theme, strings.Appearance_TextSize.uppercased())) - entries.append(.fontSize(presentationData.theme, fontSize)) + let textSizeValue: String + if presentationThemeSettings.useSystemFont { + textSizeValue = strings.Appearance_TextSize_Automatic + } else { + if presentationThemeSettings.fontSize.baseDisplaySize == presentationThemeSettings.listsFontSize.baseDisplaySize { + textSizeValue = "\(Int(presentationThemeSettings.fontSize.baseDisplaySize))pt" + } else { + textSizeValue = "\(Int(presentationThemeSettings.fontSize.baseDisplaySize))pt / \(Int(presentationThemeSettings.listsFontSize.baseDisplaySize))pt" + } + } + entries.append(.textSize(presentationData.theme, strings.Appearance_TextSizeSetting, textSizeValue)) + entries.append(.bubbleSettings(presentationData.theme, strings.Appearance_BubbleCornersSetting, "")) if !availableAppIcons.isEmpty { entries.append(.iconHeader(presentationData.theme, strings.Appearance_AppIcon.uppercased())) @@ -398,20 +520,31 @@ private func themeSettingsControllerEntries(presentationData: PresentationData, } entries.append(.otherHeader(presentationData.theme, strings.Appearance_Other.uppercased())) - entries.append(.largeEmoji(presentationData.theme, strings.Appearance_LargeEmoji, largeEmoji)) - entries.append(.animations(presentationData.theme, strings.Appearance_ReduceMotion, disableAnimations)) + entries.append(.largeEmoji(presentationData.theme, strings.Appearance_LargeEmoji, presentationData.largeEmoji)) + entries.append(.animations(presentationData.theme, strings.Appearance_ReduceMotion, presentationData.disableAnimations)) entries.append(.animationsInfo(presentationData.theme, strings.Appearance_ReduceMotionInfo)) return entries } +public protocol ThemeSettingsController { + +} + +private final class ThemeSettingsControllerImpl: ItemListController, ThemeSettingsController { +} + public func themeSettingsController(context: AccountContext, focusOnItemTag: ThemeSettingsEntryTag? = nil) -> ViewController { var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? + var updateControllersImpl: ((([UIViewController]) -> [UIViewController]) -> Void)? var presentInGlobalOverlayImpl: ((ViewController, Any?) -> Void)? var getNavigationControllerImpl: (() -> NavigationController?)? + var presentCrossfadeControllerImpl: ((Bool) -> Void)? var selectThemeImpl: ((PresentationThemeReference) -> Void)? + var selectAccentColorImpl: ((PresentationThemeAccentColor?) -> Void)? + var openAccentColorPickerImpl: ((PresentationThemeReference, Bool) -> Void)? var moreImpl: (() -> Void)? let _ = telegramWallpapers(postbox: context.account.postbox, network: context.account.network).start() @@ -432,44 +565,41 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The let updatedCloudThemes = telegramThemes(postbox: context.account.postbox, network: context.account.network, accountManager: context.sharedContext.accountManager) cloudThemes.set(updatedCloudThemes) + let removedThemeIndexesPromise = Promise>(Set()) + let removedThemeIndexes = Atomic>(value: Set()) + let arguments = ThemeSettingsControllerArguments(context: context, selectTheme: { theme in selectThemeImpl?(theme) - }, selectFontSize: { size in - let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in - return PresentationThemeSettings(chatWallpaper: current.chatWallpaper, theme: current.theme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: current.themeSpecificChatWallpapers, fontSize: size, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) - }).start() + }, selectFontSize: { _ in }, openWallpaperSettings: { pushControllerImpl?(ThemeGridController(context: context)) - }, selectAccentColor: { color in - let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in - guard let theme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: current.theme, accentColor: color.color, serviceBackgroundColor: defaultServiceBackgroundColor, baseColor: color.baseColor) else { - return current - } - - var themeSpecificAccentColors = current.themeSpecificAccentColors - themeSpecificAccentColors[current.theme.index] = color - var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers - var chatWallpaper = current.chatWallpaper - if let wallpaper = current.themeSpecificChatWallpapers[current.theme.index], wallpaper.hasWallpaper { - } else { - chatWallpaper = theme.chat.defaultWallpaper - themeSpecificChatWallpapers[current.theme.index] = chatWallpaper - } - - return PresentationThemeSettings(chatWallpaper: chatWallpaper, theme: current.theme, themeSpecificAccentColors: themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) - }).start() - }, openAccentColorPicker: { themeReference, currentColor in - let controller = ThemeAccentColorController(context: context, currentTheme: themeReference, currentColor: currentColor?.color) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, selectAccentColor: { accentColor in + selectAccentColorImpl?(accentColor) + }, openAccentColorPicker: { themeReference, create in + openAccentColorPickerImpl?(themeReference, create) }, openAutoNightTheme: { pushControllerImpl?(themeAutoNightSettingsController(context: context)) + }, openTextSize: { + let _ = (context.sharedContext.accountManager.sharedData(keys: Set([ApplicationSpecificSharedDataKeys.presentationThemeSettings])) + |> take(1) + |> deliverOnMainQueue).start(next: { view in + let settings = (view.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings] as? PresentationThemeSettings) ?? PresentationThemeSettings.defaultSettings + pushControllerImpl?(TextSizeSelectionController(context: context, presentationThemeSettings: settings)) + }) + }, openBubbleSettings: { + let _ = (context.sharedContext.accountManager.sharedData(keys: Set([ApplicationSpecificSharedDataKeys.presentationThemeSettings])) + |> take(1) + |> deliverOnMainQueue).start(next: { view in + let settings = (view.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings] as? PresentationThemeSettings) ?? PresentationThemeSettings.defaultSettings + pushControllerImpl?(BubbleSettingsController(context: context, presentationThemeSettings: settings)) + }) }, toggleLargeEmoji: { largeEmoji in let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in - return PresentationThemeSettings(chatWallpaper: current.chatWallpaper, theme: current.theme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: current.themeSpecificChatWallpapers, fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: largeEmoji, disableAnimations: current.disableAnimations) + return current.withUpdatedLargeEmoji(largeEmoji) }).start() - }, disableAnimations: { value in + }, disableAnimations: { disableAnimations in let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in - return PresentationThemeSettings(chatWallpaper: current.chatWallpaper, theme: current.theme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: current.themeSpecificChatWallpapers, fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: value) + return current.withUpdatedDisableAnimations(disableAnimations) }).start() }, selectAppIcon: { name in currentAppIconName.set(name) @@ -482,29 +612,61 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The } }) pushControllerImpl?(controller) - }, contextAction: { isCurrent, reference, node, gesture in - let _ = (context.account.postbox.transaction { transaction in - return makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: reference, accentColor: nil, serviceBackgroundColor: defaultServiceBackgroundColor, baseColor: .blue) + }, themeContextAction: { isCurrent, reference, node, gesture in + let _ = (context.sharedContext.accountManager.transaction { transaction -> (PresentationThemeAccentColor?, TelegramWallpaper?) in + let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.presentationThemeSettings) as? PresentationThemeSettings ?? PresentationThemeSettings.defaultSettings + let accentColor = settings.themeSpecificAccentColors[reference.index] + var wallpaper: TelegramWallpaper? + if let accentColor = accentColor { + wallpaper = settings.themeSpecificChatWallpapers[coloredThemeIndex(reference: reference, accentColor: accentColor)] + } + if wallpaper == nil { + wallpaper = settings.themeSpecificChatWallpapers[reference.index] + } + return (accentColor, wallpaper) } - |> deliverOnMainQueue).start(next: { theme in + |> map { accentColor, wallpaper -> (PresentationThemeAccentColor?, TelegramWallpaper) in + let effectiveWallpaper: TelegramWallpaper + if let wallpaper = wallpaper { + effectiveWallpaper = wallpaper + } else { + let theme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: reference, accentColor: accentColor?.color, bubbleColors: accentColor?.customBubbleColors, wallpaper: accentColor?.wallpaper) + effectiveWallpaper = theme?.chat.defaultWallpaper ?? .builtin(WallpaperSettings()) + } + return (accentColor, effectiveWallpaper) + } + |> mapToSignal { accentColor, wallpaper -> Signal<(PresentationThemeAccentColor?, TelegramWallpaper), NoError> in + if case let .file(file) = wallpaper, file.id == 0 { + return cachedWallpaper(account: context.account, slug: file.slug, settings: file.settings) + |> map { cachedWallpaper in + if let wallpaper = cachedWallpaper?.wallpaper, case let .file(file) = wallpaper { + return (accentColor, wallpaper) + } else { + return (accentColor, .builtin(WallpaperSettings())) + } + } + } else { + return .single((accentColor, wallpaper)) + } + } + |> mapToSignal { accentColor, wallpaper -> Signal<(PresentationTheme?, TelegramWallpaper?), NoError> in + return chatServiceBackgroundColor(wallpaper: wallpaper, mediaBox: context.sharedContext.accountManager.mediaBox) + |> map { serviceBackgroundColor in + return (makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: reference, accentColor: accentColor?.color, bubbleColors: accentColor?.customBubbleColors, serviceBackgroundColor: serviceBackgroundColor), wallpaper) + } + } + |> deliverOnMainQueue).start(next: { theme, wallpaper in guard let theme = theme else { return } let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings - let themeController = ThemePreviewController(context: context, previewTheme: theme, source: .settings(reference)) + let themeController = ThemePreviewController(context: context, previewTheme: theme, source: .settings(reference, wallpaper)) var items: [ContextMenuItem] = [] - if !isCurrent { - items.append(.action(ContextMenuActionItem(text: strings.Theme_Context_Apply, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ApplyTheme"), color: theme.contextMenu.primaryColor) }, action: { c, f in - c.dismiss(completion: { - selectThemeImpl?(reference) - }) - }))) - } if case let .cloud(theme) = reference { if theme.theme.isCreator { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Appearance_EditTheme, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { c, f in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Appearance_EditTheme, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ApplyTheme"), color: theme.contextMenu.primaryColor) }, action: { c, f in let controller = editThemeController(context: context, mode: .edit(theme), navigateToChat: { peerId in if let navigationController = getNavigationControllerImpl?() { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId))) @@ -515,6 +677,48 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The pushControllerImpl?(controller) }) }))) + } else { + items.append(.action(ContextMenuActionItem(text: strings.Theme_Context_ChangeColors, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ApplyTheme"), color: theme.contextMenu.primaryColor) }, action: { c, f in + guard let theme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: reference, preview: false) else { + return + } + + let resolvedWallpaper: Signal + if case let .file(file) = theme.chat.defaultWallpaper, file.id == 0 { + resolvedWallpaper = cachedWallpaper(account: context.account, slug: file.slug, settings: file.settings) + |> map { cachedWallpaper -> TelegramWallpaper in + return cachedWallpaper?.wallpaper ?? theme.chat.defaultWallpaper + } + } else { + resolvedWallpaper = .single(theme.chat.defaultWallpaper) + } + + let _ = (resolvedWallpaper + |> deliverOnMainQueue).start(next: { wallpaper in + let controller = ThemeAccentColorController(context: context, mode: .edit(theme: theme, wallpaper: wallpaper, generalThemeReference: reference.generalThemeReference, defaultThemeReference: nil, create: true, completion: { result, settings in + let controller = editThemeController(context: context, mode: .create(result, nil), navigateToChat: { peerId in + if let navigationController = getNavigationControllerImpl?() { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId))) + } + }) + updateControllersImpl?({ controllers in + var controllers = controllers + controllers = controllers.filter { controller in + if controller is ThemeAccentColorController { + return false + } + return true + } + controllers.append(controller) + return controllers + }) + })) + + c.dismiss(completion: { + pushControllerImpl?(controller) + }) + }) + }))) } items.append(.action(ContextMenuActionItem(text: presentationData.strings.Appearance_ShareTheme, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { c, f in c.dismiss(completion: { @@ -524,47 +728,293 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The }))) items.append(.action(ContextMenuActionItem(text: presentationData.strings.Appearance_RemoveTheme, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { c, f in c.dismiss(completion: { - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: presentationData.strings.Appearance_RemoveThemeConfirmation, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() - let _ = (cloudThemes.get() |> delay(0.5, queue: Queue.mainQueue()) + let _ = (cloudThemes.get() |> take(1) |> deliverOnMainQueue).start(next: { themes in + removedThemeIndexesPromise.set(.single(removedThemeIndexes.modify({ value in + var updated = value + updated.insert(theme.theme.id) + return updated + }))) + if isCurrent, let currentThemeIndex = themes.firstIndex(where: { $0.id == theme.theme.id }) { - let previousThemeIndex = themes.prefix(upTo: currentThemeIndex).reversed().firstIndex(where: { $0.file != nil }) - let newTheme: PresentationThemeReference - if let previousThemeIndex = previousThemeIndex { - newTheme = .cloud(PresentationCloudTheme(theme: themes[themes.index(before: previousThemeIndex.base)], resolvedWallpaper: nil)) + if let settings = theme.theme.settings { + if settings.baseTheme == .night { + selectAccentColorImpl?(PresentationThemeAccentColor(baseColor: .blue)) + } else { + selectAccentColorImpl?(nil) + } } else { - newTheme = .builtin(.nightAccent) + let previousThemeIndex = themes.prefix(upTo: currentThemeIndex).reversed().firstIndex(where: { $0.file != nil }) + let newTheme: PresentationThemeReference + if let previousThemeIndex = previousThemeIndex { + let theme = themes[themes.index(before: previousThemeIndex.base)] + newTheme = .cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: theme.isCreator ? context.account.id : nil)) + } else { + newTheme = .builtin(.nightAccent) + } + selectThemeImpl?(newTheme) } - selectThemeImpl?(newTheme) } let _ = deleteThemeInteractively(account: context.account, accountManager: context.sharedContext.accountManager, theme: theme.theme).start() }) })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) presentControllerImpl?(actionSheet, nil) }) }))) + } else { + items.append(.action(ContextMenuActionItem(text: strings.Theme_Context_ChangeColors, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ApplyTheme"), color: theme.contextMenu.primaryColor) + }, action: { c, f in + c.dismiss(completion: { + let controller = ThemeAccentColorController(context: context, mode: .colors(themeReference: reference, create: true)) + pushControllerImpl?(controller) + }) + }))) } - let contextController = ContextController(account: context.account, theme: presentationData.theme, strings: presentationData.strings, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) + presentInGlobalOverlayImpl?(contextController, nil) + }) + }, colorContextAction: { isCurrent, reference, accentColor, node, gesture in + let _ = (context.sharedContext.accountManager.transaction { transaction -> (ThemeSettingsColorOption?, TelegramWallpaper?) in + let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.presentationThemeSettings) as? PresentationThemeSettings ?? PresentationThemeSettings.defaultSettings + var wallpaper: TelegramWallpaper? + if let accentColor = accentColor { + switch accentColor { + case let .accentColor(accentColor): + wallpaper = settings.themeSpecificChatWallpapers[coloredThemeIndex(reference: reference, accentColor: accentColor)] + if wallpaper == nil { + wallpaper = settings.themeSpecificChatWallpapers[reference.index] + } + case let .theme(theme): + wallpaper = settings.themeSpecificChatWallpapers[coloredThemeIndex(reference: theme, accentColor: nil)] + } + } else if wallpaper == nil { + wallpaper = settings.themeSpecificChatWallpapers[reference.index] + } + return (accentColor, wallpaper) + } |> mapToSignal { accentColor, wallpaper -> Signal<(PresentationTheme?, PresentationThemeReference, Bool, TelegramWallpaper?), NoError> in + let generalThemeReference: PresentationThemeReference + if let accentColor = accentColor, case let .cloud(theme) = reference, let settings = theme.theme.settings { + generalThemeReference = .builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)) + } else { + generalThemeReference = reference + } + + let effectiveWallpaper: TelegramWallpaper + let effectiveThemeReference: PresentationThemeReference + if let accentColor = accentColor, case let .theme(themeReference) = accentColor { + effectiveThemeReference = themeReference + } else { + effectiveThemeReference = reference + } + + if let wallpaper = wallpaper { + effectiveWallpaper = wallpaper + } else { + let theme: PresentationTheme? + if let accentColor = accentColor, case let .theme(themeReference) = accentColor { + theme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: themeReference) + } else { + theme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: generalThemeReference, accentColor: accentColor?.accentColor, bubbleColors: accentColor?.customBubbleColors, wallpaper: accentColor?.wallpaper) + } + effectiveWallpaper = theme?.chat.defaultWallpaper ?? .builtin(WallpaperSettings()) + } + + let wallpaperSignal: Signal + if case let .file(file) = effectiveWallpaper, file.id == 0 { + wallpaperSignal = cachedWallpaper(account: context.account, slug: file.slug, settings: file.settings) + |> map { cachedWallpaper in + return cachedWallpaper?.wallpaper ?? effectiveWallpaper + } + } else { + wallpaperSignal = .single(effectiveWallpaper) + } + + return wallpaperSignal + |> mapToSignal { wallpaper in + return chatServiceBackgroundColor(wallpaper: wallpaper, mediaBox: context.sharedContext.accountManager.mediaBox) + |> map { serviceBackgroundColor in + return (wallpaper, serviceBackgroundColor) + } + } + |> map { wallpaper, serviceBackgroundColor -> (PresentationTheme?, PresentationThemeReference, TelegramWallpaper) in + if let accentColor = accentColor, case let .theme(themeReference) = accentColor { + return (makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: themeReference, serviceBackgroundColor: serviceBackgroundColor), effectiveThemeReference, wallpaper) + } else { + return (makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: generalThemeReference, accentColor: accentColor?.accentColor, bubbleColors: accentColor?.customBubbleColors, serviceBackgroundColor: serviceBackgroundColor), effectiveThemeReference, wallpaper) + } + } + |> mapToSignal { theme, reference, wallpaper in + if case let .cloud(info) = reference { + return cloudThemes.get() + |> take(1) + |> map { themes -> Bool in + if let _ = themes.first(where: { $0.id == info.theme.id }) { + return true + } else { + return false + } + } + |> map { cloudThemeExists -> (PresentationTheme?, PresentationThemeReference, Bool, TelegramWallpaper) in + return (theme, reference, cloudThemeExists, wallpaper) + } + } else { + return .single((theme, reference, false, wallpaper)) + } + } + } + |> deliverOnMainQueue).start(next: { theme, effectiveThemeReference, cloudThemeExists, wallpaper in + guard let theme = theme else { + return + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + let themeController = ThemePreviewController(context: context, previewTheme: theme, source: .settings(effectiveThemeReference, wallpaper)) + var items: [ContextMenuItem] = [] + + if let accentColor = accentColor { + if case let .accentColor(color) = accentColor, color.baseColor != .custom { + } else if case let .theme(theme) = accentColor, case let .cloud(cloudTheme) = theme { + if cloudTheme.theme.isCreator && cloudThemeExists { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Appearance_EditTheme, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ApplyTheme"), color: theme.contextMenu.primaryColor) }, action: { c, f in + let controller = editThemeController(context: context, mode: .edit(cloudTheme), navigateToChat: { peerId in + if let navigationController = getNavigationControllerImpl?() { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId))) + } + }) + + c.dismiss(completion: { + pushControllerImpl?(controller) + }) + }))) + } else { + items.append(.action(ContextMenuActionItem(text: strings.Theme_Context_ChangeColors, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ApplyTheme"), color: theme.contextMenu.primaryColor) }, action: { c, f in + guard let theme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: reference, preview: false) else { + return + } + + let resolvedWallpaper: Signal + if case let .file(file) = theme.chat.defaultWallpaper, file.id == 0 { + resolvedWallpaper = cachedWallpaper(account: context.account, slug: file.slug, settings: file.settings) + |> map { cachedWallpaper -> TelegramWallpaper in + return cachedWallpaper?.wallpaper ?? theme.chat.defaultWallpaper + } + } else { + resolvedWallpaper = .single(theme.chat.defaultWallpaper) + } + + let _ = (resolvedWallpaper + |> deliverOnMainQueue).start(next: { wallpaper in + let controller = ThemeAccentColorController(context: context, mode: .edit(theme: theme, wallpaper: wallpaper, generalThemeReference: reference.generalThemeReference, defaultThemeReference: nil, create: true, completion: { result, settings in + let controller = editThemeController(context: context, mode: .create(result, nil), navigateToChat: { peerId in + if let navigationController = getNavigationControllerImpl?() { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId))) + } + }) + updateControllersImpl?({ controllers in + var controllers = controllers + controllers = controllers.filter { controller in + if controller is ThemeAccentColorController { + return false + } + return true + } + controllers.append(controller) + return controllers + }) + })) + + c.dismiss(completion: { + pushControllerImpl?(controller) + }) + }) + }))) + } + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Appearance_ShareTheme, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { c, f in + c.dismiss(completion: { + let controller = ShareController(context: context, subject: .url("https://t.me/addtheme/\(cloudTheme.theme.slug)"), preferredAction: .default) + presentControllerImpl?(controller, nil) + }) + }))) + if cloudThemeExists { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Appearance_RemoveTheme, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { c, f in + c.dismiss(completion: { + let actionSheet = ActionSheetController(presentationData: presentationData) + var items: [ActionSheetItem] = [] + items.append(ActionSheetButtonItem(title: presentationData.strings.Appearance_RemoveThemeConfirmation, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + let _ = (cloudThemes.get() + |> take(1) + |> deliverOnMainQueue).start(next: { themes in + removedThemeIndexesPromise.set(.single(removedThemeIndexes.modify({ value in + var updated = value + updated.insert(cloudTheme.theme.id) + return updated + }))) + + if isCurrent, let settings = cloudTheme.theme.settings { + let colorThemes = themes.filter { theme in + if let settings = theme.settings { + return true + } else { + return false + } + } + + if let currentThemeIndex = colorThemes.firstIndex(where: { $0.id == cloudTheme.theme.id }) { + let previousThemeIndex = themes.prefix(upTo: currentThemeIndex).reversed().firstIndex(where: { $0.file != nil }) + let newTheme: PresentationThemeReference + if let previousThemeIndex = previousThemeIndex { + let theme = themes[themes.index(before: previousThemeIndex.base)] + selectThemeImpl?(.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: theme.isCreator ? context.account.id : nil))) + } else { + if settings.baseTheme == .night { + selectAccentColorImpl?(PresentationThemeAccentColor(baseColor: .blue)) + } else { + selectAccentColorImpl?(nil) + } + } + } + } + + let _ = deleteThemeInteractively(account: context.account, accountManager: context.sharedContext.accountManager, theme: cloudTheme.theme).start() + }) + })) + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + presentControllerImpl?(actionSheet, nil) + }) + }))) + } + } + } + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) presentInGlobalOverlayImpl?(contextController, nil) }) }) - let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings]), cloudThemes.get(), availableAppIcons, currentAppIconName.get()) - |> map { presentationData, sharedData, cloudThemes, availableAppIcons, currentAppIconName -> (ItemListControllerState, (ItemListNodeState, Any)) in + let previousThemeReference = Atomic(value: nil) + let previousAccentColor = Atomic(value: nil) + + let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings]), cloudThemes.get(), availableAppIcons, currentAppIconName.get(), removedThemeIndexesPromise.get()) + |> map { presentationData, sharedData, cloudThemes, availableAppIcons, currentAppIconName, removedThemeIndexes -> (ItemListControllerState, (ItemListNodeState, Any)) in let settings = (sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings] as? PresentationThemeSettings) ?? PresentationThemeSettings.defaultSettings - let fontSize = settings.fontSize let dateTimeFormat = presentationData.dateTimeFormat let largeEmoji = presentationData.largeEmoji let disableAnimations = presentationData.disableAnimations @@ -576,11 +1026,9 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The themeReference = settings.theme } - let theme = presentationData.theme - let accentColor = settings.themeSpecificAccentColors[themeReference.index]?.color - let wallpaper = settings.themeSpecificChatWallpapers[themeReference.index] ?? settings.chatWallpaper + let accentColor = settings.themeSpecificAccentColors[themeReference.index] - let rightNavigationButton = ItemListNavigationButton(content: .icon(.action), style: .regular, enabled: true, action: { + let rightNavigationButton = ItemListNavigationButton(content: .icon(.add), style: .regular, enabled: true, action: { moreImpl?() }) @@ -591,7 +1039,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The } defaultThemes.append(contentsOf: [.builtin(.night), .builtin(.nightAccent)]) - let cloudThemes: [PresentationThemeReference] = cloudThemes.map { .cloud(PresentationCloudTheme(theme: $0, resolvedWallpaper: nil)) } + let cloudThemes: [PresentationThemeReference] = cloudThemes.map { .cloud(PresentationCloudTheme(theme: $0, resolvedWallpaper: nil, creatorAccountId: $0.isCreator ? context.account.id : nil)) }.filter { !removedThemeIndexes.contains($0.index) } var availableThemes = defaultThemes if defaultThemes.first(where: { $0.index == themeReference.index }) == nil && cloudThemes.first(where: { $0.index == themeReference.index }) == nil { @@ -599,13 +1047,13 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The } availableThemes.append(contentsOf: cloudThemes) - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Appearance_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: themeSettingsControllerEntries(presentationData: presentationData, theme: theme, themeReference: themeReference, themeSpecificAccentColors: settings.themeSpecificAccentColors, availableThemes: availableThemes, autoNightSettings: settings.automaticThemeSwitchSetting, strings: presentationData.strings, wallpaper: wallpaper, fontSize: fontSize, dateTimeFormat: dateTimeFormat, largeEmoji: largeEmoji, disableAnimations: disableAnimations, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) - + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Appearance_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeSettingsControllerEntries(presentationData: presentationData, presentationThemeSettings: settings, themeReference: themeReference, availableThemes: availableThemes, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) + return (controllerState, (listState, arguments)) } - let controller = ItemListController(context: context, state: signal) + let controller = ThemeSettingsControllerImpl(context: context, state: signal) controller.alwaysSynchronous = true pushControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.pushViewController(c) @@ -613,14 +1061,85 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a, blockInteraction: true) } + updateControllersImpl = { [weak controller] f in + if let navigationController = controller?.navigationController as? NavigationController { + navigationController.setViewControllers(f(navigationController.viewControllers), animated: true) + } + } presentInGlobalOverlayImpl = { [weak controller] c, a in controller?.presentInGlobalOverlay(c, with: a) } getNavigationControllerImpl = { [weak controller] in return controller?.navigationController as? NavigationController } + presentCrossfadeControllerImpl = { [weak controller] hasAccentColors in + if let controller = controller, controller.isNodeLoaded, let navigationController = controller.navigationController as? NavigationController, navigationController.topViewController === controller { + var topOffset: CGFloat? + var bottomOffset: CGFloat? + var leftOffset: CGFloat? + var themeItemNode: ThemeSettingsThemeItemNode? + var colorItemNode: ThemeSettingsAccentColorItemNode? + + var view: UIView? + if #available(iOS 11.0, *) { + view = controller.navigationController?.view + } + + let controllerFrame = controller.view.convert(controller.view.bounds, to: controller.navigationController?.view) + if controllerFrame.minX > 0.0 { + leftOffset = controllerFrame.minX + } + if controllerFrame.minY > 100.0 { + view = nil + } + + controller.forEachItemNode { node in + if let itemNode = node as? ItemListItemNode { + if let itemTag = itemNode.tag { + if itemTag.isEqual(to: ThemeSettingsEntryTag.theme) { + let frame = node.view.convert(node.view.bounds, to: controller.navigationController?.view) + topOffset = frame.minY + bottomOffset = frame.maxY + if let itemNode = node as? ThemeSettingsThemeItemNode { + themeItemNode = itemNode + } + } else if itemTag.isEqual(to: ThemeSettingsEntryTag.accentColor) && hasAccentColors { + let frame = node.view.convert(node.view.bounds, to: controller.navigationController?.view) + bottomOffset = frame.maxY + if let itemNode = node as? ThemeSettingsAccentColorItemNode { + colorItemNode = itemNode + } + } + } + } + } + + if let navigationBar = controller.navigationBar { + if let offset = topOffset { + topOffset = max(offset, navigationBar.frame.maxY) + } else { + topOffset = navigationBar.frame.maxY + } + } + + if view != nil { + themeItemNode?.prepareCrossfadeTransition() + colorItemNode?.prepareCrossfadeTransition() + } + + let crossfadeController = ThemeSettingsCrossfadeController(view: view, topOffset: topOffset, bottomOffset: bottomOffset, leftOffset: leftOffset) + crossfadeController.didAppear = { [weak themeItemNode, weak colorItemNode] in + if view != nil { + themeItemNode?.animateCrossfadeTransition() + colorItemNode?.animateCrossfadeTransition() + } + } + + context.sharedContext.presentGlobalController(crossfadeController, nil) + } + } selectThemeImpl = { theme in - guard let presentationTheme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: theme, accentColor: nil, serviceBackgroundColor: .black, baseColor: nil) else { + guard let presentationTheme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: theme) else { return } @@ -642,58 +1161,177 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The } let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: cloudTheme).start() - let _ = (resolvedWallpaper - |> mapToSignal { resolvedWallpaper -> Signal in - var updatedTheme = theme - if case let .cloud(info) = theme { - updatedTheme = .cloud(PresentationCloudTheme(theme: info.theme, resolvedWallpaper: resolvedWallpaper)) + let currentTheme = context.sharedContext.accountManager.transaction { transaction -> (PresentationThemeReference) in + let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.presentationThemeSettings) as? PresentationThemeSettings ?? PresentationThemeSettings.defaultSettings + if autoNightModeTriggered { + return settings.automaticThemeSwitchSetting.theme + } else { + return settings.theme } - return (context.sharedContext.accountManager.transaction { transaction -> Void in - transaction.updateSharedData(ApplicationSpecificSharedDataKeys.presentationThemeSettings, { entry in - let current: PresentationThemeSettings - if let entry = entry as? PresentationThemeSettings { - current = entry - } else { - current = PresentationThemeSettings.defaultSettings - } + } + + let _ = (combineLatest(resolvedWallpaper, currentTheme) + |> map { resolvedWallpaper, currentTheme -> Bool in + var updatedTheme = theme + var currentThemeBaseIndex: Int64? + if case let .cloud(info) = currentTheme, let settings = info.theme.settings { + currentThemeBaseIndex = PresentationThemeReference.builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)).index + } else { + currentThemeBaseIndex = currentTheme.index + } + + var baseThemeIndex: Int64? + var updatedThemeBaseIndex: Int64? + if case let .cloud(info) = theme { + updatedTheme = .cloud(PresentationCloudTheme(theme: info.theme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: info.theme.isCreator ? context.account.id : nil)) + if let settings = info.theme.settings { + baseThemeIndex = PresentationThemeReference.builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)).index + updatedThemeBaseIndex = baseThemeIndex + } + } else { + updatedThemeBaseIndex = theme.index + } + + let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + var updatedThemeSpecificAccentColors = current.themeSpecificAccentColors + if let baseThemeIndex = baseThemeIndex { + updatedThemeSpecificAccentColors[baseThemeIndex] = PresentationThemeAccentColor(themeIndex: updatedTheme.index) + } + + if autoNightModeTriggered { + var updatedAutomaticThemeSwitchSetting = current.automaticThemeSwitchSetting + updatedAutomaticThemeSwitchSetting.theme = updatedTheme - var theme = current.theme - var automaticThemeSwitchSetting = current.automaticThemeSwitchSetting - if autoNightModeTriggered { - automaticThemeSwitchSetting.theme = updatedTheme + return current.withUpdatedAutomaticThemeSwitchSetting(updatedAutomaticThemeSwitchSetting).withUpdatedThemeSpecificAccentColors(updatedThemeSpecificAccentColors) + } else { + return current.withUpdatedTheme(updatedTheme).withUpdatedThemeSpecificAccentColors(updatedThemeSpecificAccentColors) + } + }).start() + + return currentThemeBaseIndex != updatedThemeBaseIndex + } |> deliverOnMainQueue).start(next: { crossfadeAccentColors in + presentCrossfadeControllerImpl?((cloudTheme == nil || cloudTheme?.settings != nil) && !crossfadeAccentColors) + }) + } + openAccentColorPickerImpl = { [weak controller] themeReference, create in + if let _ = controller?.navigationController?.viewControllers.first(where: { $0 is ThemeAccentColorController }) { + return + } + let controller = ThemeAccentColorController(context: context, mode: .colors(themeReference: themeReference, create: create)) + pushControllerImpl?(controller) + } + selectAccentColorImpl = { accentColor in + var wallpaperSignal: Signal = .single(nil) + if let colorWallpaper = accentColor?.wallpaper, case let .file(file) = colorWallpaper { + wallpaperSignal = cachedWallpaper(account: context.account, slug: file.slug, settings: colorWallpaper.settings) + |> mapToSignal { cachedWallpaper in + if let wallpaper = cachedWallpaper?.wallpaper, case let .file(file) = wallpaper { + let resource = file.file.resource + let representation = CachedPatternWallpaperRepresentation(color: file.settings.color ?? 0xd6e2ee, bottomColor: file.settings.bottomColor, intensity: file.settings.intensity ?? 50, rotation: file.settings.rotation) + + let _ = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)).start() + + let _ = (context.account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: false, fetch: true) + |> filter({ $0.complete })).start(next: { data in + if data.complete, let path = context.account.postbox.mediaBox.completedResourcePath(resource) { + if let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: maybeData, synchronous: true) + } + if let maybeData = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: .mappedRead) { + context.sharedContext.accountManager.mediaBox.storeCachedResourceRepresentation(resource, representation: representation, data: maybeData) + } + } + }) + return .single(wallpaper) + + } else { + return .single(nil) + } + } + } + + let _ = (wallpaperSignal + |> deliverOnMainQueue).start(next: { presetWallpaper in + let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + let autoNightModeTriggered = context.sharedContext.currentPresentationData.with { $0 }.autoNightModeTriggered + var currentTheme = current.theme + if autoNightModeTriggered { + currentTheme = current.automaticThemeSwitchSetting.theme + } + + let generalThemeReference: PresentationThemeReference + if case let .cloud(theme) = currentTheme, let settings = theme.theme.settings { + generalThemeReference = .builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)) + } else { + generalThemeReference = currentTheme + } + + currentTheme = generalThemeReference + var updatedTheme = current.theme + var updatedAutomaticThemeSwitchSetting = current.automaticThemeSwitchSetting + + if autoNightModeTriggered { + updatedAutomaticThemeSwitchSetting.theme = generalThemeReference + } else { + updatedTheme = generalThemeReference + } + + guard let theme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: generalThemeReference, accentColor: accentColor?.color, wallpaper: presetWallpaper) else { + return current + } + + var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers + var themeSpecificAccentColors = current.themeSpecificAccentColors + themeSpecificAccentColors[generalThemeReference.index] = accentColor?.withUpdatedWallpaper(presetWallpaper) + + if case let .builtin(theme) = generalThemeReference { + let index = coloredThemeIndex(reference: currentTheme, accentColor: accentColor) + if let wallpaper = current.themeSpecificChatWallpapers[index] { + if wallpaper.isColorOrGradient || wallpaper.isPattern || wallpaper.isBuiltin { + themeSpecificChatWallpapers[index] = presetWallpaper + } } else { - theme = updatedTheme + themeSpecificChatWallpapers[index] = presetWallpaper } - - let chatWallpaper: TelegramWallpaper - if let themeSpecificWallpaper = current.themeSpecificChatWallpapers[updatedTheme.index] { - chatWallpaper = themeSpecificWallpaper - } else { - let presentationTheme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: updatedTheme, accentColor: current.themeSpecificAccentColors[updatedTheme.index]?.color, serviceBackgroundColor: .black, baseColor: nil) ?? defaultPresentationTheme - chatWallpaper = resolvedWallpaper ?? presentationTheme.chat.defaultWallpaper - } - - return PresentationThemeSettings(chatWallpaper: chatWallpaper, theme: theme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: current.themeSpecificChatWallpapers, fontSize: current.fontSize, automaticThemeSwitchSetting: automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) - }) - }) - }).start() + } + + return PresentationThemeSettings(theme: updatedTheme, themeSpecificAccentColors: themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, useSystemFont: current.useSystemFont, fontSize: current.fontSize, listsFontSize: current.listsFontSize, chatBubbleSettings: current.chatBubbleSettings, automaticThemeSwitchSetting: updatedAutomaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) + }).start() + + presentCrossfadeControllerImpl?(true) + }) } moreImpl = { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: presentationData.strings.Appearance_CreateTheme, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() - let controller = editThemeController(context: context, mode: .create, navigateToChat: { peerId in - if let navigationController = getNavigationControllerImpl?() { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId))) + let _ = (context.sharedContext.accountManager.transaction { transaction -> PresentationThemeReference in + let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.presentationThemeSettings) as? PresentationThemeSettings ?? PresentationThemeSettings.defaultSettings + + let themeReference: PresentationThemeReference + let autoNightModeTriggered = context.sharedContext.currentPresentationData.with { $0 }.autoNightModeTriggered + if autoNightModeTriggered { + themeReference = settings.automaticThemeSwitchSetting.theme + } else { + themeReference = settings.theme } + + return themeReference + } + |> deliverOnMainQueue).start(next: { themeReference in + let controller = editThemeController(context: context, mode: .create(nil, nil), navigateToChat: { peerId in + if let navigationController = getNavigationControllerImpl?() { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId))) + } + }) + pushControllerImpl?(controller) }) - pushControllerImpl?(controller) })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -703,11 +1341,70 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The } public final class ThemeSettingsCrossfadeController: ViewController { - private let snapshotView: UIView? + private var snapshotView: UIView? - public init() { - self.snapshotView = UIScreen.main.snapshotView(afterScreenUpdates: false) - + private var topSnapshotView: UIView? + private var bottomSnapshotView: UIView? + private var sideSnapshotView: UIView? + + fileprivate var didAppear: (() -> Void)? + + public init(view: UIView? = nil, topOffset: CGFloat? = nil, bottomOffset: CGFloat? = nil, leftOffset: CGFloat? = nil) { + if let view = view { + if var leftOffset = leftOffset { + leftOffset += UIScreenPixel + + if let view = view.snapshotView(afterScreenUpdates: false) { + let clipView = UIView() + clipView.clipsToBounds = true + clipView.addSubview(view) + + view.clipsToBounds = true + view.contentMode = .topLeft + + if let topOffset = topOffset, let bottomOffset = bottomOffset { + var frame = view.frame + frame.origin.y = topOffset + frame.size.width = leftOffset + frame.size.height = bottomOffset - topOffset + clipView.frame = frame + + frame = view.frame + frame.origin.y = -topOffset + frame.size.width = leftOffset + frame.size.height = bottomOffset + view.frame = frame + } + + self.sideSnapshotView = clipView + } + } + + if let view = view.snapshotView(afterScreenUpdates: false) { + view.clipsToBounds = true + view.contentMode = .top + if let topOffset = topOffset { + var frame = view.frame + frame.size.height = topOffset + view.frame = frame + } + self.topSnapshotView = view + } + + if let view = view.snapshotView(afterScreenUpdates: false) { + view.clipsToBounds = true + view.contentMode = .bottom + if let bottomOffset = bottomOffset { + var frame = view.frame + frame.origin.y = bottomOffset + frame.size.height -= bottomOffset + view.frame = frame + } + self.bottomSnapshotView = view + } + } else { + self.snapshotView = UIScreen.main.snapshotView(afterScreenUpdates: false) + } super.init(navigationBarPresentationData: nil) self.statusBar.statusBarStyle = .Ignore @@ -726,6 +1423,15 @@ public final class ThemeSettingsCrossfadeController: ViewController { if let snapshotView = self.snapshotView { self.displayNode.view.addSubview(snapshotView) } + if let topSnapshotView = self.topSnapshotView { + self.displayNode.view.addSubview(topSnapshotView) + } + if let bottomSnapshotView = self.bottomSnapshotView { + self.displayNode.view.addSubview(bottomSnapshotView) + } + if let sideSnapshotView = self.sideSnapshotView { + self.displayNode.view.addSubview(sideSnapshotView) + } } override public func viewDidAppear(_ animated: Bool) { @@ -734,5 +1440,7 @@ public final class ThemeSettingsCrossfadeController: ViewController { self.displayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in self?.presentingViewController?.dismiss(animated: false, completion: nil) }) + + self.didAppear?() } } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsFontSizeItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsFontSizeItem.swift index 3e97f00deb..61b07eabff 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsFontSizeItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsFontSizeItem.swift @@ -15,13 +15,21 @@ import AppBundle class ThemeSettingsFontSizeItem: ListViewItem, ItemListItem { let theme: PresentationTheme let fontSize: PresentationFontSize + let disableLeadingInset: Bool + let displayIcons: Bool + let force: Bool + let enabled: Bool let sectionId: ItemListSectionId let updated: (PresentationFontSize) -> Void let tag: ItemListItemTag? - init(theme: PresentationTheme, fontSize: PresentationFontSize, sectionId: ItemListSectionId, updated: @escaping (PresentationFontSize) -> Void, tag: ItemListItemTag? = nil) { + init(theme: PresentationTheme, fontSize: PresentationFontSize, enabled: Bool = true, disableLeadingInset: Bool = false, displayIcons: Bool = true, force: Bool = false, sectionId: ItemListSectionId, updated: @escaping (PresentationFontSize) -> Void, tag: ItemListItemTag? = nil) { self.theme = theme self.fontSize = fontSize + self.enabled = enabled + self.disableLeadingInset = disableLeadingInset + self.displayIcons = displayIcons + self.force = force self.sectionId = sectionId self.updated = updated self.tag = tag @@ -79,6 +87,7 @@ class ThemeSettingsFontSizeItemNode: ListViewItemNode, ItemListItemNode { private var sliderView: TGPhotoEditorSliderView? private let leftIconNode: ASImageNode private let rightIconNode: ASImageNode + private let disabledOverlayNode: ASDisplayNode private var item: ThemeSettingsFontSizeItem? private var layoutParams: ListViewItemLayoutParams? @@ -107,10 +116,14 @@ class ThemeSettingsFontSizeItemNode: ListViewItemNode, ItemListItemNode { self.rightIconNode.displaysAsynchronously = false self.rightIconNode.displayWithoutProcessing = true + self.disabledOverlayNode = ASDisplayNode() + super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.leftIconNode) self.addSubnode(self.rightIconNode) + + self.addSubnode(self.disabledOverlayNode) } override func didLoad() { @@ -128,6 +141,8 @@ class ThemeSettingsFontSizeItemNode: ListViewItemNode, ItemListItemNode { sliderView.positionsCount = 7 sliderView.disablesInteractiveTransitionGestureRecognizer = true if let item = self.item, let params = self.layoutParams { + sliderView.isUserInteractionEnabled = item.enabled + let value: CGFloat switch item.fontSize { case .extraSmall: @@ -148,12 +163,14 @@ class ThemeSettingsFontSizeItemNode: ListViewItemNode, ItemListItemNode { sliderView.value = value sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor sliderView.backColor = item.theme.list.disclosureArrowColor - sliderView.trackColor = item.theme.list.itemAccentColor + sliderView.trackColor = item.enabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor sliderView.knobImage = generateKnobImage() - sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 38.0, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 38.0 * 2.0, height: 44.0)) + let sliderInset: CGFloat = item.displayIcons ? 38.0 : 16.0 + + sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + sliderInset, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - sliderInset * 2.0, height: 44.0)) } - self.view.addSubview(sliderView) + self.view.insertSubview(sliderView, belowSubview: self.disabledOverlayNode.view) sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) self.sliderView = sliderView } @@ -174,17 +191,23 @@ class ThemeSettingsFontSizeItemNode: ListViewItemNode, ItemListItemNode { } let contentSize: CGSize - let insets: UIEdgeInsets + var insets: UIEdgeInsets let separatorHeight = UIScreenPixel contentSize = CGSize(width: params.width, height: 60.0) insets = itemListNeighborsGroupedInsets(neighbors) + if item.disableLeadingInset { + insets.top = 0.0 + insets.bottom = 0.0 + } + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size return (layout, { [weak self] in if let strongSelf = self { + let firstTime = strongSelf.item == nil || item.force strongSelf.item = item strongSelf.layoutParams = params @@ -192,6 +215,10 @@ class ThemeSettingsFontSizeItemNode: ListViewItemNode, ItemListItemNode { strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.disabledOverlayNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4) + strongSelf.disabledOverlayNode.isHidden = item.enabled + strongSelf.disabledOverlayNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: 44.0)) + if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) } @@ -248,15 +275,42 @@ class ThemeSettingsFontSizeItemNode: ListViewItemNode, ItemListItemNode { strongSelf.rightIconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 14.0 - image.size.width, y: 21.0), size: CGSize(width: image.size.width, height: image.size.height)) } + strongSelf.leftIconNode.isHidden = !item.displayIcons + strongSelf.rightIconNode.isHidden = !item.displayIcons + if let sliderView = strongSelf.sliderView { + sliderView.isUserInteractionEnabled = item.enabled + sliderView.trackColor = item.enabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor + if themeUpdated { sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor sliderView.backColor = item.theme.list.disclosureArrowColor - sliderView.trackColor = item.theme.list.itemAccentColor sliderView.knobImage = generateKnobImage() } - sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 38.0, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 38.0 * 2.0, height: 44.0)) + let value: CGFloat + switch item.fontSize { + case .extraSmall: + value = 0.0 + case .small: + value = 1.0 + case .medium: + value = 2.0 + case .regular: + value = 3.0 + case .large: + value = 4.0 + case .extraLarge: + value = 5.0 + case .extraLargeX2: + value = 6.0 + } + if firstTime { + sliderView.value = value + } + + let sliderInset: CGFloat = item.displayIcons ? 38.0 : 16.0 + sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + sliderInset, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - sliderInset * 2.0, height: 44.0)) } } }) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsThemeItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsThemeItem.swift index f6e90cc483..70366675ee 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsThemeItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsThemeItem.swift @@ -7,6 +7,7 @@ import Postbox import TelegramCore import SyncCore import TelegramPresentationData +import MergeLists import TelegramUIPreferences import ItemListUI import PresentationDataUtils @@ -15,41 +16,155 @@ import AccountContext import AppBundle import ContextUI -private var borderImages: [String: UIImage] = [:] +private struct ThemeSettingsThemeEntry: Comparable, Identifiable { + let index: Int + let themeReference: PresentationThemeReference + let title: String + let accentColor: PresentationThemeAccentColor? + var selected: Bool + let theme: PresentationTheme + let wallpaper: TelegramWallpaper? + + var stableId: Int64 { + return self.themeReference.generalThemeReference.index + } + + static func ==(lhs: ThemeSettingsThemeEntry, rhs: ThemeSettingsThemeEntry) -> Bool { + if lhs.index != rhs.index { + return false + } + if lhs.themeReference.index != rhs.themeReference.index { + return false + } + if lhs.accentColor != rhs.accentColor { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.selected != rhs.selected { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.wallpaper != rhs.wallpaper { + return false + } + return true + } + + static func <(lhs: ThemeSettingsThemeEntry, rhs: ThemeSettingsThemeEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(context: AccountContext, action: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem { + return ThemeSettingsThemeIconItem(context: context, themeReference: self.themeReference, accentColor: self.accentColor, selected: self.selected, title: self.title, theme: self.theme, wallpaper: self.wallpaper, action: action, contextAction: contextAction) + } +} + +private class ThemeSettingsThemeIconItem: ListViewItem { + let context: AccountContext + let themeReference: PresentationThemeReference + let accentColor: PresentationThemeAccentColor? + let selected: Bool + let title: String + let theme: PresentationTheme + let wallpaper: TelegramWallpaper? + let action: (PresentationThemeReference) -> Void + let contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)? + + public init(context: AccountContext, themeReference: PresentationThemeReference, accentColor: PresentationThemeAccentColor?, selected: Bool, title: String, theme: PresentationTheme, wallpaper: TelegramWallpaper?, action: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?) { + self.context = context + self.themeReference = themeReference + self.accentColor = accentColor + self.selected = selected + self.title = title + self.theme = theme + self.wallpaper = wallpaper + self.action = action + self.contextAction = contextAction + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = ThemeSettingsThemeItemIconNode() + let (nodeLayout, apply) = node.asyncLayout()(self, params) + node.insets = nodeLayout.insets + node.contentSize = nodeLayout.contentSize + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in + apply(false) + }) + }) + } + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + assert(node() is ThemeSettingsThemeItemIconNode) + if let nodeValue = node() as? ThemeSettingsThemeItemIconNode { + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params) + Queue.mainQueue().async { + completion(nodeLayout, { _ in + apply(animation.isAnimated) + }) + } + } + } + } + } + + public var selectable = true + public func selected(listView: ListView) { + self.action(self.themeReference) + } +} + + +private let textFont = Font.regular(12.0) +private let selectedTextFont = Font.bold(12.0) + +private var cachedBorderImages: [String: UIImage] = [:] private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selected: Bool) -> UIImage? { let key = "\(theme.list.itemBlocksBackgroundColor.hexString)_\(selected ? "s" + theme.list.itemAccentColor.hexString : theme.list.disclosureArrowColor.hexString)" - if let image = borderImages[key] { + if let image = cachedBorderImages[key] { return image } else { let image = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) - context.setFillColor(theme.list.itemBlocksBackgroundColor.cgColor) - context.fill(bounds) - - context.setBlendMode(.clear) - context.fillEllipse(in: bounds.insetBy(dx: 1.0, dy: 1.0)) - context.setBlendMode(.normal) - + context.clear(bounds) + let lineWidth: CGFloat if selected { + lineWidth = 2.0 + context.setLineWidth(lineWidth) + context.setStrokeColor(theme.list.itemBlocksBackgroundColor.cgColor) + + context.strokeEllipse(in: bounds.insetBy(dx: 3.0 + lineWidth / 2.0, dy: 3.0 + lineWidth / 2.0)) + var accentColor = theme.list.itemAccentColor if accentColor.rgb == 0xffffff { accentColor = UIColor(rgb: 0x999999) } context.setStrokeColor(accentColor.cgColor) - lineWidth = 2.0 } else { context.setStrokeColor(theme.list.disclosureArrowColor.withAlphaComponent(0.4).cgColor) lineWidth = 1.0 } - + if bordered || selected { context.setLineWidth(lineWidth) context.strokeEllipse(in: bounds.insetBy(dx: 1.0 + lineWidth / 2.0, dy: 1.0 + lineWidth / 2.0)) } })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 16) - borderImages[key] = image + cachedBorderImages[key] = image return image } } @@ -58,12 +173,11 @@ private func createThemeImage(theme: PresentationTheme) -> Signal<(TransformImag return .single(theme) |> map { theme -> (TransformImageArguments) -> DrawingContext? in return { arguments in - let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: false) + let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true) let drawingRect = arguments.drawingRect context.withContext { c in - c.setFillColor(theme.list.itemBlocksBackgroundColor.cgColor) - c.fill(drawingRect) + c.clear(CGRect(origin: CGPoint(), size: drawingRect.size)) c.translateBy(x: drawingRect.width / 2.0, y: drawingRect.height / 2.0) c.scaleBy(x: 1.0, y: -1.0) @@ -73,49 +187,203 @@ private func createThemeImage(theme: PresentationTheme) -> Signal<(TransformImag c.draw(icon.cgImage!, in: CGRect(origin: CGPoint(x: floor((drawingRect.width - icon.size.width) / 2.0) - 3.0, y: floor((drawingRect.height - icon.size.height) / 2.0)), size: icon.size)) } } - + addCorners(context, arguments: arguments) return context } } } +private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { + private let containerNode: ContextControllerSourceNode + private let imageNode: TransformImageNode + private let overlayNode: ASImageNode + private let titleNode: TextNode + var snapshotView: UIView? + + var item: ThemeSettingsThemeIconItem? + + init() { + self.containerNode = ContextControllerSourceNode() + + self.imageNode = TransformImageNode() + self.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 98.0, height: 62.0)) + self.imageNode.isLayerBacked = true + + self.overlayNode = ASImageNode() + self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 100.0, height: 64.0)) + self.overlayNode.isLayerBacked = true + + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.imageNode) + self.containerNode.addSubnode(self.overlayNode) + self.containerNode.addSubnode(self.titleNode) + + self.containerNode.activated = { [weak self] gesture in + guard let strongSelf = self, let item = strongSelf.item else { + gesture.cancel() + return + } + item.contextAction?(item.themeReference, strongSelf.containerNode, gesture) + } + } + + override func didLoad() { + super.didLoad() + + self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + } + + func asyncLayout() -> (ThemeSettingsThemeIconItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeImageLayout = self.imageNode.asyncLayout() + + let currentItem = self.item + + return { [weak self] item, params in + var updatedThemeReference = false + var updatedAccentColor = false + var updatedTheme = false + var updatedSelected = false + + if currentItem?.themeReference != item.themeReference { + updatedThemeReference = true + } + if currentItem == nil || currentItem?.accentColor != item.accentColor { + updatedAccentColor = true + } + if currentItem?.theme !== item.theme { + updatedTheme = true + } + if currentItem?.selected != item.selected { + updatedSelected = true + } + + let title = NSAttributedString(string: item.title, font: item.selected ? selectedTextFont : textFont, textColor: item.selected ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 116.0, height: 116.0), insets: UIEdgeInsets()) + return (itemLayout, { animated in + if let strongSelf = self { + strongSelf.item = item + + if case let .cloud(theme) = item.themeReference, theme.theme.file == nil && theme.theme.settings == nil { + if updatedTheme { + strongSelf.imageNode.setSignal(createThemeImage(theme: item.theme)) + } + strongSelf.containerNode.isGestureEnabled = false + } else { + if updatedThemeReference || updatedAccentColor { + strongSelf.imageNode.setSignal(themeIconImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, theme: item.themeReference, color: item.accentColor, wallpaper: item.wallpaper)) + } + strongSelf.containerNode.isGestureEnabled = true + } + if updatedTheme || updatedSelected { + strongSelf.overlayNode.image = generateBorderImage(theme: item.theme, bordered: true, selected: item.selected) + } + + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: itemLayout.contentSize) + + let _ = titleApply() + + let imageSize = CGSize(width: 98.0, height: 62.0) + strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: 10.0, y: 14.0), size: imageSize) + let applyLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: .clear)) + applyLayout() + + strongSelf.overlayNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 13.0), size: CGSize(width: 100.0, height: 64.0)) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 88.0), size: CGSize(width: itemLayout.contentSize.width, height: 16.0)) + } + }) + } + } + + func prepareCrossfadeTransition() { + guard self.snapshotView == nil else { + return + } + + if let snapshotView = self.containerNode.view.snapshotView(afterScreenUpdates: false) { + self.view.insertSubview(snapshotView, aboveSubview: self.containerNode.view) + self.snapshotView = snapshotView + } + } + + func animateCrossfadeTransition() { + guard self.snapshotView?.layer.animationKeys()?.isEmpty ?? true else { + return + } + + self.snapshotView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in + self?.snapshotView?.removeFromSuperview() + self?.snapshotView = nil + }) + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + super.animateRemoved(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + super.animateAdded(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } +} + class ThemeSettingsThemeItem: ListViewItem, ItemListItem { var sectionId: ItemListSectionId - + let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let themes: [PresentationThemeReference] + let allThemes: [PresentationThemeReference] let displayUnsupported: Bool let themeSpecificAccentColors: [Int64: PresentationThemeAccentColor] + let themeSpecificChatWallpapers: [Int64: TelegramWallpaper] let currentTheme: PresentationThemeReference let updatedTheme: (PresentationThemeReference) -> Void let contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)? let tag: ItemListItemTag? - - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, themes: [PresentationThemeReference], displayUnsupported: Bool, themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], currentTheme: PresentationThemeReference, updatedTheme: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?, tag: ItemListItemTag? = nil) { + + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, themes: [PresentationThemeReference], allThemes: [PresentationThemeReference], displayUnsupported: Bool, themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], currentTheme: PresentationThemeReference, updatedTheme: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?, tag: ItemListItemTag? = nil) { self.context = context self.theme = theme self.strings = strings self.themes = themes + self.allThemes = allThemes self.displayUnsupported = displayUnsupported self.themeSpecificAccentColors = themeSpecificAccentColors + self.themeSpecificChatWallpapers = themeSpecificChatWallpapers self.currentTheme = currentTheme self.updatedTheme = updatedTheme self.contextAction = contextAction self.tag = tag self.sectionId = sectionId } - + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = ThemeSettingsThemeItemNode() let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) - + node.contentSize = layout.contentSize node.insets = layout.insets - + Queue.mainQueue().async { completion(node, { return (nil, { _ in apply() }) @@ -123,12 +391,12 @@ class ThemeSettingsThemeItem: ListViewItem, ItemListItem { } } } - + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { if let nodeValue = node() as? ThemeSettingsThemeItemNode { let makeLayout = nodeValue.asyncLayout() - + async { let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { @@ -142,214 +410,163 @@ class ThemeSettingsThemeItem: ListViewItem, ItemListItem { } } -private final class ThemeSettingsThemeItemIconNode : ASDisplayNode { - private let containerNode: ContextControllerSourceNode - private let imageNode: TransformImageNode - private let overlayNode: ASImageNode - private let textNode: ASTextNode - private var action: (() -> Void)? - private var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? +private struct ThemeSettingsThemeItemNodeTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let crossfade: Bool + let entries: [ThemeSettingsThemeEntry] +} + +private func preparedTransition(context: AccountContext, action: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?, from fromEntries: [ThemeSettingsThemeEntry], to toEntries: [ThemeSettingsThemeEntry], crossfade: Bool) -> ThemeSettingsThemeItemNodeTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) - private var theme: PresentationThemeReference? - private var currentTheme: PresentationTheme? - private var accentColor: UIColor? - private var bordered: Bool? - private var selected: Bool? + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, action: action, contextAction: contextAction), directionHint: .Down) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, action: action, contextAction: contextAction), directionHint: nil) } - override init() { - self.containerNode = ContextControllerSourceNode() - - self.imageNode = TransformImageNode() - self.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 98.0, height: 62.0)) - self.imageNode.isLayerBacked = true - - self.overlayNode = ASImageNode() - self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 100.0, height: 64.0)) - self.overlayNode.isLayerBacked = true - - self.textNode = ASTextNode() - self.textNode.isUserInteractionEnabled = false - self.textNode.displaysAsynchronously = true - - super.init() - - self.addSubnode(self.containerNode) - self.containerNode.addSubnode(self.imageNode) - self.containerNode.addSubnode(self.overlayNode) - self.containerNode.addSubnode(self.textNode) - - self.containerNode.activated = { [weak self] gesture in - guard let strongSelf = self else { - gesture.cancel() - return - } - strongSelf.contextAction?(strongSelf.containerNode, gesture) - } - } - - func setup(context: AccountContext, theme: PresentationThemeReference, accentColor: UIColor?, currentTheme: PresentationTheme, title: NSAttributedString, bordered: Bool, selected: Bool, action: @escaping () -> Void, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?) { - let updatedTheme = self.currentTheme == nil || currentTheme !== self.currentTheme! - var contextActionEnabled = true - if case let .cloud(theme) = theme, theme.theme.file == nil { - if updatedTheme || accentColor != self.accentColor { - self.imageNode.setSignal(createThemeImage(theme: currentTheme)) - self.currentTheme = currentTheme - self.accentColor = accentColor - contextActionEnabled = false - } - } else { - if theme != self.theme || accentColor != self.accentColor { - self.imageNode.setSignal(themeIconImage(account: context.account, accountManager: context.sharedContext.accountManager, theme: theme, accentColor: accentColor)) - self.theme = theme - self.accentColor = accentColor + return ThemeSettingsThemeItemNodeTransition(deletions: deletions, insertions: insertions, updates: updates, crossfade: crossfade, entries: toEntries) +} + +private func ensureThemeVisible(listNode: ListView, themeReference: PresentationThemeReference, animated: Bool) -> Bool { + var resultNode: ThemeSettingsThemeItemIconNode? + listNode.forEachItemNode { node in + if resultNode == nil, let node = node as? ThemeSettingsThemeItemIconNode { + if node.item?.themeReference.index == themeReference.index { + resultNode = node } } - if updatedTheme || bordered != self.bordered || selected != self.selected { - self.overlayNode.image = generateBorderImage(theme: currentTheme, bordered: bordered, selected: selected) - self.currentTheme = currentTheme - self.bordered = bordered - self.selected = selected - } - self.textNode.attributedText = title - self.action = action - self.contextAction = contextAction - self.containerNode.isGestureEnabled = contextActionEnabled } - - override func didLoad() { - super.didLoad() - - let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) - recognizer.delaysTouchesBegan = false - recognizer.tapActionAtPoint = { point in - return .waitForSingleTap - } - self.view.addGestureRecognizer(recognizer) - } - - @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { - switch recognizer.state { - case .ended: - if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { - switch gesture { - case .tap: - self.action?() - default: - break - } - } - default: - break - } - } - - override func layout() { - super.layout() - - let bounds = self.bounds - - self.containerNode.frame = CGRect(origin: CGPoint(), size: bounds.size) - - let imageSize = CGSize(width: 98.0, height: 62.0) - self.imageNode.frame = CGRect(origin: CGPoint(x: 10.0, y: 14.0), size: imageSize) - let makeLayout = self.imageNode.asyncLayout() - let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: .clear)) - applyLayout() - - self.overlayNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 13.0), size: CGSize(width: 100.0, height: 64.0)) - self.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 14.0 + 60.0 + 4.0 + 9.0), size: CGSize(width: bounds.size.width, height: 16.0)) + if let resultNode = resultNode { + listNode.ensureItemNodeVisible(resultNode, animated: animated, overflow: 57.0) + return true + } else { + return false } } - -private let textFont = Font.regular(12.0) -private let selectedTextFont = Font.bold(12.0) - class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { + private let containerNode: ASDisplayNode private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let maskNode: ASImageNode + private var snapshotView: UIView? - private let scrollNode: ASScrollNode - private var nodes: [ThemeSettingsThemeItemIconNode] = [] - + private let listNode: ListView + private var entries: [ThemeSettingsThemeEntry]? + private var enqueuedTransitions: [ThemeSettingsThemeItemNodeTransition] = [] + private var initialized = false + private var item: ThemeSettingsThemeItem? private var layoutParams: ListViewItemLayoutParams? - + var tag: ItemListItemTag? { return self.item?.tag } - + init() { + self.containerNode = ASDisplayNode() + self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - + self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true - + self.bottomStripeNode = ASDisplayNode() self.bottomStripeNode.isLayerBacked = true - + self.maskNode = ASImageNode() - - self.scrollNode = ASScrollNode() - + + self.listNode = ListView() + self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + super.init(layerBacked: false, dynamicBounce: false) - - self.addSubnode(self.scrollNode) + + self.addSubnode(self.containerNode) + self.addSubnode(self.listNode) } - + override func didLoad() { super.didLoad() - self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true - self.scrollNode.view.showsHorizontalScrollIndicator = false + self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true } - private func scrollToNode(_ node: ThemeSettingsThemeItemIconNode, animated: Bool) { - let bounds = self.scrollNode.view.bounds - let frame = node.frame.insetBy(dx: -48.0, dy: 0.0) + private func enqueueTransition(_ transition: ThemeSettingsThemeItemNodeTransition) { + self.enqueuedTransitions.append(transition) - if frame.minX < bounds.minX || frame.maxX > bounds.maxX { - self.scrollNode.view.scrollRectToVisible(frame, animated: animated) + if let _ = self.item { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } } } + private func dequeueTransition() { + guard let item = self.item, let transition = self.enqueuedTransitions.first else { + return + } + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + if self.initialized && transition.crossfade { + options.insert(.AnimateCrossfade) + } + options.insert(.Synchronous) + + var scrollToItem: ListViewScrollToItem? + if !self.initialized { + if let index = transition.entries.firstIndex(where: { entry in + return entry.theme.index == item.currentTheme.index + }) { + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(-57.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down) + self.initialized = true + } + } + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in + }) + } + func asyncLayout() -> (_ item: ThemeSettingsThemeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + return { item, params, neighbors in let contentSize: CGSize let insets: UIEdgeInsets let separatorHeight = UIScreenPixel - + contentSize = CGSize(width: params.width, height: 116.0) insets = itemListNeighborsGroupedInsets(neighbors) - + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size - + return (layout, { [weak self] in if let strongSelf = self { + let isFirstLayout = currentItem == nil + strongSelf.item = item strongSelf.layoutParams = params - - strongSelf.scrollNode.view.contentInset = UIEdgeInsets(top: 0.0, left: params.leftInset, bottom: 0.0, right: params.rightInset) + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - + if strongSelf.backgroundNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + strongSelf.containerNode.insertSubnode(strongSelf.backgroundNode, at: 0) } if strongSelf.topStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + strongSelf.containerNode.insertSubnode(strongSelf.topStripeNode, at: 1) } if strongSelf.bottomStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + strongSelf.containerNode.insertSubnode(strongSelf.bottomStripeNode, at: 2) } if strongSelf.maskNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + strongSelf.containerNode.insertSubnode(strongSelf.maskNode, at: 3) } - + let hasCorners = itemListHasRoundedBlockLayout(params) var hasTopCorners = false var hasBottomCorners = false @@ -372,79 +589,60 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { hasBottomCorners = true strongSelf.bottomStripeNode.isHidden = hasCorners } - + + strongSelf.containerNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: contentSize.height) strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil - + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + + var listInsets = UIEdgeInsets() + listInsets.top += params.leftInset + 4.0 + listInsets.bottom += params.rightInset + 4.0 - strongSelf.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: CGSize(width: layoutSize.width, height: layoutSize.height)) + strongSelf.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width) + strongSelf.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0 + 2.0) + strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - let nodeInset: CGFloat = 4.0 - let nodeSize = CGSize(width: 116.0, height: 112.0) - var nodeOffset = nodeInset + var themes: [Int64: PresentationThemeReference] = [:] + for theme in item.allThemes { + themes[theme.index] = theme + } - var updated = false - var selectedNode: ThemeSettingsThemeItemIconNode? - - var i = 0 - for theme in item.themes { + var entries: [ThemeSettingsThemeEntry] = [] + var index: Int = 0 + for var theme in item.themes { if !item.displayUnsupported, case let .cloud(theme) = theme, theme.theme.file == nil { continue } - - let imageNode: ThemeSettingsThemeItemIconNode - if strongSelf.nodes.count > i { - imageNode = strongSelf.nodes[i] - } else { - imageNode = ThemeSettingsThemeItemIconNode() - strongSelf.nodes.append(imageNode) - strongSelf.scrollNode.addSubnode(imageNode) - updated = true - } - - let selected = theme.index == item.currentTheme.index - if selected { - selectedNode = imageNode - } - - let name = themeDisplayName(strings: item.strings, reference: theme) - imageNode.setup(context: item.context, theme: theme, accentColor: item.themeSpecificAccentColors[theme.index]?.color, currentTheme: item.theme, title: NSAttributedString(string: name, font: selected ? selectedTextFont : textFont, textColor: selected ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center), bordered: true, selected: selected, action: { [weak self, weak imageNode] in - item.updatedTheme(theme) - if let imageNode = imageNode { - self?.scrollToNode(imageNode, animated: true) + let title = themeDisplayName(strings: item.strings, reference: theme) + var accentColor = item.themeSpecificAccentColors[theme.generalThemeReference.index] + if let customThemeIndex = accentColor?.themeIndex { + if let customTheme = themes[customThemeIndex] { + theme = customTheme } - }, contextAction: item.contextAction.flatMap { - contextAction in - return { node, gesture in - contextAction(theme, node, gesture) - } - }) + accentColor = nil + } - imageNode.frame = CGRect(origin: CGPoint(x: nodeOffset, y: 0.0), size: nodeSize) - nodeOffset += nodeSize.width + 2.0 - - i += 1 + let wallpaper = accentColor?.wallpaper + entries.append(ThemeSettingsThemeEntry(index: index, themeReference: theme, title: title, accentColor: accentColor, selected: item.currentTheme.index == theme.index, theme: item.theme, wallpaper: wallpaper)) + index += 1 } - for k in (i ..< strongSelf.nodes.count).reversed() { - let node = strongSelf.nodes[k] - strongSelf.nodes.remove(at: k) - node.removeFromSupernode() - } - - if let lastNode = strongSelf.nodes.last { - let contentSize = CGSize(width: lastNode.frame.maxX + nodeInset, height: strongSelf.scrollNode.frame.height) - if strongSelf.scrollNode.view.contentSize != contentSize { - strongSelf.scrollNode.view.contentSize = contentSize + let action: (PresentationThemeReference) -> Void = { [weak self, weak item] themeReference in + if let strongSelf = self { + strongSelf.item?.updatedTheme(themeReference) + ensureThemeVisible(listNode: strongSelf.listNode, themeReference: themeReference, animated: true) } } + let previousEntries = strongSelf.entries ?? [] + let crossfade = previousEntries.count != entries.count + let transition = preparedTransition(context: item.context, action: action, contextAction: item.contextAction, from: previousEntries, to: entries, crossfade: crossfade) + strongSelf.enqueueTransition(transition) - if updated, let selectedNode = selectedNode { - strongSelf.scrollToNode(selectedNode, animated: false) - } + strongSelf.entries = entries } }) } @@ -453,8 +651,56 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } - + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } + + func prepareCrossfadeTransition() { + guard self.snapshotView == nil else { + return + } + + if let snapshotView = self.containerNode.view.snapshotView(afterScreenUpdates: false) { + self.view.insertSubview(snapshotView, aboveSubview: self.containerNode.view) + self.snapshotView = snapshotView + } + + self.listNode.forEachVisibleItemNode { node in + if let node = node as? ThemeSettingsThemeItemIconNode { + node.prepareCrossfadeTransition() + } + } + } + + func animateCrossfadeTransition() { + guard self.snapshotView?.layer.animationKeys()?.isEmpty ?? true else { + return + } + + var views: [UIView] = [] + if let snapshotView = self.snapshotView { + views.append(snapshotView) + self.snapshotView = nil + } + + self.listNode.forEachVisibleItemNode { node in + if let node = node as? ThemeSettingsThemeItemIconNode { + if let snapshotView = node.snapshotView { + views.append(snapshotView) + node.snapshotView = nil + } + } + } + + UIView.animate(withDuration: 0.3, animations: { + for view in views { + view.alpha = 0.0 + } + }, completion: { _ in + for view in views { + view.removeFromSuperview() + } + }) + } } diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperColorPanelNode.swift b/submodules/SettingsUI/Sources/Themes/WallpaperColorPanelNode.swift index 3e0a1dc81a..3bb307b4ab 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperColorPanelNode.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperColorPanelNode.swift @@ -31,74 +31,108 @@ private func textInputBackgroundImage(fieldColor: UIColor, strokeColor: UIColor, } } -final class WallpaperColorPanelNode: ASDisplayNode, UITextFieldDelegate { +private func generateSwatchBorderImage(theme: PresentationTheme) -> UIImage? { + return nil + return generateImage(CGSize(width: 21.0, height: 21.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + context.setLineWidth(1.0) + context.setStrokeColor(theme.chat.inputPanel.inputControlColor.cgColor) + context.strokeEllipse(in: bounds.insetBy(dx: 1.0, dy: 1.0)) + }) +} + +private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { private var theme: PresentationTheme - private let backgroundNode: ASDisplayNode - private let topSeparatorNode: ASDisplayNode - private let bottomSeparatorNode: ASDisplayNode + private let swatchNode: ASDisplayNode + private let borderNode: ASImageNode + private let removeButton: HighlightableButtonNode private let textBackgroundNode: ASImageNode - private let textFieldNode: TextFieldNode + private let selectionNode: ASDisplayNode + let textFieldNode: TextFieldNode + private let measureNode: ImmediateTextNode private let prefixNode: ASTextNode - private let doneButton: HighlightableButtonNode - private let colorPickerNode: WallpaperColorPickerNode - var previousColor: UIColor? - var color: UIColor { - get { - return self.colorPickerNode.color - } - set { - self.setColor(newValue) + private var gestureRecognizer: UITapGestureRecognizer? + + var colorChanged: ((UIColor, Bool) -> Void)? + var colorRemoved: (() -> Void)? + var colorSelected: (() -> Void)? + + private var color: UIColor? + + private var isDefault = false { + didSet { + self.updateSelectionVisibility() } } - - var colorChanged: ((UIColor, Bool) -> Void)? - - init(theme: PresentationTheme, strings: PresentationStrings) { + + var isRemovable: Bool = false { + didSet { + self.removeButton.isUserInteractionEnabled = self.isRemovable + } + } + + var isSelected: Bool = false { + didSet { + self.updateSelectionVisibility() + self.gestureRecognizer?.isEnabled = !self.isSelected + if !self.isSelected { + self.textFieldNode.textField.resignFirstResponder() + } + } + } + + private var previousIsDefault: Bool? + private var previousColor: UIColor? + private var validLayout: (CGSize, Bool)? + + private var skipEndEditing = false + + init(theme: PresentationTheme) { self.theme = theme - self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = theme.chat.inputPanel.panelBackgroundColor - - self.topSeparatorNode = ASDisplayNode() - self.topSeparatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor - self.bottomSeparatorNode = ASDisplayNode() - self.bottomSeparatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor - self.textBackgroundNode = ASImageNode() self.textBackgroundNode.image = textInputBackgroundImage(fieldColor: theme.chat.inputPanel.inputBackgroundColor, strokeColor: theme.chat.inputPanel.inputStrokeColor, diameter: 33.0) self.textBackgroundNode.displayWithoutProcessing = true self.textBackgroundNode.displaysAsynchronously = false + self.selectionNode = ASDisplayNode() + self.selectionNode.backgroundColor = theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.2) + self.selectionNode.cornerRadius = 3.0 + self.selectionNode.isUserInteractionEnabled = false + self.textFieldNode = TextFieldNode() + self.measureNode = ImmediateTextNode() + self.prefixNode = ASTextNode() self.prefixNode.attributedText = NSAttributedString(string: "#", font: Font.regular(17.0), textColor: self.theme.chat.inputPanel.inputTextColor) - - self.doneButton = HighlightableButtonNode() - self.doneButton.setImage(PresentationResourcesChat.chatInputPanelApplyButtonImage(theme), for: .normal) - self.colorPickerNode = WallpaperColorPickerNode(strings: strings) - + self.swatchNode = ASDisplayNode() + self.swatchNode.cornerRadius = 10.5 + + self.borderNode = ASImageNode() + self.borderNode.displaysAsynchronously = false + self.borderNode.displayWithoutProcessing = true + self.borderNode.image = generateSwatchBorderImage(theme: theme) + + self.removeButton = HighlightableButtonNode() + self.removeButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorRemoveIcon"), color: theme.chat.inputPanel.inputControlColor), for: .normal) + super.init() - self.addSubnode(self.backgroundNode) - self.addSubnode(self.topSeparatorNode) - self.addSubnode(self.bottomSeparatorNode) self.addSubnode(self.textBackgroundNode) + self.addSubnode(self.selectionNode) self.addSubnode(self.textFieldNode) self.addSubnode(self.prefixNode) - self.addSubnode(self.doneButton) - self.addSubnode(self.colorPickerNode) + self.addSubnode(self.swatchNode) + self.addSubnode(self.borderNode) + self.addSubnode(self.removeButton) - self.colorPickerNode.colorChanged = { [weak self] color in - self?.setColor(color, updatePicker: false, ended: false) - } - self.colorPickerNode.colorChangeEnded = { [weak self] color in - self?.setColor(color, updatePicker: false, ended: true) - } + self.removeButton.addTarget(self, action: #selector(self.removePressed), forControlEvents: .touchUpInside) } - + override func didLoad() { super.didLoad() @@ -113,29 +147,507 @@ final class WallpaperColorPanelNode: ASDisplayNode, UITextFieldDelegate { self.textFieldNode.textField.addTarget(self, action: #selector(self.textFieldTextChanged(_:)), for: .editingChanged) self.textFieldNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) self.textFieldNode.textField.tintColor = self.theme.list.itemAccentColor + + let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapped)) + self.view.addGestureRecognizer(gestureRecognizer) + self.gestureRecognizer = gestureRecognizer } + func updateTheme(_ theme: PresentationTheme) { + self.theme = theme + + self.textBackgroundNode.image = textInputBackgroundImage(fieldColor: self.theme.chat.inputPanel.inputBackgroundColor, strokeColor: self.theme.chat.inputPanel.inputStrokeColor, diameter: 33.0) + + self.textFieldNode.textField.textColor = self.isDefault ? self.theme.chat.inputPanel.inputPlaceholderColor : self.theme.chat.inputPanel.inputTextColor + self.textFieldNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance + self.textFieldNode.textField.tintColor = self.theme.list.itemAccentColor + + self.selectionNode.backgroundColor = theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.2) + self.borderNode.image = generateSwatchBorderImage(theme: theme) + self.updateBorderVisibility() + } + + func setColor(_ color: UIColor, isDefault: Bool = false, update: Bool = true, ended: Bool = true) { + self.color = color + self.isDefault = isDefault + let text = color.hexString.uppercased() + self.textFieldNode.textField.text = text + self.textFieldNode.textField.textColor = isDefault ? self.theme.chat.inputPanel.inputPlaceholderColor : self.theme.chat.inputPanel.inputTextColor + if let (size, _) = self.validLayout { + self.updateSelectionLayout(size: size, transition: .immediate) + } + if update { + self.colorChanged?(color, ended) + } + self.swatchNode.backgroundColor = color + self.updateBorderVisibility() + } + + private func updateBorderVisibility() { + guard let color = self.swatchNode.backgroundColor else { + return + } + let inputBackgroundColor = self.theme.chat.inputPanel.inputBackgroundColor + if color.distance(to: inputBackgroundColor) < 200 { + self.borderNode.alpha = 1.0 + } else { + self.borderNode.alpha = 0.0 + } + } + + @objc private func removePressed() { + if self.textFieldNode.textField.isFirstResponder { + self.skipEndEditing = true + } + + self.colorRemoved?() + self.removeButton.layer.removeAnimation(forKey: "opacity") + self.removeButton.alpha = 1.0 + } + + @objc private func tapped() { + self.colorSelected?() + } + + @objc internal func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + var updated = textField.text ?? "" + updated.replaceSubrange(updated.index(updated.startIndex, offsetBy: range.lowerBound) ..< updated.index(updated.startIndex, offsetBy: range.upperBound), with: string) + if updated.count <= 6 && updated.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789abcdefABCDEF").inverted) == nil { + textField.text = updated.uppercased() + textField.textColor = self.theme.chat.inputPanel.inputTextColor + + if updated.count == 6, let color = UIColor(hexString: updated) { + self.setColor(color) + } + + if let (size, _) = self.validLayout { + self.updateSelectionLayout(size: size, transition: .immediate) + } + } + return false + } + + @objc func textFieldTextChanged(_ sender: UITextField) { + if let color = self.colorFromCurrentText() { + self.setColor(color) + } + + if let (size, _) = self.validLayout { + self.updateSelectionLayout(size: size, transition: .immediate) + } + } + + @objc func textFieldShouldReturn(_ textField: UITextField) -> Bool { + self.skipEndEditing = true + if let color = self.colorFromCurrentText() { + self.setColor(color) + } else { + self.setColor(self.previousColor ?? .black, isDefault: self.previousIsDefault ?? false) + } + self.textFieldNode.textField.resignFirstResponder() + return false + } + + func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + if self.isSelected { + self.skipEndEditing = false + self.previousColor = self.color + self.previousIsDefault = self.isDefault + + textField.textColor = self.theme.chat.inputPanel.inputTextColor + + return true + } else { + self.colorSelected?() + return false + } + } + + @objc func textFieldDidEndEditing(_ textField: UITextField) { + if !self.skipEndEditing { + if let color = self.colorFromCurrentText() { + self.setColor(color) + } else { + self.setColor(self.previousColor ?? .black, isDefault: self.previousIsDefault ?? false) + } + } + } + + func setSkipEndEditingIfNeeded() { + if self.textFieldNode.textField.isFirstResponder && self.colorFromCurrentText() != nil { + self.skipEndEditing = true + } + } + + private func colorFromCurrentText() -> UIColor? { + if let text = self.textFieldNode.textField.text, text.count == 6, let color = UIColor(hexString: text) { + return color + } else { + return nil + } + } + + private func updateSelectionLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.measureNode.attributedText = NSAttributedString(string: self.textFieldNode.textField.text ?? "", font: self.textFieldNode.textField.font) + let size = self.measureNode.updateLayout(size) + transition.updateFrame(node: self.selectionNode, frame: CGRect(x: self.textFieldNode.frame.minX, y: 6.0, width: max(0.0, size.width), height: 20.0)) + } + + private func updateSelectionVisibility() { + self.selectionNode.isHidden = !self.isSelected || self.isDefault + } + + func updateLayout(size: CGSize, condensed: Bool, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, condensed) + + let swatchFrame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 21.0, height: 21.0)) + transition.updateFrame(node: self.swatchNode, frame: swatchFrame) + transition.updateFrame(node: self.borderNode, frame: swatchFrame) + + let textPadding: CGFloat = condensed ? 31.0 : 37.0 + + transition.updateFrame(node: self.textBackgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) + transition.updateFrame(node: self.textFieldNode, frame: CGRect(x: textPadding + 10.0, y: 1.0, width: size.width - (21.0 + textPadding), height: size.height - 2.0)) + + self.updateSelectionLayout(size: size, transition: transition) + + let prefixSize = self.prefixNode.measure(size) + transition.updateFrame(node: self.prefixNode, frame: CGRect(origin: CGPoint(x: textPadding - UIScreenPixel, y: 6.0), size: prefixSize)) + + let removeSize = CGSize(width: 33.0, height: 33.0) + let removeOffset: CGFloat = condensed ? 3.0 : 0.0 + transition.updateFrame(node: self.removeButton, frame: CGRect(origin: CGPoint(x: size.width - removeSize.width + removeOffset, y: 0.0), size: removeSize)) + transition.updateAlpha(node: self.removeButton, alpha: self.isRemovable ? 1.0 : 0.0) + } +} + +enum WallpaperColorPanelNodeSelectionState { + case none + case first + case second +} + +struct WallpaperColorPanelNodeState { + var selection: WallpaperColorPanelNodeSelectionState + var firstColor: UIColor? + var defaultColor: UIColor? + var secondColor: UIColor? + var secondColorAvailable: Bool + var rotateAvailable: Bool + var rotation: Int32 + var preview: Bool + var simpleGradientGeneration: Bool +} + +final class WallpaperColorPanelNode: ASDisplayNode { + private var theme: PresentationTheme + + private var state: WallpaperColorPanelNodeState + + private let backgroundNode: ASDisplayNode + private let topSeparatorNode: ASDisplayNode + private let bottomSeparatorNode: ASDisplayNode + private let firstColorFieldNode: ColorInputFieldNode + private let secondColorFieldNode: ColorInputFieldNode + private let rotateButton: HighlightableButtonNode + private let swapButton: HighlightableButtonNode + private let addButton: HighlightableButtonNode + private let doneButton: HighlightableButtonNode + private let colorPickerNode: WallpaperColorPickerNode + + var colorsChanged: ((UIColor?, UIColor?, Bool) -> Void)? + var colorSelected: (() -> Void)? + var rotate: (() -> Void)? + + var colorAdded: (() -> Void)? + var colorRemoved: (() -> Void)? + + private var validLayout: CGSize? + + init(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.backgroundColor = theme.chat.inputPanel.panelBackgroundColor + + self.topSeparatorNode = ASDisplayNode() + self.topSeparatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor + + self.doneButton = HighlightableButtonNode() + self.doneButton.setImage(PresentationResourcesChat.chatInputPanelApplyButtonImage(theme), for: .normal) + + self.colorPickerNode = WallpaperColorPickerNode(strings: strings) + + self.rotateButton = HighlightableButtonNode() + self.rotateButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorRotateIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal) + self.swapButton = HighlightableButtonNode() + self.swapButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorSwapIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal) + self.addButton = HighlightableButtonNode() + self.addButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorAddIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal) + + self.firstColorFieldNode = ColorInputFieldNode(theme: theme) + self.secondColorFieldNode = ColorInputFieldNode(theme: theme) + + self.state = WallpaperColorPanelNodeState(selection: .first, firstColor: nil, secondColor: nil, secondColorAvailable: false, rotateAvailable: false, rotation: 0, preview: false, simpleGradientGeneration: false) + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.topSeparatorNode) + self.addSubnode(self.bottomSeparatorNode) + self.addSubnode(self.firstColorFieldNode) + self.addSubnode(self.secondColorFieldNode) + self.addSubnode(self.doneButton) + self.addSubnode(self.colorPickerNode) + + self.addSubnode(self.rotateButton) + self.addSubnode(self.swapButton) + self.addSubnode(self.addButton) + + self.rotateButton.addTarget(self, action: #selector(self.rotatePressed), forControlEvents: .touchUpInside) + self.swapButton.addTarget(self, action: #selector(self.swapPressed), forControlEvents: .touchUpInside) + self.addButton.addTarget(self, action: #selector(self.addPressed), forControlEvents: .touchUpInside) + + self.firstColorFieldNode.colorChanged = { [weak self] color, ended in + if let strongSelf = self { + strongSelf.updateState({ current in + var updated = current + updated.firstColor = color + return updated + }) + } + } + self.firstColorFieldNode.colorRemoved = { [weak self] in + if let strongSelf = self { + strongSelf.colorRemoved?() + strongSelf.updateState({ current in + var updated = current + updated.selection = .first + if let defaultColor = current.defaultColor, updated.secondColor == nil { + updated.firstColor = nil + } else { + updated.firstColor = updated.secondColor ?? updated.firstColor + } + updated.secondColor = nil + return updated + }, animated: strongSelf.state.secondColor != nil) + } + } + self.firstColorFieldNode.colorSelected = { [weak self] in + if let strongSelf = self { + strongSelf.secondColorFieldNode.setSkipEndEditingIfNeeded() + strongSelf.updateState({ current in + var updated = current + if updated.selection != .none { + updated.selection = .first + } + return updated + }) + + strongSelf.colorSelected?() + } + } + + self.secondColorFieldNode.colorChanged = { [weak self] color, ended in + if let strongSelf = self { + strongSelf.updateState({ current in + var updated = current + updated.secondColor = color + return updated + }) + } + } + self.secondColorFieldNode.colorRemoved = { [weak self] in + if let strongSelf = self { + strongSelf.colorRemoved?() + strongSelf.updateState({ current in + var updated = current + if updated.selection != .none { + updated.selection = .first + } + updated.secondColor = nil + return updated + }) + } + } + self.secondColorFieldNode.colorSelected = { [weak self] in + if let strongSelf = self { + strongSelf.firstColorFieldNode.setSkipEndEditingIfNeeded() + strongSelf.updateState({ current in + var updated = current + updated.selection = .second + return updated + }) + + strongSelf.colorSelected?() + } + } + + self.colorPickerNode.colorChanged = { [weak self] color in + if let strongSelf = self { + strongSelf.updateState({ current in + var updated = current + updated.preview = true + switch strongSelf.state.selection { + case .first: + updated.firstColor = color + case .second: + updated.secondColor = color + default: + break + } + return updated + }, updateLayout: false) + } + } + self.colorPickerNode.colorChangeEnded = { [weak self] color in + if let strongSelf = self { + strongSelf.updateState({ current in + var updated = current + updated.preview = false + switch strongSelf.state.selection { + case .first: + updated.firstColor = color + case .second: + updated.secondColor = color + default: + break + } + return updated + }, updateLayout: false) + } + } + } + func updateTheme(_ theme: PresentationTheme) { self.theme = theme self.backgroundNode.backgroundColor = self.theme.chat.inputPanel.panelBackgroundColor self.topSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor self.bottomSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor - self.textBackgroundNode.image = textInputBackgroundImage(fieldColor: self.theme.chat.inputPanel.inputBackgroundColor, strokeColor: self.theme.chat.inputPanel.inputStrokeColor, diameter: 33.0) + self.firstColorFieldNode.updateTheme(theme) + self.secondColorFieldNode.updateTheme(theme) + } + + func updateState(_ f: (WallpaperColorPanelNodeState) -> WallpaperColorPanelNodeState, updateLayout: Bool = true, animated: Bool = true) { + var updateLayout = updateLayout + let previousFirstColor = self.state.firstColor + let previousSecondColor = self.state.secondColor + let previousPreview = self.state.preview + let previousRotation = self.state.rotation + self.state = f(self.state) - self.textFieldNode.textField.textColor = self.theme.chat.inputPanel.inputTextColor - self.textFieldNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance - self.textFieldNode.textField.tintColor = self.theme.list.itemAccentColor - } - - private func setColor(_ color: UIColor, updatePicker: Bool = true, ended: Bool = true) { - self.textFieldNode.textField.text = color.hexString.uppercased() - if updatePicker { - self.colorPickerNode.color = color + let firstColor: UIColor + var firstColorIsDefault = false + if let color = self.state.firstColor { + firstColor = color + } else if let defaultColor = self.state.defaultColor { + firstColor = defaultColor + firstColorIsDefault = true + } else { + firstColor = .white + } + let secondColor = self.state.secondColor + + if secondColor == nil && previousSecondColor != nil && firstColor == previousSecondColor && animated { + self.animateLeftColorFieldOut() + } + + self.firstColorFieldNode.setColor(firstColor, isDefault: self.state.firstColor == nil, update: false) + if let secondColor = secondColor { + self.secondColorFieldNode.setColor(secondColor, update: false) + } + + var firstColorWasRemovable = self.firstColorFieldNode.isRemovable + self.firstColorFieldNode.isRemovable = self.state.secondColor != nil || (self.state.defaultColor != nil && self.state.firstColor != nil) + if firstColorWasRemovable != self.firstColorFieldNode.isRemovable { + updateLayout = true + } + + if updateLayout, let size = self.validLayout { + switch self.state.selection { + case .first: + self.colorPickerNode.color = firstColor + case .second: + if let secondColor = secondColor { + self.colorPickerNode.color = secondColor + } + default: + break + } + + self.updateLayout(size: size, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) + } + + if self.state.firstColor?.argb != previousFirstColor?.argb || self.state.secondColor?.argb != previousSecondColor?.argb || self.state.preview != previousPreview { + self.colorsChanged?(firstColorIsDefault ? nil : firstColor, secondColor, !self.state.preview) } - self.colorChanged?(color, ended) } - func updateLayout(size: CGSize, keyboardHeight: CGFloat, transition: ContainedViewLayoutTransition) { + private func animateLeftColorFieldOut() { + guard let size = self.validLayout else { + return + } + + let condensedLayout = size.width < 375.0 + let leftInset: CGFloat + let fieldSpacing: CGFloat + if condensedLayout { + leftInset = 6.0 + fieldSpacing = 40.0 + } else { + leftInset = 15.0 + fieldSpacing = 45.0 + } + let rightInsetWithButton: CGFloat = 42.0 + + let offset: CGFloat = -(self.secondColorFieldNode.frame.minX - leftInset) + + if let fieldSnapshotView = self.firstColorFieldNode.view.snapshotView(afterScreenUpdates: false) { + fieldSnapshotView.frame = self.firstColorFieldNode.frame + self.view.addSubview(fieldSnapshotView) + + fieldSnapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: offset, y: 0.0), duration: 0.3, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, force: false) { _ in + fieldSnapshotView.removeFromSuperview() + } + } + + let middleButton: ASDisplayNode + if self.rotateButton.alpha > 1.0 { + middleButton = self.rotateButton + } else { + middleButton = self.swapButton + } + if let buttonSnapshotView = middleButton.view.snapshotContentTree() { + buttonSnapshotView.frame = middleButton.frame + self.view.addSubview(buttonSnapshotView) + + buttonSnapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: offset, y: 0.0), duration: 0.3, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, force: false) { _ in + buttonSnapshotView.removeFromSuperview() + } + } + + self.rotateButton.alpha = 0.0 + self.swapButton.alpha = 0.0 + + let buttonOffset: CGFloat = (rightInsetWithButton - 13.0) / 2.0 + var buttonFrame = self.addButton.frame + buttonFrame.origin.x = size.width + self.addButton.frame = buttonFrame + self.addButton.alpha = 1.0 + + self.firstColorFieldNode.frame = self.secondColorFieldNode.frame + + var fieldFrame = self.secondColorFieldNode.frame + fieldFrame.origin.x = fieldFrame.maxX + fieldSpacing + self.secondColorFieldNode.frame = fieldFrame + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = size + + let condensedLayout = size.width < 375.0 let separatorHeight = UIScreenPixel let topPanelHeight: CGFloat = 47.0 transition.updateFrame(node: self.backgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: topPanelHeight)) @@ -143,52 +655,141 @@ final class WallpaperColorPanelNode: ASDisplayNode, UITextFieldDelegate { transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(x: 0.0, y: topPanelHeight, width: size.width, height: separatorHeight)) let fieldHeight: CGFloat = 33.0 - let buttonSpacing: CGFloat = keyboardHeight > 0.0 ? 3.0 : 6.0 - let leftInset: CGFloat = 5.0 - let rightInset: CGFloat = 5.0 + let leftInset: CGFloat + let rightInset: CGFloat + let fieldSpacing: CGFloat + if condensedLayout { + leftInset = 6.0 + rightInset = 6.0 + fieldSpacing = 40.0 + } else { + leftInset = 15.0 + rightInset = 15.0 + fieldSpacing = 45.0 + } + let rightInsetWithButton: CGFloat = 42.0 - transition.updateFrame(node: self.textBackgroundNode, frame: CGRect(x: leftInset, y: (topPanelHeight - fieldHeight) / 2.0, width: size.width - leftInset - rightInset, height: fieldHeight)) - transition.updateFrame(node: self.textFieldNode, frame: CGRect(x: leftInset + 24.0, y: (topPanelHeight - fieldHeight) / 2.0 + 1.0, width: size.width - leftInset - rightInset - 36.0, height: fieldHeight - 2.0)) + let buttonSize = CGSize(width: 26.0, height: 26.0) + let buttonOffset: CGFloat = (rightInsetWithButton - 13.0) / 2.0 + let middleButtonFrame = CGRect(origin: CGPoint(x: self.state.secondColor != nil ? floor((size.width - 26.0) / 2.0) : (self.state.secondColorAvailable ? size.width - rightInsetWithButton + floor((rightInsetWithButton - buttonSize.width) / 2.0) : size.width + buttonOffset), y: floor((topPanelHeight - buttonSize.height) / 2.0)), size: buttonSize) - let prefixSize = self.prefixNode.measure(CGSize(width: size.width, height: fieldHeight)) - transition.updateFrame(node: self.prefixNode, frame: CGRect(origin: CGPoint(x: leftInset + 13.0, y: 12.0 + UIScreenPixel), size: prefixSize)) - transition.updateFrame(node: self.doneButton, frame: CGRect(x: 0.0, y: size.width - rightInset + buttonSpacing, width: topPanelHeight, height: topPanelHeight)) + transition.updateFrame(node: self.rotateButton, frame: middleButtonFrame) + transition.updateFrame(node: self.swapButton, frame: middleButtonFrame) + transition.updateFrame(node: self.addButton, frame: middleButtonFrame) + + let rotateButtonAlpha: CGFloat + let swapButtonAlpha: CGFloat + let addButtonAlpha: CGFloat + if let _ = self.state.secondColor { + if self.state.rotateAvailable { + rotateButtonAlpha = 1.0 + swapButtonAlpha = 0.0 + } else { + rotateButtonAlpha = 0.0 + swapButtonAlpha = 1.0 + } + addButtonAlpha = 0.0 + } else { + swapButtonAlpha = 0.0 + rotateButtonAlpha = 0.0 + if self.state.secondColorAvailable { + addButtonAlpha = 1.0 + } else { + addButtonAlpha = 0.0 + } + } + transition.updateAlpha(node: self.rotateButton, alpha: rotateButtonAlpha) + transition.updateAlpha(node: self.swapButton, alpha: swapButtonAlpha) + transition.updateAlpha(node: self.addButton, alpha: addButtonAlpha) + + func degreesToRadians(_ degrees: CGFloat) -> CGFloat + { + var degrees = degrees + if degrees >= 270.0 { + degrees = degrees - 360.0 + } + return degrees * CGFloat.pi / 180.0 + } + + transition.updateTransformRotation(node: self.rotateButton, angle: degreesToRadians(CGFloat(self.state.rotation)), beginWithCurrentState: true, completion: nil) + + self.firstColorFieldNode.isRemovable = self.state.secondColor != nil || (self.state.defaultColor != nil && self.state.firstColor != nil) + self.secondColorFieldNode.isRemovable = true + + self.firstColorFieldNode.isSelected = self.state.selection == .first + self.secondColorFieldNode.isSelected = self.state.selection == .second + + let firstFieldFrame = CGRect(x: leftInset, y: (topPanelHeight - fieldHeight) / 2.0, width: self.state.secondColor != nil ? floorToScreenPixels((size.width - fieldSpacing) / 2.0) - leftInset : size.width - leftInset - (self.state.secondColorAvailable ? rightInsetWithButton : rightInset), height: fieldHeight) + transition.updateFrame(node: self.firstColorFieldNode, frame: firstFieldFrame) + self.firstColorFieldNode.updateLayout(size: firstFieldFrame.size, condensed: condensedLayout, transition: transition) + + let secondFieldFrame = CGRect(x: firstFieldFrame.maxX + fieldSpacing, y: (topPanelHeight - fieldHeight) / 2.0, width: firstFieldFrame.width, height: fieldHeight) + transition.updateFrame(node: self.secondColorFieldNode, frame: secondFieldFrame) + self.secondColorFieldNode.updateLayout(size: secondFieldFrame.size, condensed: condensedLayout, transition: transition) let colorPickerSize = CGSize(width: size.width, height: size.height - topPanelHeight - separatorHeight) transition.updateFrame(node: self.colorPickerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight + separatorHeight), size: colorPickerSize)) self.colorPickerNode.updateLayout(size: colorPickerSize, transition: transition) } - @objc internal func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - var updated = textField.text ?? "" - updated.replaceSubrange(updated.index(updated.startIndex, offsetBy: range.lowerBound) ..< updated.index(updated.startIndex, offsetBy: range.upperBound), with: string) - if updated.count <= 6 && updated.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789abcdefABCDEF").inverted) == nil { - textField.text = updated.uppercased() - } - return false + @objc private func rotatePressed() { + self.rotate?() + self.updateState({ current in + var updated = current + var newRotation = updated.rotation + 45 + if newRotation >= 360 { + newRotation = 0 + } + updated.rotation = newRotation + return updated + }) } - @objc func textFieldTextChanged(_ sender: UITextField) { - if let text = sender.text, text.count == 6, let color = UIColor(hexString: text) { - self.setColor(color) - } + @objc private func swapPressed() { + self.updateState({ current in + var updated = current + if let secondColor = current.secondColor { + updated.firstColor = secondColor + updated.secondColor = current.firstColor + } + return updated + }) } - @objc func textFieldShouldReturn(_ textField: UITextField) -> Bool { - self.textFieldNode.resignFirstResponder() - return false - } - - func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { - self.previousColor = self.color - return true - } - - @objc func textFieldDidEndEditing(_ textField: UITextField) { - if let text = self.textFieldNode.textField.text, text.count == 6, let color = UIColor(hexString: text) { - self.setColor(color) - } else { - self.setColor(self.previousColor ?? .black) - } + @objc private func addPressed() { + self.colorSelected?() + self.colorAdded?() + + self.firstColorFieldNode.setSkipEndEditingIfNeeded() + + self.updateState({ current in + var updated = current + updated.selection = .second + + let firstColor = current.firstColor ?? current.defaultColor + if let color = firstColor { + updated.firstColor = color + + let secondColor: UIColor + if updated.simpleGradientGeneration { + var hsb = color.hsb + if hsb.1 > 0.5 { + hsb.1 -= 0.15 + } else { + hsb.1 += 0.15 + } + if hsb.0 > 0.5 { + hsb.0 -= 0.05 + } else { + hsb.0 += 0.05 + } + updated.secondColor = UIColor(hue: hsb.0, saturation: hsb.1, brightness: hsb.2, alpha: 1.0) + } else { + updated.secondColor = generateGradientColors(color: color).1 + } + } + + return updated + }) } } diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperColorPickerNode.swift b/submodules/SettingsUI/Sources/Themes/WallpaperColorPickerNode.swift index c0ebb29a7d..cf89d9b0d7 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperColorPickerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperColorPickerNode.swift @@ -5,20 +5,23 @@ import SwiftSignalKit import Display import TelegramPresentationData -private let shadowImage: UIImage = { - return generateImage(CGSize(width: 45.0, height: 45.0), opaque: false, scale: nil, rotatedContext: { size, context in - context.setBlendMode(.clear) - context.setFillColor(UIColor.clear.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) +private let knobBackgroundImage: UIImage? = { + return generateImage(CGSize(width: 45.0, height: 45.0), contextGenerator: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setShadow(offset: CGSize(width: 0.0, height: -1.5), blur: 4.5, color: UIColor(rgb: 0x000000, alpha: 0.4).cgColor) + context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.4).cgColor) + context.fillEllipse(in: bounds.insetBy(dx: 3.0 + UIScreenPixel, dy: 3.0 + UIScreenPixel)) + context.setBlendMode(.normal) - context.setShadow(offset: CGSize(width: 0.0, height: 1.5), blur: 4.5, color: UIColor(rgb: 0x000000, alpha: 0.5).cgColor) - context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.5).cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: 3.0 + UIScreenPixel, dy: 3.0 + UIScreenPixel)) - })! + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: bounds.insetBy(dx: 3.0, dy: 3.0)) + }, opaque: false, scale: nil) }() -private let pointerImage: UIImage = { - return generateImage(CGSize(width: 12.0, height: 42.0), opaque: false, scale: nil, rotatedContext: { size, context in +private let pointerImage: UIImage? = { + return generateImage(CGSize(width: 12.0, height: 55.0), opaque: false, scale: nil, rotatedContext: { size, context in context.setBlendMode(.clear) context.setFillColor(UIColor.clear.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) @@ -29,8 +32,9 @@ private let pointerImage: UIImage = { context.setStrokeColor(UIColor.white.cgColor) context.setLineWidth(lineWidth) context.setLineCap(.round) + context.setLineJoin(.round) - let pointerHeight: CGFloat = 6.0 + let pointerHeight: CGFloat = 7.0 context.move(to: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0)) context.addLine(to: CGPoint(x: size.width - lineWidth / 2.0, y: lineWidth / 2.0)) context.addLine(to: CGPoint(x: size.width / 2.0, y: lineWidth / 2.0 + pointerHeight)) @@ -42,10 +46,36 @@ private let pointerImage: UIImage = { context.addLine(to: CGPoint(x: size.width - lineWidth / 2.0, y: size.height - lineWidth / 2.0)) context.closePath() context.drawPath(using: .fillStroke) - })! + }) }() -private final class HSVParameter: NSObject { +private let brightnessMaskImage: UIImage? = { + return generateImage(CGSize(width: 36.0, height: 36.0), opaque: false, scale: nil, rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + + context.setFillColor(UIColor.white.cgColor) + context.fill(bounds) + + context.setBlendMode(.clear) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: bounds) + })?.stretchableImage(withLeftCapWidth: 18, topCapHeight: 18) +}() + +private let brightnessGradientImage: UIImage? = { + return generateImage(CGSize(width: 160.0, height: 1.0), opaque: false, scale: nil, rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let gradientColors = [UIColor.black.withAlphaComponent(0.0), UIColor.black].map { $0.cgColor } as CFArray + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + }) +}() + +private final class HSBParameter: NSObject { let hue: CGFloat let saturation: CGFloat let value: CGFloat @@ -59,56 +89,49 @@ private final class HSVParameter: NSObject { } private final class WallpaperColorKnobNode: ASDisplayNode { - var hsv: (CGFloat, CGFloat, CGFloat) = (0.0, 0.0, 1.0) { + var hsb: (CGFloat, CGFloat, CGFloat) = (0.0, 0.0, 1.0) { didSet { - if self.hsv != oldValue { - self.setNeedsDisplay() + if self.hsb != oldValue { + let color = UIColor(hue: hsb.0, saturation: hsb.1, brightness: hsb.2, alpha: 1.0) + self.colorNode.backgroundColor = color } } } + private let backgroundNode: ASImageNode + private let colorNode: ASDisplayNode + override init() { + self.backgroundNode = ASImageNode() + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.image = knobBackgroundImage + + self.colorNode = ASDisplayNode() + super.init() - self.isOpaque = false - self.displaysAsynchronously = false self.isUserInteractionEnabled = false + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.colorNode) } - override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - return HSVParameter(hue: self.hsv.0, saturation: self.hsv.1, value: self.hsv.2) - } - - @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { - guard let parameters = parameters as? HSVParameter else { - return - } - let context = UIGraphicsGetCurrentContext()! + override func layout() { + super.layout() - if !isRasterizing { - context.setBlendMode(.copy) - context.setFillColor(UIColor.clear.cgColor) - context.fill(bounds) - } - - context.draw(shadowImage.cgImage!, in: bounds) - - context.setBlendMode(.normal) - context.setFillColor(UIColor.white.cgColor) - context.fillEllipse(in: bounds.insetBy(dx: 3.0, dy: 3.0)) - - let color = UIColor(hue: parameters.hue, saturation: parameters.saturation, brightness: parameters.value, alpha: 1.0) - context.setFillColor(color.cgColor) - - let borderWidth: CGFloat = bounds.width > 30.0 ? 5.0 : 5.0 - context.fillEllipse(in: bounds.insetBy(dx: borderWidth - UIScreenPixel, dy: borderWidth - UIScreenPixel)) + self.backgroundNode.frame = self.bounds + self.colorNode.frame = self.bounds.insetBy(dx: 7.0 - UIScreenPixel, dy: 7.0 - UIScreenPixel) + self.colorNode.cornerRadius = self.colorNode.frame.width / 2.0 } } private final class WallpaperColorHueSaturationNode: ASDisplayNode { var value: CGFloat = 1.0 { didSet { - self.setNeedsDisplay() + if self.value != oldValue { + self.setNeedsDisplay() + } } } @@ -120,11 +143,11 @@ private final class WallpaperColorHueSaturationNode: ASDisplayNode { } override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - return HSVParameter(hue: 1.0, saturation: 1.0, value: self.value) + return HSBParameter(hue: 1.0, saturation: 1.0, value: 1.0) } @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { - guard let parameters = parameters as? HSVParameter else { + guard let parameters = parameters as? HSBParameter else { return } let context = UIGraphicsGetCurrentContext()! @@ -146,47 +169,43 @@ private final class WallpaperColorHueSaturationNode: ASDisplayNode { } private final class WallpaperColorBrightnessNode: ASDisplayNode { - var hsv: (CGFloat, CGFloat, CGFloat) = (0.0, 1.0, 1.0) { + private let gradientNode: ASImageNode + private let maskNode: ASImageNode + + var hsb: (CGFloat, CGFloat, CGFloat) = (0.0, 1.0, 1.0) { didSet { - self.setNeedsDisplay() + if self.hsb.0 != oldValue.0 || self.hsb.1 != oldValue.1 { + let color = UIColor(hue: hsb.0, saturation: hsb.1, brightness: 1.0, alpha: 1.0) + self.backgroundColor = color + } } } override init() { + self.gradientNode = ASImageNode() + self.gradientNode.displaysAsynchronously = false + self.gradientNode.displayWithoutProcessing = true + self.gradientNode.image = brightnessGradientImage + self.gradientNode.contentMode = .scaleToFill + + self.maskNode = ASImageNode() + self.maskNode.displaysAsynchronously = false + self.maskNode.displayWithoutProcessing = true + self.maskNode.image = brightnessMaskImage + self.maskNode.contentMode = .scaleToFill + super.init() self.isOpaque = true - self.displaysAsynchronously = false + self.addSubnode(self.gradientNode) + self.addSubnode(self.maskNode) } - override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - return HSVParameter(hue: self.hsv.0, saturation: self.hsv.1, value: self.hsv.2) - } - - @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { - guard let parameters = parameters as? HSVParameter else { - return - } - let context = UIGraphicsGetCurrentContext()! - let colorSpace = CGColorSpaceCreateDeviceRGB() + override func layout() { + super.layout() - context.setFillColor(UIColor(white: parameters.value, alpha: 1.0).cgColor) - context.fill(bounds) - - let path = UIBezierPath(roundedRect: bounds, cornerRadius: bounds.height / 2.0) - context.addPath(path.cgPath) - context.setFillColor(UIColor.white.cgColor) - context.fillPath() - - let innerPath = UIBezierPath(roundedRect: bounds.insetBy(dx: 1.0, dy: 1.0), cornerRadius: bounds.height / 2.0) - context.addPath(innerPath.cgPath) - context.clip() - - let color = UIColor(hue: parameters.hue, saturation: parameters.saturation, brightness: 1.0, alpha: 1.0) - let colors = [color.cgColor, UIColor.black.cgColor] - var locations: [CGFloat] = [0.0, 1.0] - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! - context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: bounds.width, y: 0.0), options: CGGradientDrawingOptions()) + self.gradientNode.frame = self.bounds + self.maskNode.frame = self.bounds } } @@ -198,25 +217,15 @@ final class WallpaperColorPickerNode: ASDisplayNode { private var validLayout: CGSize? - var colorHSV: (CGFloat, CGFloat, CGFloat) = (0.0, 1.0, 1.0) + var colorHsb: (CGFloat, CGFloat, CGFloat) = (0.0, 1.0, 1.0) var color: UIColor { get { - return UIColor(hue: self.colorHSV.0, saturation: self.colorHSV.1, brightness: self.colorHSV.2, alpha: 1.0) + return UIColor(hue: self.colorHsb.0, saturation: self.colorHsb.1, brightness: self.colorHsb.2, alpha: 1.0) } set { - var hue: CGFloat = 0.0 - var saturation: CGFloat = 0.0 - var value: CGFloat = 0.0 - - let newHSV: (CGFloat, CGFloat, CGFloat) - if newValue.getHue(&hue, saturation: &saturation, brightness: &value, alpha: nil) { - newHSV = (hue, saturation, value) - } else { - newHSV = (0.0, 0.0, 1.0) - } - - if newHSV != self.colorHSV { - self.colorHSV = newHSV + let newHsb = newValue.hsb + if newHsb != self.colorHsb { + self.colorHsb = newHsb self.update() } } @@ -259,17 +268,17 @@ final class WallpaperColorPickerNode: ASDisplayNode { } private func update() { - self.backgroundColor = UIColor(white: self.colorHSV.2, alpha: 1.0) - self.colorNode.value = self.colorHSV.2 - self.brightnessNode.hsv = self.colorHSV - self.colorKnobNode.hsv = self.colorHSV + self.backgroundColor = .white + self.colorNode.value = self.colorHsb.2 + self.brightnessNode.hsb = self.colorHsb + self.colorKnobNode.hsb = self.colorHsb } - func updateKnobLayout(size: CGSize, panningColor: Bool, transition: ContainedViewLayoutTransition) { + private func updateKnobLayout(size: CGSize, panningColor: Bool, transition: ContainedViewLayoutTransition) { let knobSize = CGSize(width: 45.0, height: 45.0) let colorHeight = size.height - 66.0 - var colorKnobFrame = CGRect(x: -knobSize.width / 2.0 + size.width * self.colorHSV.0, y: -knobSize.height / 2.0 + (colorHeight * (1.0 - self.colorHSV.1)), width: knobSize.width, height: knobSize.height) + var colorKnobFrame = CGRect(x: floorToScreenPixels(-knobSize.width / 2.0 + size.width * self.colorHsb.0), y: floorToScreenPixels(-knobSize.height / 2.0 + (colorHeight * (1.0 - self.colorHsb.1))), width: knobSize.width, height: knobSize.height) var origin = colorKnobFrame.origin if !panningColor { origin = CGPoint(x: max(0.0, min(origin.x, size.width - knobSize.width)), y: max(0.0, min(origin.y, colorHeight - knobSize.height))) @@ -279,9 +288,9 @@ final class WallpaperColorPickerNode: ASDisplayNode { colorKnobFrame.origin = origin transition.updateFrame(node: self.colorKnobNode, frame: colorKnobFrame) - let inset: CGFloat = 42.0 - let brightnessKnobSize = CGSize(width: 12.0, height: 42.0) - let brightnessKnobFrame = CGRect(x: inset - brightnessKnobSize.width / 2.0 + (size.width - inset * 2.0) * (1.0 - self.colorHSV.2), y: size.height - 61.0, width: brightnessKnobSize.width, height: brightnessKnobSize.height) + let inset: CGFloat = 15.0 + let brightnessKnobSize = CGSize(width: 12.0, height: 55.0) + let brightnessKnobFrame = CGRect(x: inset - brightnessKnobSize.width / 2.0 + (size.width - inset * 2.0) * (1.0 - self.colorHsb.2), y: size.height - 65.0, width: brightnessKnobSize.width, height: brightnessKnobSize.height) transition.updateFrame(node: self.brightnessKnobNode, frame: brightnessKnobFrame) } @@ -291,8 +300,8 @@ final class WallpaperColorPickerNode: ASDisplayNode { let colorHeight = size.height - 66.0 transition.updateFrame(node: self.colorNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: colorHeight)) - let inset: CGFloat = 42.0 - transition.updateFrame(node: self.brightnessNode, frame: CGRect(x: inset, y: size.height - 55.0, width: size.width - inset * 2.0, height: 29.0)) + let inset: CGFloat = 15.0 + transition.updateFrame(node: self.brightnessNode, frame: CGRect(x: inset, y: size.height - 55.0, width: size.width - inset * 2.0, height: 35.0)) self.updateKnobLayout(size: size, panningColor: false, transition: .immediate) } @@ -307,8 +316,8 @@ final class WallpaperColorPickerNode: ASDisplayNode { let location = recognizer.location(in: recognizer.view) let newHue = max(0.0, min(1.0, location.x / size.width)) let newSaturation = max(0.0, min(1.0, (1.0 - location.y / colorHeight))) - self.colorHSV.0 = newHue - self.colorHSV.1 = newSaturation + self.colorHsb.0 = newHue + self.colorHsb.1 = newSaturation self.updateKnobLayout(size: size, panningColor: false, transition: .immediate) @@ -330,13 +339,13 @@ final class WallpaperColorPickerNode: ASDisplayNode { if recognizer.state == .began { let newHue = max(0.0, min(1.0, location.x / size.width)) let newSaturation = max(0.0, min(1.0, (1.0 - location.y / colorHeight))) - self.colorHSV.0 = newHue - self.colorHSV.1 = newSaturation + self.colorHsb.0 = newHue + self.colorHsb.1 = newSaturation } else { - let newHue = max(0.0, min(1.0, self.colorHSV.0 + transition.x / size.width)) - let newSaturation = max(0.0, min(1.0, self.colorHSV.1 - transition.y / (size.height - 66.0))) - self.colorHSV.0 = newHue - self.colorHSV.1 = newSaturation + let newHue = max(0.0, min(1.0, self.colorHsb.0 + transition.x / size.width)) + let newSaturation = max(0.0, min(1.0, self.colorHsb.1 - transition.y / (size.height - 66.0))) + self.colorHsb.0 = newHue + self.colorHsb.1 = newSaturation } var ended = false @@ -372,8 +381,8 @@ final class WallpaperColorPickerNode: ASDisplayNode { let transition = recognizer.translation(in: recognizer.view) let brightnessWidth: CGFloat = size.width - 42.0 * 2.0 - let newValue = max(0.0, min(1.0, self.colorHSV.2 - transition.x / brightnessWidth)) - self.colorHSV.2 = newValue + let newValue = max(0.0, min(1.0, self.colorHsb.2 - transition.x / brightnessWidth)) + self.colorHsb.2 = newValue var ended = false switch recognizer.state { diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift index f3304f3499..0157a7c7ba 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift @@ -24,11 +24,11 @@ public enum WallpaperListType { public enum WallpaperListSource { case list(wallpapers: [TelegramWallpaper], central: TelegramWallpaper, type: WallpaperListType) - case wallpaper(TelegramWallpaper, WallpaperPresentationOptions?, UIColor?, Int32?, Message?) - case slug(String, TelegramMediaFile?, WallpaperPresentationOptions?, UIColor?, Int32?, Message?) + case wallpaper(TelegramWallpaper, WallpaperPresentationOptions?, UIColor?, UIColor?, Int32?, Int32?, Message?) + case slug(String, TelegramMediaFile?, WallpaperPresentationOptions?, UIColor?, UIColor?, Int32?, Int32?, Message?) case asset(PHAsset) case contextResult(ChatContextResult) - case customColor(Int32?) + case customColor(UInt32?) } private func areMessagesEqual(_ lhsMessage: Message?, _ rhsMessage: Message?) -> Bool { @@ -97,27 +97,34 @@ class WallpaperGalleryControllerNode: GalleryControllerNode { } } -private func updatedFileWallpaper(wallpaper: TelegramWallpaper, color: UIColor?, intensity: Int32?) -> TelegramWallpaper { +private func updatedFileWallpaper(wallpaper: TelegramWallpaper, firstColor: UIColor?, secondColor: UIColor?, intensity: Int32?, rotation: Int32?) -> TelegramWallpaper { if case let .file(file) = wallpaper { - return updatedFileWallpaper(id: file.id, accessHash: file.accessHash, slug: file.slug, file: file.file, color: color, intensity: intensity) + return updatedFileWallpaper(id: file.id, accessHash: file.accessHash, slug: file.slug, file: file.file, firstColor: firstColor, secondColor: secondColor, intensity: intensity, rotation: rotation) } else { return wallpaper } } -private func updatedFileWallpaper(id: Int64? = nil, accessHash: Int64? = nil, slug: String, file: TelegramMediaFile, color: UIColor?, intensity: Int32?) -> TelegramWallpaper { - let isPattern = file.mimeType == "image/png" - var colorValue: Int32? +private func updatedFileWallpaper(id: Int64? = nil, accessHash: Int64? = nil, slug: String, file: TelegramMediaFile, firstColor: UIColor?, secondColor: UIColor?, intensity: Int32?, rotation: Int32?) -> TelegramWallpaper { + var isPattern = ["image/png", "image/svg+xml", "application/x-tgwallpattern"].contains(file.mimeType) + if let fileName = file.fileName, fileName.hasSuffix(".svgbg") { + isPattern = true + } + var firstColorValue: UInt32? + var secondColorValue: UInt32? var intensityValue: Int32? - if let color = color { - colorValue = Int32(bitPattern: color.rgb) + if let firstColor = firstColor { + firstColorValue = firstColor.argb intensityValue = intensity } else if isPattern { - colorValue = 0xd6e2ee + firstColorValue = 0xd6e2ee intensityValue = 50 } + if let secondColor = secondColor { + secondColorValue = secondColor.argb + } - return .file(id: id ?? 0, accessHash: accessHash ?? 0, isCreator: false, isDefault: false, isPattern: isPattern, isDark: false, slug: slug, file: file, settings: WallpaperSettings(blur: false, motion: false, color: colorValue, intensity: intensityValue)) + return .file(id: id ?? 0, accessHash: accessHash ?? 0, isCreator: false, isDefault: false, isPattern: isPattern, isDark: false, slug: slug, file: file, settings: WallpaperSettings(color: firstColorValue, bottomColor: secondColorValue, intensity: intensityValue, rotation: rotation)) } public class WallpaperGalleryController: ViewController { @@ -155,12 +162,9 @@ public class WallpaperGalleryController: ViewController { private var validLayout: (ContainerViewLayout, CGFloat)? private var overlayNode: WallpaperGalleryOverlayNode? - private var messageNodes: [ListViewItemNode]? private var toolbarNode: WallpaperGalleryToolbarNode? - private var colorPanelNode: WallpaperColorPanelNode? private var patternPanelNode: WallpaperPatternPanelNode? - private var colorPanelEnabled = false private var patternPanelEnabled = false public init(context: AccountContext, source: WallpaperListSource) { @@ -185,15 +189,15 @@ public class WallpaperGalleryController: ViewController { if case let .wallpapers(wallpaperOptions) = type, let options = wallpaperOptions { self.initialOptions = options } - case let .slug(slug, file, options, color, intensity, message): + case let .slug(slug, file, options, firstColor, secondColor, intensity, rotation, message): if let file = file { - let wallpaper = updatedFileWallpaper(slug: slug, file: file, color: color, intensity: intensity) + let wallpaper = updatedFileWallpaper(slug: slug, file: file, firstColor: firstColor, secondColor: secondColor, intensity: intensity, rotation: rotation) entries = [.wallpaper(wallpaper, message)] centralEntryIndex = 0 self.initialOptions = options } - case let .wallpaper(wallpaper, options, color, intensity, message): - let wallpaper = updatedFileWallpaper(wallpaper: wallpaper, color: color, intensity: intensity) + case let .wallpaper(wallpaper, options, firstColor, secondColor, intensity, rotation, message): + let wallpaper = updatedFileWallpaper(wallpaper: wallpaper, firstColor: firstColor, secondColor: secondColor, intensity: intensity, rotation: rotation) entries = [.wallpaper(wallpaper, message)] centralEntryIndex = 0 self.initialOptions = options @@ -204,8 +208,7 @@ public class WallpaperGalleryController: ViewController { entries = [.contextResult(result)] centralEntryIndex = 0 case let .customColor(color): - self.colorPanelEnabled = true - let initialColor = color ?? 0x000000 + let initialColor: UInt32 = color ?? 0x000000 entries = [.wallpaper(.color(initialColor), nil)] centralEntryIndex = 0 } @@ -278,6 +281,8 @@ public class WallpaperGalleryController: ViewController { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) self.toolbarNode?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.patternPanelNode?.updateTheme(self.presentationData.theme) + self.patternPanelNode?.backgroundColors = self.presentationData.theme.overallDarkAppearance ? (self.presentationData.theme.list.blocksBackgroundColor, nil, nil) : nil } func dismiss(forceAway: Bool) { @@ -292,7 +297,7 @@ public class WallpaperGalleryController: ViewController { var i: Int = 0 var updateItems: [GalleryPagerUpdateItem] = [] for entry in entries { - let item = GalleryPagerUpdateItem(index: i, previousIndex: i, item: WallpaperGalleryItem(context: self.context, entry: entry, arguments: arguments)) + let item = GalleryPagerUpdateItem(index: i, previousIndex: i, item: WallpaperGalleryItem(context: self.context, index: updateItems.count, entry: entry, arguments: arguments, source: self.source)) updateItems.append(item) i += 1 } @@ -311,7 +316,6 @@ public class WallpaperGalleryController: ViewController { self.displayNode = WallpaperGalleryControllerNode(controllerInteraction: controllerInteraction, pageGap: 0.0) self.displayNodeDidLoad() - self.galleryNode.statusBar = self.statusBar self.galleryNode.navigationBar = self.navigationBar self.galleryNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) @@ -340,27 +344,32 @@ public class WallpaperGalleryController: ViewController { self.galleryNode.overlayNode = overlayNode self.galleryNode.addSubnode(overlayNode) - let colorPanelNode = WallpaperColorPanelNode(theme: presentationData.theme, strings: presentationData.strings) - colorPanelNode.colorChanged = { [weak self] color, ended in - if let strongSelf = self { - strongSelf.updateEntries(color: color, preview: !ended) + var doneButtonType: WallpaperGalleryToolbarDoneButtonType = .set + switch self.source { + case let .wallpaper(wallpaper): + switch wallpaper.0 { + case let .file(file): + if file.id == 0 { + doneButtonType = .none + } + default: + break } + default: + break } - if case let .customColor(colorValue) = self.source, let color = colorValue { - colorPanelNode.color = UIColor(rgb: UInt32(bitPattern: color)) - } - self.colorPanelNode = colorPanelNode - overlayNode.addSubnode(colorPanelNode) - - let toolbarNode = WallpaperGalleryToolbarNode(theme: presentationData.theme, strings: presentationData.strings) + + let toolbarNode = WallpaperGalleryToolbarNode(theme: presentationData.theme, strings: presentationData.strings, doneButtonType: doneButtonType) self.toolbarNode = toolbarNode overlayNode.addSubnode(toolbarNode) toolbarNode.cancel = { [weak self] in self?.dismiss(forceAway: true) } + var dismissed = false toolbarNode.done = { [weak self] in - if let strongSelf = self { + if let strongSelf = self, !dismissed { + dismissed = true if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { let options = centralItemNode.options if !strongSelf.entries.isEmpty { @@ -381,21 +390,27 @@ public class WallpaperGalleryController: ViewController { let completion: (TelegramWallpaper) -> Void = { wallpaper in let baseSettings = wallpaper.settings - let updatedSettings = WallpaperSettings(blur: options.contains(.blur), motion: options.contains(.motion), color: baseSettings?.color, intensity: baseSettings?.intensity) + let updatedSettings = WallpaperSettings(blur: options.contains(.blur), motion: options.contains(.motion), color: baseSettings?.color, bottomColor: baseSettings?.bottomColor, intensity: baseSettings?.intensity) let wallpaper = wallpaper.withUpdatedSettings(updatedSettings) let autoNightModeTriggered = strongSelf.presentationData.autoNightModeTriggered let _ = (updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers - var chatWallpaper = current.chatWallpaper + var wallpaper = wallpaper.isBasicallyEqual(to: strongSelf.presentationData.theme.chat.defaultWallpaper) ? nil : wallpaper + let themeReference: PresentationThemeReference if autoNightModeTriggered { - themeSpecificChatWallpapers[current.automaticThemeSwitchSetting.theme.index] = wallpaper + themeReference = current.automaticThemeSwitchSetting.theme } else { - themeSpecificChatWallpapers[current.theme.index] = wallpaper - chatWallpaper = wallpaper + themeReference = current.theme } - - return PresentationThemeSettings(chatWallpaper: chatWallpaper, theme: current.theme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) + let accentColor = current.themeSpecificAccentColors[themeReference.index] + if let accentColor = accentColor, accentColor.baseColor == .custom { + themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = wallpaper + } else { + themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = nil + themeSpecificChatWallpapers[themeReference.index] = wallpaper + } + return current.withUpdatedThemeSpecificChatWallpapers(themeSpecificChatWallpapers) }) |> deliverOnMainQueue).start(completed: { self?.dismiss(forceAway: true) }) @@ -432,8 +447,8 @@ public class WallpaperGalleryController: ViewController { } } } else if case let .file(file) = wallpaper, let resource = resource { - if file.isPattern, let color = file.settings.color, let intensity = file.settings.intensity { - let representation = CachedPatternWallpaperRepresentation(color: color, intensity: intensity) + if wallpaper.isPattern, let color = file.settings.color, let intensity = file.settings.intensity { + let representation = CachedPatternWallpaperRepresentation(color: color, bottomColor: file.settings.bottomColor, intensity: intensity, rotation: file.settings.rotation) var data: Data? if let path = strongSelf.context.account.postbox.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { @@ -538,10 +553,6 @@ public class WallpaperGalleryController: ViewController { strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.3, curve: .spring)) } } - - if let (layout, bottomInset) = self.validLayout { - self.updateMessagesLayout(layout: layout, bottomInset: bottomInset, transition: .immediate) - } } } @@ -556,7 +567,7 @@ public class WallpaperGalleryController: ViewController { case let .wallpaper(wallpaper, _): switch wallpaper { case .color: - currentEntry = .wallpaper(.color(Int32(color.rgb)), nil) + currentEntry = .wallpaper(.color(color.argb), nil) default: break } @@ -572,7 +583,7 @@ public class WallpaperGalleryController: ViewController { private func updateEntries(pattern: TelegramWallpaper?, intensity: Int32? = nil, preview: Bool = false) { var updatedEntries: [WallpaperGalleryEntry] = [] for entry in self.entries { - var entryColor: Int32? + var entryColor: UInt32? if case let .wallpaper(wallpaper, _) = entry { if case let .color(color) = wallpaper { entryColor = color @@ -584,7 +595,7 @@ public class WallpaperGalleryController: ViewController { if let entryColor = entryColor { if let pattern = pattern, case let .file(file) = pattern { let newSettings = WallpaperSettings(blur: file.settings.blur, motion: file.settings.motion, color: entryColor, intensity: intensity) - let newWallpaper = TelegramWallpaper.file(id: file.id, accessHash: file.accessHash, isCreator: file.isCreator, isDefault: file.isDefault, isPattern: file.isPattern, isDark: file.isDark, slug: file.slug, file: file.file, settings: newSettings) + let newWallpaper = TelegramWallpaper.file(id: file.id, accessHash: file.accessHash, isCreator: file.isCreator, isDefault: file.isDefault, isPattern: pattern.isPattern, isDark: file.isDark, slug: file.slug, file: file.file, settings: newSettings) updatedEntries.append(.wallpaper(newWallpaper, nil)) } else { let newWallpaper = TelegramWallpaper.color(entryColor) @@ -597,103 +608,7 @@ public class WallpaperGalleryController: ViewController { self.galleryNode.pager.transaction(self.updateTransaction(entries: updatedEntries, arguments: WallpaperGalleryItemArguments(colorPreview: preview, isColorsList: true, patternEnabled: self.patternPanelEnabled))) } - private func updateMessagesLayout(layout: ContainerViewLayout, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { - var items: [ListViewItem] = [] - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: 1) - let otherPeerId = self.context.account.peerId - var peers = SimpleDictionary() - let messages = SimpleDictionary() - peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_PreviewReplyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) - peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_PreviewReplyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) - - var currentWallpaper: TelegramWallpaper = self.presentationData.chatWallpaper - if let entry = self.currentEntry(), case let .wallpaper(wallpaper, _) = entry { - currentWallpaper = wallpaper - } - - //let chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: self.presentationData.theme, wallpaper: currentWallpaper), fontSize: self.presentationData.fontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: false, largeEmoji: false) - - var topMessageText: String - var bottomMessageText: String - switch self.source { - case .wallpaper, .slug: - topMessageText = presentationData.strings.WallpaperPreview_PreviewTopText - bottomMessageText = presentationData.strings.WallpaperPreview_PreviewBottomText - case let .list(_, _, type): - switch type { - case .wallpapers: - topMessageText = presentationData.strings.WallpaperPreview_SwipeTopText - bottomMessageText = presentationData.strings.WallpaperPreview_SwipeBottomText - case .colors: - topMessageText = presentationData.strings.WallpaperPreview_SwipeColorsTopText - bottomMessageText = presentationData.strings.WallpaperPreview_SwipeColorsBottomText - } - case .asset, .contextResult: - topMessageText = presentationData.strings.WallpaperPreview_CropTopText - bottomMessageText = presentationData.strings.WallpaperPreview_CropBottomText - case .customColor: - topMessageText = presentationData.strings.WallpaperPreview_CustomColorTopText - bottomMessageText = presentationData.strings.WallpaperPreview_CustomColorBottomText - } - - if self.colorPanelEnabled { - topMessageText = presentationData.strings.WallpaperPreview_CustomColorTopText - bottomMessageText = presentationData.strings.WallpaperPreview_CustomColorBottomText - } - - let message1 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: bottomMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message1, theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.fontSize, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil)) - - let message2 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: topMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message2, theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.fontSize, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil)) - - let params = ListViewItemLayoutParams(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) - if let messageNodes = self.messageNodes { - for i in 0 ..< items.count { - let itemNode = messageNodes[i] - items[i].updateNode(async: { $0() }, node: { - return itemNode - }, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in - let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height)) - - itemNode.contentSize = layout.contentSize - itemNode.insets = layout.insets - itemNode.frame = nodeFrame - itemNode.isUserInteractionEnabled = false - - apply(ListViewItemApply(isOnScreen: true)) - }) - } - } else { - var messageNodes: [ListViewItemNode] = [] - for i in 0 ..< items.count { - var itemNode: ListViewItemNode? - items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in - itemNode = node - apply().1(ListViewItemApply(isOnScreen: true)) - }) - itemNode!.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) - itemNode!.isUserInteractionEnabled = false - messageNodes.append(itemNode!) - self.overlayNode?.addSubnode(itemNode!) - } - self.messageNodes = messageNodes - } - - if let messageNodes = self.messageNodes { - var bottomOffset: CGFloat = layout.size.height - bottomInset - 9.0 - if self.colorPanelEnabled { - } else { - bottomOffset -= 66.0 - } - for itemNode in messageNodes { - transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset - itemNode.frame.height), size: itemNode.frame.size)) - bottomOffset -= itemNode.frame.height - itemNode.updateFrame(itemNode.frame, within: layout.size) - } - } - } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { let hadLayout = self.validLayout != nil @@ -710,16 +625,6 @@ public class WallpaperGalleryController: ViewController { var bottomInset = layout.intrinsicInsets.bottom + 49.0 let standardInputHeight = layout.deviceMetrics.keyboardHeight(inLandscape: false) let height = max(standardInputHeight, layout.inputHeight ?? 0.0) - bottomInset + 47.0 - if let colorPanelNode = self.colorPanelNode { - var colorPanelFrame = CGRect(x: 0.0, y: layout.size.height, width: layout.size.width, height: height) - if self.colorPanelEnabled { - colorPanelFrame.origin = CGPoint(x: 0.0, y: layout.size.height - bottomInset - height) - bottomInset += height - } - - transition.updateFrame(node: colorPanelNode, frame: colorPanelFrame) - colorPanelNode.updateLayout(size: colorPanelFrame.size, keyboardHeight: layout.inputHeight ?? 0.0, transition: transition) - } let currentPatternPanelNode: WallpaperPatternPanelNode if let patternPanelNode = self.patternPanelNode { @@ -731,23 +636,23 @@ public class WallpaperGalleryController: ViewController { strongSelf.updateEntries(pattern: pattern, intensity: intensity, preview: preview) } } + patternPanelNode.backgroundColors = self.presentationData.theme.overallDarkAppearance ? (self.presentationData.theme.list.blocksBackgroundColor, nil, nil) : nil self.patternPanelNode = patternPanelNode currentPatternPanelNode = patternPanelNode self.overlayNode?.insertSubnode(patternPanelNode, belowSubnode: self.toolbarNode!) } - let panelHeight: CGFloat = 190.0 + let panelHeight: CGFloat = 235.0 var patternPanelFrame = CGRect(x: 0.0, y: layout.size.height, width: layout.size.width, height: panelHeight) if self.patternPanelEnabled { patternPanelFrame.origin = CGPoint(x: 0.0, y: layout.size.height - bottomInset - panelHeight) bottomInset += panelHeight } + bottomInset += 66.0 transition.updateFrame(node: currentPatternPanelNode, frame: patternPanelFrame) currentPatternPanelNode.updateLayout(size: patternPanelFrame.size, transition: transition) - self.updateMessagesLayout(layout: layout, bottomInset: bottomInset, transition: transition) - self.validLayout = (layout, bottomInset) if !hadLayout { var colors = false @@ -755,7 +660,7 @@ public class WallpaperGalleryController: ViewController { colors = true } - self.galleryNode.pager.replaceItems(self.entries.map({ WallpaperGalleryItem(context: self.context, entry: $0, arguments: WallpaperGalleryItemArguments(isColorsList: colors)) }), centralItemIndex: self.centralEntryIndex) + self.galleryNode.pager.replaceItems(zip(0 ..< self.entries.count, self.entries).map({ WallpaperGalleryItem(context: self.context, index: $0, entry: $1, arguments: WallpaperGalleryItemArguments(isColorsList: colors), source: self.source) }), centralItemIndex: self.centralEntryIndex) if let initialOptions = self.initialOptions, let itemNode = self.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { itemNode.options = initialOptions @@ -801,11 +706,18 @@ public class WallpaperGalleryController: ViewController { case let .file(_, _, _, _, isPattern, _, slug, _, settings): if isPattern { if let color = settings.color { - options.append("bg_color=\(UIColor(rgb: UInt32(bitPattern: color)).hexString)") + if let bottomColor = settings.bottomColor { + options.append("bg_color=\(UIColor(rgb: color).hexString)-\(UIColor(rgb: bottomColor).hexString)") + } else { + options.append("bg_color=\(UIColor(rgb: color).hexString)") + } } if let intensity = settings.intensity { options.append("intensity=\(intensity)") } + if let rotation = settings.rotation { + options.append("rotation=\(rotation)") + } } var optionsString = "" @@ -815,7 +727,9 @@ public class WallpaperGalleryController: ViewController { controller = ShareController(context: context, subject: .url("https://t.me/bg/\(slug)\(optionsString)")) case let .color(color): - controller = ShareController(context: context, subject: .url("https://t.me/bg/\(UIColor(rgb: UInt32(bitPattern: color)).hexString)")) + controller = ShareController(context: context, subject: .url("https://t.me/bg/\(UIColor(rgb: color).hexString)")) + case let .gradient(topColor, bottomColor, _): + controller = ShareController(context: context, subject:. url("https://t.me/bg/\(UIColor(rgb: topColor).hexString)-\(UIColor(rgb: bottomColor).hexString)")) default: break } diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift index 16c46210e4..07774073d6 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift @@ -32,25 +32,34 @@ struct WallpaperGalleryItemArguments { } class WallpaperGalleryItem: GalleryItem { + var id: AnyHashable { + return self.index + } + + let index: Int + let context: AccountContext let entry: WallpaperGalleryEntry let arguments: WallpaperGalleryItemArguments + let source: WallpaperListSource - init(context: AccountContext, entry: WallpaperGalleryEntry, arguments: WallpaperGalleryItemArguments) { + init(context: AccountContext, index: Int, entry: WallpaperGalleryEntry, arguments: WallpaperGalleryItemArguments, source: WallpaperListSource) { self.context = context + self.index = index self.entry = entry self.arguments = arguments + self.source = source } func node() -> GalleryItemNode { let node = WallpaperGalleryItemNode(context: self.context) - node.setEntry(self.entry, arguments: self.arguments) + node.setEntry(self.entry, arguments: self.arguments, source: self.source) return node } func updateNode(node: GalleryItemNode) { if let node = node as? WallpaperGalleryItemNode { - node.setEntry(self.entry, arguments: self.arguments) + node.setEntry(self.entry, arguments: self.arguments, source: self.source) } } @@ -66,12 +75,15 @@ private func reference(for resource: MediaResource, media: Media, message: Messa if let message = message { return .media(media: .message(message: MessageReference(message), media: media), resource: resource) } - return .wallpaper(resource: resource) + return .wallpaper(wallpaper: nil, resource: resource) } final class WallpaperGalleryItemNode: GalleryItemNode { private let context: AccountContext + private let presentationData: PresentationData + var entry: WallpaperGalleryEntry? + var source: WallpaperListSource? private var colorPreview: Bool = false private var contentSize: CGSize? private var arguments = WallpaperGalleryItemArguments() @@ -86,6 +98,9 @@ final class WallpaperGalleryItemNode: GalleryItemNode { private var motionButtonNode: WallpaperOptionButtonNode private var patternButtonNode: WallpaperOptionButtonNode + private let messagesContainerNode: ASDisplayNode + private var messageNodes: [ListViewItemNode]? + fileprivate let _ready = Promise() private let fetchDisposable = MetaDisposable() private let statusDisposable = MetaDisposable() @@ -102,6 +117,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { init(context: AccountContext) { self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.wrapperNode = ASDisplayNode() self.imageNode = TransformImageNode() @@ -113,12 +129,14 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.blurredNode = BlurredImageNode() - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.blurButtonNode = WallpaperOptionButtonNode(title: presentationData.strings.WallpaperPreview_Blurred, value: .check(false)) + self.messagesContainerNode = ASDisplayNode() + self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + + self.blurButtonNode = WallpaperOptionButtonNode(title: self.presentationData.strings.WallpaperPreview_Blurred, value: .check(false)) self.blurButtonNode.setEnabled(false) - self.motionButtonNode = WallpaperOptionButtonNode(title: presentationData.strings.WallpaperPreview_Motion, value: .check(false)) + self.motionButtonNode = WallpaperOptionButtonNode(title: self.presentationData.strings.WallpaperPreview_Motion, value: .check(false)) self.motionButtonNode.setEnabled(false) - self.patternButtonNode = WallpaperOptionButtonNode(title: presentationData.strings.WallpaperPreview_Pattern, value: .check(false)) + self.patternButtonNode = WallpaperOptionButtonNode(title: self.presentationData.strings.WallpaperPreview_Pattern, value: .check(false)) self.patternButtonNode.setEnabled(false) super.init() @@ -135,6 +153,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.addSubnode(self.wrapperNode) self.addSubnode(self.statusNode) + self.addSubnode(self.messagesContainerNode) self.addSubnode(self.blurButtonNode) self.addSubnode(self.motionButtonNode) @@ -171,9 +190,10 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.action?() } - func setEntry(_ entry: WallpaperGalleryEntry, arguments: WallpaperGalleryItemArguments) { + func setEntry(_ entry: WallpaperGalleryEntry, arguments: WallpaperGalleryItemArguments, source: WallpaperListSource) { let previousArguments = self.arguments self.arguments = arguments + self.source = source if self.arguments.colorPreview != previousArguments.colorPreview { if self.arguments.colorPreview { @@ -200,7 +220,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let subtitleSignal: Signal var actionSignal: Signal = .single(nil) var colorSignal: Signal = serviceColor(from: imagePromise.get()) - var color: UIColor? + var patternArguments: PatternWallpaperArguments? let displaySize: CGSize let contentSize: CGSize @@ -226,16 +246,25 @@ final class WallpaperGalleryItemNode: GalleryItemNode { case let .color(color): displaySize = CGSize(width: 1.0, height: 1.0) contentSize = displaySize - signal = solidColor(UIColor(rgb: UInt32(bitPattern: color))) + signal = solidColorImage(UIColor(rgb: color)) + fetchSignal = .complete() + statusSignal = .single(.Local) + subtitleSignal = .single(nil) + actionSignal = .single(defaultAction) + colorSignal = chatServiceBackgroundColor(wallpaper: wallpaper, mediaBox: self.context.account.postbox.mediaBox) + isBlurrable = false + case let .gradient(topColor, bottomColor, settings): + displaySize = CGSize(width: 1.0, height: 1.0) + contentSize = displaySize + signal = gradientImage([UIColor(rgb: topColor), UIColor(rgb: bottomColor)], rotation: settings.rotation) fetchSignal = .complete() statusSignal = .single(.Local) subtitleSignal = .single(nil) actionSignal = .single(defaultAction) colorSignal = chatServiceBackgroundColor(wallpaper: wallpaper, mediaBox: self.context.account.postbox.mediaBox) isBlurrable = false - //self.backgroundColor = UIColor(rgb: UInt32(bitPattern: color)) case let .file(file): - let dimensions = file.file.dimensions ?? PixelDimensions(width: 100, height: 100) + let dimensions = file.file.dimensions ?? PixelDimensions(width: 2000, height: 4000) contentSize = dimensions.cgSize displaySize = dimensions.cgSize.dividedByScreenScale().integralFloor @@ -245,15 +274,24 @@ final class WallpaperGalleryItemNode: GalleryItemNode { } convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource), reference: reference(for: file.file.resource, media: file.file, message: message))) - if file.isPattern { + if wallpaper.isPattern { + var patternColors: [UIColor] = [] var patternColor = UIColor(rgb: 0xd6e2ee, alpha: 0.5) var patternIntensity: CGFloat = 0.5 + if let color = file.settings.color { if let intensity = file.settings.intensity { patternIntensity = CGFloat(intensity) / 100.0 } - patternColor = UIColor(rgb: UInt32(bitPattern: color), alpha: patternIntensity) + patternColor = UIColor(rgb: color, alpha: patternIntensity) + patternColors.append(patternColor) + + if let bottomColor = file.settings.bottomColor { + patternColors.append(UIColor(rgb: bottomColor, alpha: patternIntensity)) + } } + + patternArguments = PatternWallpaperArguments(colors: patternColors, rotation: file.settings.rotation, preview: self.arguments.colorPreview) self.backgroundColor = patternColor.withAlphaComponent(1.0) @@ -261,7 +299,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let makeImageLayout = self.imageNode.asyncLayout() Queue.concurrentDefaultQueue().async { - let apply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets(), emptyColor: patternColor)) + let apply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets(), custom: patternArguments)) Queue.mainQueue().async { if self.colorPreview { apply() @@ -271,14 +309,13 @@ final class WallpaperGalleryItemNode: GalleryItemNode { return } else if let offset = self.validOffset, self.arguments.colorPreview && abs(offset) > 0.0 { return - } - else { - color = patternColor + } else { + patternArguments = PatternWallpaperArguments(colors: patternColors, rotation: file.settings.rotation) } self.colorPreview = self.arguments.colorPreview - signal = patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, mode: self.arguments.colorPreview ? .fastScreen : .screen, autoFetchFullSize: true) + signal = patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, mode: .screen, autoFetchFullSize: true) colorSignal = chatServiceBackgroundColor(wallpaper: wallpaper, mediaBox: context.account.postbox.mediaBox) isBlurrable = false @@ -307,13 +344,17 @@ final class WallpaperGalleryItemNode: GalleryItemNode { } else { subtitleSignal = .single(nil) } - actionSignal = .single(defaultAction) + if file.id == 0 { + actionSignal = .single(nil) + } else { + actionSignal = .single(defaultAction) + } case let .image(representations, _): if let largestSize = largestImageRepresentation(representations) { contentSize = largestSize.dimensions.cgSize displaySize = largestSize.dimensions.cgSize.dividedByScreenScale().integralFloor - let convertedRepresentations: [ImageRepresentationWithReference] = representations.map({ ImageRepresentationWithReference(representation: $0, reference: .wallpaper(resource: $0.resource)) }) + let convertedRepresentations: [ImageRepresentationWithReference] = representations.map({ ImageRepresentationWithReference(representation: $0, reference: .wallpaper(wallpaper: nil, resource: $0.resource)) }) signal = wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, alwaysShowThumbnailFirst: true, autoFetchFullSize: false) if let largestIndex = convertedRepresentations.firstIndex(where: { $0.representation == largestSize }) { @@ -409,7 +450,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource)) } representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource)) - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) signal = chatMessagePhoto(postbox: context.account.postbox, photoReference: .standalone(media: tmpImage)) fetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .media(media: .standalone(media: tmpImage), resource: imageResource)) @@ -434,7 +475,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { } self.imageNode.setSignal(signal, dispatchOnDisplayLink: false) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets(), emptyColor: color))() + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets(), custom: patternArguments))() self.imageNode.imageUpdated = { [weak self] image in if let strongSelf = self { var image = isBlurrable ? image : nil @@ -494,6 +535,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { if let layout = self.validLayout { self.updateButtonsLayout(layout: layout, offset: CGPoint(), transition: .immediate) + self.updateMessagesLayout(layout: layout, offset: CGPoint(), transition: .immediate) } } } @@ -507,12 +549,14 @@ final class WallpaperGalleryItemNode: GalleryItemNode { if let layout = self.validLayout { self.updateWrapperLayout(layout: layout, offset: offset, transition: .immediate) self.updateButtonsLayout(layout: layout, offset: CGPoint(x: offset, y: 0.0), transition: .immediate) + self.updateMessagesLayout(layout: layout, offset: CGPoint(x: offset, y: 0.0), transition:.immediate) } } func updateDismissTransition(_ value: CGFloat) { if let layout = self.validLayout { self.updateButtonsLayout(layout: layout, offset: CGPoint(x: 0.0, y: value), transition: .immediate) + self.updateMessagesLayout(layout: layout, offset: CGPoint(x: 0.0, y: value), transition: .immediate) } } @@ -588,7 +632,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { } } - @objc func toggleMotion() { + @objc private func toggleMotion() { let value = !self.motionButtonNode.isSelected self.motionButtonNode.setSelected(value, animated: true) self.setMotionEnabled(value, animated: true) @@ -598,18 +642,19 @@ final class WallpaperGalleryItemNode: GalleryItemNode { return self.patternButtonNode.isSelected } - @objc func togglePattern() { + @objc private func togglePattern() { let value = !self.patternButtonNode.isSelected - self.patternButtonNode.setSelected(value, animated: true) + self.patternButtonNode.setSelected(value, animated: false) self.requestPatternPanel?(value) } private func preparePatternEditing() { if let entry = self.entry, case let .wallpaper(wallpaper, _) = entry, case let .file(file) = wallpaper { - if let size = file.file.dimensions?.cgSize.fitted(CGSize(width: 1280.0, height: 1280.0)) { - let _ = self.context.account.postbox.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPatternWallpaperMaskRepresentation(size: size), complete: false, fetch: true).start() - } + let dimensions = file.file.dimensions ?? PixelDimensions(width: 1440, height: 2960) + + let size = dimensions.cgSize.fitted(CGSize(width: 1280.0, height: 1280.0)) + let _ = self.context.account.postbox.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPatternWallpaperMaskRepresentation(size: size), complete: false, fetch: true).start() } } @@ -671,7 +716,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { var additionalYOffset: CGFloat = 0.0 if self.patternButtonNode.isSelected { - additionalYOffset = -190.0 + additionalYOffset = -235.0 } let leftButtonFrame = CGRect(origin: CGPoint(x: floor(layout.size.width / 2.0 - buttonSize.width - 10.0) + offset.x, y: layout.size.height - 49.0 - layout.intrinsicInsets.bottom - 54.0 + offset.y + additionalYOffset), size: buttonSize) @@ -715,8 +760,10 @@ final class WallpaperGalleryItemNode: GalleryItemNode { blurFrame = leftButtonFrame motionAlpha = 1.0 motionFrame = rightButtonFrame + case .gradient: + motionAlpha = 1.0 case let .file(file): - if file.isPattern { + if wallpaper.isPattern { motionAlpha = 1.0 if self.arguments.isColorsList { patternAlpha = 1.0 @@ -745,6 +792,104 @@ final class WallpaperGalleryItemNode: GalleryItemNode { transition.updateAlpha(node: self.motionButtonNode, alpha: motionAlpha * alpha) } + private func updateMessagesLayout(layout: ContainerViewLayout, offset: CGPoint, transition: ContainedViewLayoutTransition) { + var bottomInset: CGFloat = 115.0 + if self.patternButtonNode.isSelected { + bottomInset = 350.0 + } + + var items: [ListViewItem] = [] + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: 1) + let otherPeerId = self.context.account.peerId + var peers = SimpleDictionary() + let messages = SimpleDictionary() + peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_PreviewReplyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_PreviewReplyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + + var topMessageText = "" + var bottomMessageText = "" + var currentWallpaper: TelegramWallpaper = self.presentationData.chatWallpaper + if let entry = self.entry, case let .wallpaper(wallpaper, _) = entry { + currentWallpaper = wallpaper + } + + if let source = self.source { + switch source { + case .wallpaper, .slug: + topMessageText = presentationData.strings.WallpaperPreview_PreviewTopText + bottomMessageText = presentationData.strings.WallpaperPreview_PreviewBottomText + case let .list(_, _, type): + switch type { + case .wallpapers: + topMessageText = presentationData.strings.WallpaperPreview_SwipeTopText + bottomMessageText = presentationData.strings.WallpaperPreview_SwipeBottomText + case .colors: + topMessageText = presentationData.strings.WallpaperPreview_SwipeColorsTopText + bottomMessageText = presentationData.strings.WallpaperPreview_SwipeColorsBottomText + } + case .asset, .contextResult: + topMessageText = presentationData.strings.WallpaperPreview_CropTopText + bottomMessageText = presentationData.strings.WallpaperPreview_CropBottomText + case .customColor: + topMessageText = presentationData.strings.WallpaperPreview_CustomColorTopText + bottomMessageText = presentationData.strings.WallpaperPreview_CustomColorBottomText + } + } + + let theme = self.presentationData.theme.withUpdated(preview: true) + + let message1 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: bottomMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message1, theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil)) + + let message2 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: topMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, message: message2, theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil)) + + let params = ListViewItemLayoutParams(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) + if let messageNodes = self.messageNodes { +// for i in 0 ..< items.count { +// let itemNode = messageNodes[i] +// items[i].updateNode(async: { $0() }, node: { +// return itemNode +// }, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in +// let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height)) +// +// itemNode.contentSize = layout.contentSize +// itemNode.insets = layout.insets +// itemNode.frame = nodeFrame +// itemNode.isUserInteractionEnabled = false +// +// apply(ListViewItemApply(isOnScreen: true)) +// }) +// } + } else { + var messageNodes: [ListViewItemNode] = [] + for i in 0 ..< items.count { + var itemNode: ListViewItemNode? + items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in + itemNode = node + apply().1(ListViewItemApply(isOnScreen: true)) + }) + itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + itemNode!.isUserInteractionEnabled = false + messageNodes.append(itemNode!) + self.messagesContainerNode.addSubnode(itemNode!) + } + self.messageNodes = messageNodes + } + + let alpha = 1.0 - min(1.0, max(0.0, abs(offset.y) / 50.0)) + + if let messageNodes = self.messageNodes { + var bottomOffset: CGFloat = 9.0 + bottomInset + layout.intrinsicInsets.bottom + for itemNode in messageNodes { + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: offset.x, y: bottomOffset - offset.y), size: itemNode.frame.size)) + bottomOffset += itemNode.frame.height + itemNode.updateFrame(itemNode.frame, within: layout.size) + transition.updateAlpha(node: itemNode, alpha: alpha) + } + } + } + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) @@ -755,6 +900,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.wrapperNode.bounds = CGRect(origin: CGPoint(), size: layout.size) self.updateWrapperLayout(layout: layout, offset: offset, transition: transition) + self.messagesContainerNode.frame = CGRect(origin: CGPoint(), size: layout.size) if self.cropNode.supernode == nil { self.imageNode.frame = self.wrapperNode.bounds @@ -779,6 +925,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.statusNode.frame = CGRect(x: layout.safeInsets.left + floorToScreenPixels((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - progressDiameter) / 2.0), y: floorToScreenPixels((layout.size.height + additionalYOffset - progressDiameter) / 2.0), width: progressDiameter, height: progressDiameter) self.updateButtonsLayout(layout: layout, offset: CGPoint(x: offset, y: 0.0), transition: transition) + self.updateMessagesLayout(layout: layout, offset: CGPoint(x: offset, y: 0.0), transition: transition) self.validLayout = layout } diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryToolbarNode.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryToolbarNode.swift index 1207761b79..97e93331b7 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryToolbarNode.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryToolbarNode.swift @@ -4,23 +4,57 @@ import AsyncDisplayKit import Display import TelegramPresentationData +enum WallpaperGalleryToolbarCancelButtonType { + case cancel + case discard +} + +enum WallpaperGalleryToolbarDoneButtonType { + case set + case proceed + case apply + case none +} + final class WallpaperGalleryToolbarNode: ASDisplayNode { private var theme: PresentationTheme + private let strings: PresentationStrings - private let cancelButton = HighlightableButtonNode() - private let doneButton = HighlightableButtonNode() + var cancelButtonType: WallpaperGalleryToolbarCancelButtonType { + didSet { + self.updateThemeAndStrings(theme: self.theme, strings: self.strings) + } + } + var doneButtonType: WallpaperGalleryToolbarDoneButtonType { + didSet { + self.updateThemeAndStrings(theme: self.theme, strings: self.strings) + } + } + + private let cancelButton = HighlightTrackingButtonNode() + private let cancelHighlightBackgroundNode = ASDisplayNode() + private let doneButton = HighlightTrackingButtonNode() + private let doneHighlightBackgroundNode = ASDisplayNode() private let separatorNode = ASDisplayNode() private let topSeparatorNode = ASDisplayNode() var cancel: (() -> Void)? var done: (() -> Void)? - init(theme: PresentationTheme, strings: PresentationStrings) { + init(theme: PresentationTheme, strings: PresentationStrings, cancelButtonType: WallpaperGalleryToolbarCancelButtonType = .cancel, doneButtonType: WallpaperGalleryToolbarDoneButtonType = .set) { self.theme = theme + self.strings = strings + self.cancelButtonType = cancelButtonType + self.doneButtonType = doneButtonType + + self.cancelHighlightBackgroundNode.alpha = 0.0 + self.doneHighlightBackgroundNode.alpha = 0.0 super.init() + self.addSubnode(self.cancelHighlightBackgroundNode) self.addSubnode(self.cancelButton) + self.addSubnode(self.doneHighlightBackgroundNode) self.addSubnode(self.doneButton) self.addSubnode(self.separatorNode) self.addSubnode(self.topSeparatorNode) @@ -30,11 +64,11 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode { self.cancelButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { - strongSelf.cancelButton.backgroundColor = strongSelf.theme.list.itemHighlightedBackgroundColor + strongSelf.cancelHighlightBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.cancelHighlightBackgroundNode.alpha = 1.0 } else { - UIView.animate(withDuration: 0.3, animations: { - strongSelf.cancelButton.backgroundColor = .clear - }) + strongSelf.cancelHighlightBackgroundNode.alpha = 0.0 + strongSelf.cancelHighlightBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) } } } @@ -42,11 +76,11 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode { self.doneButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { - strongSelf.doneButton.backgroundColor = strongSelf.theme.list.itemHighlightedBackgroundColor + strongSelf.doneHighlightBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.doneHighlightBackgroundNode.alpha = 1.0 } else { - UIView.animate(withDuration: 0.3, animations: { - strongSelf.doneButton.backgroundColor = .clear - }) + strongSelf.doneHighlightBackgroundNode.alpha = 0.0 + strongSelf.doneHighlightBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) } } } @@ -65,14 +99,37 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode { self.backgroundColor = theme.rootController.tabBar.backgroundColor self.separatorNode.backgroundColor = theme.rootController.tabBar.separatorColor self.topSeparatorNode.backgroundColor = theme.rootController.tabBar.separatorColor + self.cancelHighlightBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor + self.doneHighlightBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor - self.cancelButton.setTitle(strings.Common_Cancel, with: Font.regular(17.0), with: theme.list.itemPrimaryTextColor, for: []) - self.doneButton.setTitle(strings.Wallpaper_Set, with: Font.regular(17.0), with: theme.list.itemPrimaryTextColor, for: []) + let cancelTitle: String + switch self.cancelButtonType { + case .cancel: + cancelTitle = strings.Common_Cancel + case .discard: + cancelTitle = strings.WallpaperPreview_PatternPaternDiscard + } + let doneTitle: String + switch self.doneButtonType { + case .set: + doneTitle = strings.Wallpaper_Set + case .proceed: + doneTitle = strings.Theme_Colors_Proceed + case .apply: + doneTitle = strings.WallpaperPreview_PatternPaternApply + case .none: + doneTitle = "" + self.doneButton.isUserInteractionEnabled = false + } + self.cancelButton.setTitle(cancelTitle, with: Font.regular(17.0), with: theme.list.itemPrimaryTextColor, for: []) + self.doneButton.setTitle(doneTitle, with: Font.regular(17.0), with: theme.list.itemPrimaryTextColor, for: []) } func updateLayout(size: CGSize, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.cancelButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: floor(size.width / 2.0), height: size.height)) + self.cancelHighlightBackgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: floor(size.width / 2.0), height: size.height)) self.doneButton.frame = CGRect(origin: CGPoint(x: floor(size.width / 2.0), y: 0.0), size: CGSize(width: size.width - floor(size.width / 2.0), height: size.height)) + self.doneHighlightBackgroundNode.frame = CGRect(origin: CGPoint(x: floor(size.width / 2.0), y: 0.0), size: CGSize(width: size.width - floor(size.width / 2.0), height: size.height)) self.separatorNode.frame = CGRect(origin: CGPoint(x: floor(size.width / 2.0), y: 0.0), size: CGSize(width: UIScreenPixel, height: size.height + layout.intrinsicInsets.bottom)) self.topSeparatorNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: UIScreenPixel)) } @@ -82,7 +139,6 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode { } @objc func donePressed() { - self.doneButton.isUserInteractionEnabled = false self.done?() } } diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryDecorationNode.swift b/submodules/SettingsUI/Sources/Themes/WallpaperOptionButtonNode.swift similarity index 80% rename from submodules/SettingsUI/Sources/Themes/WallpaperGalleryDecorationNode.swift rename to submodules/SettingsUI/Sources/Themes/WallpaperOptionButtonNode.swift index ec1a347b00..18bd92880a 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryDecorationNode.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperOptionButtonNode.swift @@ -43,7 +43,7 @@ final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { self.backgroundNode = ASDisplayNode() self.backgroundNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.3) - self.backgroundNode.cornerRadius = 6.0 + self.backgroundNode.cornerRadius = 14.0 self.checkNode = ModernCheckNode(theme: CheckNodeTheme(backgroundColor: .white, strokeColor: .clear, borderColor: .white, hasShadow: false)) self.checkNode.isUserInteractionEnabled = false @@ -147,7 +147,7 @@ final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { override func measure(_ constrainedSize: CGSize) -> CGSize { let size = self.textNode.measure(constrainedSize) self.textSize = size - return CGSize(width: ceil(size.width) + 52.0, height: 30.0) + return CGSize(width: ceil(size.width) + 48.0, height: 30.0) } override func layout() { @@ -159,42 +159,15 @@ final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { return } - let checkSize = CGSize(width: 18.0, height: 18.0) + let padding: CGFloat = 6.0 let spacing: CGFloat = 9.0 - let totalWidth = checkSize.width + spacing + textSize.width - let origin = floor((self.bounds.width - totalWidth) / 2.0) + let checkSize = CGSize(width: 18.0, height: 18.0) - self.checkNode.frame = CGRect(origin: CGPoint(x: origin, y: 6.0), size: checkSize) - self.colorNode.frame = CGRect(origin: CGPoint(x: origin, y: 6.0), size: checkSize) + self.checkNode.frame = CGRect(origin: CGPoint(x: padding, y: padding), size: checkSize) + self.colorNode.frame = CGRect(origin: CGPoint(x: padding, y: padding), size: checkSize) if let textSize = self.textSize { - self.textNode.frame = CGRect(x: origin + checkSize.width + spacing, y: 6.0 + UIScreenPixel, width: textSize.width, height: textSize.height) - } - } -} - -final class WallpaperGalleryDecorationNode: ASDisplayNode { - private let dismiss: () -> Void - private let apply: () -> Void - -// private var messageNodes: [ListViewItemNode]? -// private var blurredButtonNode: WallpaperOptionButtonNode? -// private var motionButtonNode: WallpaperOptionButtonNode? -// private var toolbarNode: WallpaperGalleryToolbarNode? - - init(source: WallpaperListSource, dismiss: @escaping () -> Void, apply: @escaping () -> Void) { - self.dismiss = dismiss - self.apply = apply - - super.init() - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let result = super.hitTest(point, with: event) - if result != self.view { - return result - } else { - return nil + self.textNode.frame = CGRect(x: max(padding + checkSize.width + spacing, padding + checkSize.width + floor((self.bounds.width - padding - checkSize.width - textSize.width) / 2.0) - 2.0), y: 6.0 + UIScreenPixel, width: textSize.width, height: textSize.height) } } } diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperPatternPanelNode.swift b/submodules/SettingsUI/Sources/Themes/WallpaperPatternPanelNode.swift index d06b8e1f35..92fb90eb6d 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperPatternPanelNode.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperPatternPanelNode.swift @@ -8,53 +8,240 @@ import SyncCore import TelegramPresentationData import LegacyComponents import AccountContext +import MergeLists private let itemSize = CGSize(width: 88.0, height: 88.0) private let inset: CGFloat = 12.0 + +private struct WallpaperPatternEntry: Comparable, Identifiable { + let index: Int + let wallpaper: TelegramWallpaper + let selected: Bool + + var stableId: Int64 { + if case let .file(file) = self.wallpaper { + return file.id + } else { + return Int64(self.index) + } + } + + static func ==(lhs: WallpaperPatternEntry, rhs: WallpaperPatternEntry) -> Bool { + if lhs.index != rhs.index { + return false + } + if lhs.wallpaper != rhs.wallpaper { + return false + } + return true + } + + static func <(lhs: WallpaperPatternEntry, rhs: WallpaperPatternEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(context: AccountContext, action: @escaping (TelegramWallpaper) -> Void) -> ListViewItem { + return WallpaperPatternItem(context: context, wallpaper: self.wallpaper, selected: self.selected, action: action) + } +} + +private class WallpaperPatternItem: ListViewItem { + let context: AccountContext + let wallpaper: TelegramWallpaper + let selected: Bool + let action: (TelegramWallpaper) -> Void + + public init(context: AccountContext, wallpaper: TelegramWallpaper, selected: Bool, action: @escaping (TelegramWallpaper) -> Void) { + self.context = context + self.wallpaper = wallpaper + self.selected = selected + self.action = action + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = WallpaperPatternItemNode() + let (nodeLayout, apply) = node.asyncLayout()(self, params) + node.insets = nodeLayout.insets + node.contentSize = nodeLayout.contentSize + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in + apply(false) + }) + }) + } + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + assert(node() is WallpaperPatternItemNode) + if let nodeValue = node() as? WallpaperPatternItemNode { + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params) + Queue.mainQueue().async { + completion(nodeLayout, { _ in + apply(animation.isAnimated) + }) + } + } + } + } + } + + public var selectable = true + public func selected(listView: ListView) { + self.action(self.wallpaper) + } +} + +private final class WallpaperPatternItemNode : ListViewItemNode { + private let wallpaperNode: SettingsThemeWallpaperNode + + var item: WallpaperPatternItem? + + init() { + self.wallpaperNode = SettingsThemeWallpaperNode() + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.wallpaperNode) + } + + override func didLoad() { + super.didLoad() + + self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + } + + func asyncLayout() -> (WallpaperPatternItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let currentItem = self.item + + return { [weak self] item, params in + var updatedWallpaper = false + var updatedSelected = false + + if currentItem?.wallpaper != item.wallpaper { + updatedWallpaper = true + } + if currentItem?.selected != item.selected { + updatedSelected = true + } + + let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 112.0, height: 112.0), insets: UIEdgeInsets()) + return (itemLayout, { animated in + if let strongSelf = self { + strongSelf.item = item + strongSelf.wallpaperNode.frame = CGRect(x: 0.0, y: 12.0, width: itemSize.width, height: itemSize.height) + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + super.animateRemoved(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + super.animateAdded(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } +} + + + + + + + + final class WallpaperPatternPanelNode: ASDisplayNode { - private let theme: PresentationTheme + private let context: AccountContext + private var theme: PresentationTheme private let backgroundNode: ASDisplayNode private let topSeparatorNode: ASDisplayNode private let scrollNode: ASScrollNode - private let labelNode: ASTextNode + private let titleNode: ImmediateTextNode + private let labelNode: ImmediateTextNode private var sliderView: TGPhotoEditorSliderView? private var disposable: Disposable? - private var wallpapers: [TelegramWallpaper] = [] + var wallpapers: [TelegramWallpaper] = [] private var currentWallpaper: TelegramWallpaper? - var patternChanged: ((TelegramWallpaper, Int32?, Bool) -> Void)? + var serviceBackgroundColor: UIColor = UIColor(rgb: 0x748698) { + didSet { + guard let nodes = self.scrollNode.subnodes else { + return + } + for case let node as SettingsThemeWallpaperNode in nodes { + node.setOverlayBackgroundColor(self.serviceBackgroundColor.withAlphaComponent(0.4)) + } + } + } + + var backgroundColors: (UIColor, UIColor?, Int32?)? = nil { + didSet { + if oldValue?.0.rgb != self.backgroundColors?.0.rgb || oldValue?.1?.rgb != self.backgroundColors?.1?.rgb + || oldValue?.2 != self.backgroundColors?.2 { + self.updateWallpapers() + } + } + } + + private var validLayout: CGSize? + + var patternChanged: ((TelegramWallpaper?, Int32?, Bool) -> Void)? init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { + self.context = context self.theme = theme self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = theme.chat.inputPanel.panelBackgroundColor + self.backgroundNode.backgroundColor = self.theme.chat.inputPanel.panelBackgroundColor self.topSeparatorNode = ASDisplayNode() - self.topSeparatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor + self.topSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor self.scrollNode = ASScrollNode() - self.labelNode = ASTextNode() + self.titleNode = ImmediateTextNode() + self.titleNode.attributedText = NSAttributedString(string: strings.WallpaperPreview_PatternTitle, font: Font.bold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor) + + self.labelNode = ImmediateTextNode() self.labelNode.attributedText = NSAttributedString(string: strings.WallpaperPreview_PatternIntensity, font: Font.regular(14.0), textColor: theme.rootController.navigationBar.primaryTextColor) super.init() + self.allowsGroupOpacity = true + self.addSubnode(self.backgroundNode) self.addSubnode(self.topSeparatorNode) self.addSubnode(self.scrollNode) + self.addSubnode(self.titleNode) self.addSubnode(self.labelNode) self.disposable = ((telegramWallpapers(postbox: context.account.postbox, network: context.account.network) |> map { wallpapers in return wallpapers.filter { wallpaper in - if case let .file(file) = wallpaper, file.isPattern, file.file.mimeType != "image/webp" { + if case let .file(file) = wallpaper, wallpaper.isPattern, file.file.mimeType != "image/webp" { return true } else { return false @@ -63,46 +250,8 @@ final class WallpaperPatternPanelNode: ASDisplayNode { } |> deliverOnMainQueue).start(next: { [weak self] wallpapers in if let strongSelf = self { - if let subnodes = strongSelf.scrollNode.subnodes { - for node in subnodes { - node.removeFromSupernode() - } - } - - var selected = true - for wallpaper in wallpapers { - let node = SettingsThemeWallpaperNode(overlayBackgroundColor: UIColor(rgb: 0x748698, alpha: 0.4)) - node.clipsToBounds = true - node.cornerRadius = 5.0 - - var updatedWallpaper = wallpaper - if case let .file(file) = updatedWallpaper { - let settings = WallpaperSettings(blur: false, motion: false, color: 0xd6e2ee, intensity: 100) - updatedWallpaper = .file(id: file.id, accessHash: file.accessHash, isCreator: file.isCreator, isDefault: file.isDefault, isPattern: file.isPattern, isDark: file.isDark, slug: file.slug, file: file.file, settings: settings) - } - - node.setWallpaper(context: context, wallpaper: updatedWallpaper, selected: selected, size: itemSize) - node.pressed = { [weak self, weak node] in - if let strongSelf = self { - strongSelf.currentWallpaper = updatedWallpaper - if let sliderView = strongSelf.sliderView { - strongSelf.patternChanged?(updatedWallpaper, Int32(sliderView.value), false) - } - if let subnodes = strongSelf.scrollNode.subnodes { - for case let subnode as SettingsThemeWallpaperNode in subnodes { - subnode.setSelected(node === subnode, animated: true) - } - } - } - } - strongSelf.scrollNode.addSubnode(node) - - selected = false - } - strongSelf.scrollNode.view.contentSize = CGSize(width: (itemSize.width + inset) * CGFloat(wallpapers.count) + inset, height: 112.0) - strongSelf.layoutItemNodes(transition: .immediate) - strongSelf.wallpapers = wallpapers + strongSelf.updateWallpapers() } })) } @@ -135,6 +284,79 @@ final class WallpaperPatternPanelNode: ASDisplayNode { self.sliderView = sliderView } + func updateWallpapers() { + guard let subnodes = self.scrollNode.subnodes else { + return + } + + for node in subnodes { + node.removeFromSupernode() + } + + let backgroundColors = self.backgroundColors ?? (UIColor(rgb: 0xd6e2ee), nil, nil) + + var selectedFileId: Int64? + if let currentWallpaper = self.currentWallpaper, case let .file(file) = currentWallpaper { + selectedFileId = file.id + } + + for wallpaper in self.wallpapers { + let node = SettingsThemeWallpaperNode(overlayBackgroundColor: self.serviceBackgroundColor.withAlphaComponent(0.4)) + node.clipsToBounds = true + node.cornerRadius = 5.0 + + var updatedWallpaper = wallpaper + if case let .file(file) = updatedWallpaper { + let settings = WallpaperSettings(color: backgroundColors.0.rgb, bottomColor: backgroundColors.1.flatMap { $0.rgb }, intensity: 100, rotation: backgroundColors.2) + updatedWallpaper = .file(id: file.id, accessHash: file.accessHash, isCreator: file.isCreator, isDefault: file.isDefault, isPattern: updatedWallpaper.isPattern, isDark: file.isDark, slug: file.slug, file: file.file, settings: settings) + } + + var selected = false + if case let .file(file) = wallpaper, file.id == selectedFileId { + selected = true + } + + node.setWallpaper(context: self.context, wallpaper: updatedWallpaper, selected: selected, size: itemSize) + node.pressed = { [weak self, weak node] in + if let strongSelf = self { + strongSelf.currentWallpaper = updatedWallpaper + if let sliderView = strongSelf.sliderView { + strongSelf.patternChanged?(updatedWallpaper, Int32(sliderView.value), false) + } + if let subnodes = strongSelf.scrollNode.subnodes { + for case let subnode as SettingsThemeWallpaperNode in subnodes { + let selected = node === subnode + subnode.setSelected(selected, animated: true) + if selected { + strongSelf.scrollToNode(subnode, animated: true) + } + } + } + } + } + self.scrollNode.addSubnode(node) + } + + self.scrollNode.view.contentSize = CGSize(width: (itemSize.width + inset) * CGFloat(wallpapers.count) + inset, height: 112.0) + self.layoutItemNodes(transition: .immediate) + } + + func updateTheme(_ theme: PresentationTheme) { + self.theme = theme + + self.backgroundNode.backgroundColor = self.theme.chat.inputPanel.panelBackgroundColor + self.topSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor + + self.sliderView?.backColor = self.theme.list.disclosureArrowColor + self.sliderView?.trackColor = self.theme.list.itemAccentColor + self.titleNode.attributedText = NSAttributedString(string: self.labelNode.attributedText?.string ?? "", font: Font.bold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + self.labelNode.attributedText = NSAttributedString(string: self.labelNode.attributedText?.string ?? "", font: Font.regular(14.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + + if let size = self.validLayout { + self.updateLayout(size: size, transition: .immediate) + } + } + @objc func sliderValueChanged() { guard let sliderView = self.sliderView else { return @@ -145,37 +367,79 @@ final class WallpaperPatternPanelNode: ASDisplayNode { } } - func didAppear() { - if let wallpaper = self.wallpapers.first { + func didAppear(initialWallpaper: TelegramWallpaper? = nil, intensity: Int32? = nil) { + var wallpaper = initialWallpaper ?? self.wallpapers.first + + if let wallpaper = wallpaper { + var selectedFileId: Int64? + if case let .file(file) = wallpaper { + selectedFileId = file.id + } + self.currentWallpaper = wallpaper - self.sliderView?.value = 40.0 + self.sliderView?.value = CGFloat(intensity ?? 50) self.scrollNode.view.contentOffset = CGPoint() - var selected = true + var selectedNode: SettingsThemeWallpaperNode? if let subnodes = self.scrollNode.subnodes { for case let subnode as SettingsThemeWallpaperNode in subnodes { + var selected = false + if case let .file(file) = subnode.wallpaper, file.id == selectedFileId { + selected = true + selectedNode = subnode + } subnode.setSelected(selected, animated: false) - selected = false } } - - if let wallpaper = self.currentWallpaper, let sliderView = self.sliderView { + + if initialWallpaper == nil, let wallpaper = self.currentWallpaper, let sliderView = self.sliderView { self.patternChanged?(wallpaper, Int32(sliderView.value), false) } + + if let selectedNode = selectedNode { + self.scrollToNode(selectedNode) + } + } + } + + private func scrollToNode(_ node: SettingsThemeWallpaperNode, animated: Bool = false) { + let bounds = self.scrollNode.view.bounds + let frame = node.frame.insetBy(dx: -48.0, dy: 0.0) + + if frame.minX < bounds.minX || frame.maxX > bounds.maxX { + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate + + var origin = CGPoint() + if frame.minX < bounds.minX { + origin.x = max(0.0, frame.minX) + } else if frame.maxX > bounds.maxX { + origin.x = min(self.scrollNode.view.contentSize.width - bounds.width, frame.maxX - bounds.width) + } + + transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: origin, size: self.scrollNode.frame.size)) } } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - let separatorHeight = UIScreenPixel + self.validLayout = size + transition.updateFrame(node: self.backgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: separatorHeight)) - transition.updateFrame(node: self.scrollNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: 114.0)) + transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: UIScreenPixel)) - let labelSize = self.labelNode.measure(self.bounds.size) - transition.updateFrame(node: labelNode, frame: CGRect(origin: CGPoint(x: 14.0, y: 128.0), size: labelSize)) + let titleSize = self.titleNode.updateLayout(self.bounds.size) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((self.bounds.width - titleSize.width) / 2.0), y: 19.0), size: titleSize)) - self.sliderView?.frame = CGRect(origin: CGPoint(x: 15.0, y: 136.0), size: CGSize(width: size.width - 15.0 * 2.0, height: 44.0)) + let scrollViewFrame = CGRect(x: 0.0, y: 52.0, width: size.width, height: 114.0) + transition.updateFrame(node: self.scrollNode, frame: scrollViewFrame) + + let labelSize = self.labelNode.updateLayout(self.bounds.size) + var combinedHeight = labelSize.height + 34.0 + + var originY: CGFloat = scrollViewFrame.maxY + floor((size.height - scrollViewFrame.maxY - combinedHeight) / 2.0) + transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: 14.0, y: originY), size: labelSize)) + + self.sliderView?.frame = CGRect(origin: CGPoint(x: 15.0, y: originY + 8.0), size: CGSize(width: size.width - 15.0 * 2.0, height: 44.0)) self.layoutItemNodes(transition: transition) } diff --git a/submodules/SettingsUI/Sources/UsernameSetupController.swift b/submodules/SettingsUI/Sources/UsernameSetupController.swift index 3482cd4ff4..a30977b730 100644 --- a/submodules/SettingsUI/Sources/UsernameSetupController.swift +++ b/submodules/SettingsUI/Sources/UsernameSetupController.swift @@ -91,16 +91,16 @@ private enum UsernameSetupEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! UsernameSetupControllerArguments switch self { case let .editablePublicLink(theme, strings, prefix, currentText, text): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: prefix, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .username, spacing: 10.0, clearType: .always, tag: UsernameEntryTag.username, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: prefix, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .username, spacing: 10.0, clearType: .always, tag: UsernameEntryTag.username, sectionId: self.section, textUpdated: { updatedText in arguments.updatePublicLinkText(currentText, updatedText) }, action: { }) case let .publicLinkInfo(theme, text): - return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section, linkAction: { action in + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { action in if case .tap = action { arguments.shareLink() } @@ -122,7 +122,7 @@ private enum UsernameSetupEntry: ItemListNodeEntry { string = NSAttributedString(string: text, textColor: theme.list.freeTextColor) displayActivity = true } - return ItemListActivityTextItem(displayActivity: displayActivity, theme: theme, text: string, sectionId: self.section) + return ItemListActivityTextItem(displayActivity: displayActivity, presentationData: presentationData, text: string, sectionId: self.section) } } } @@ -339,8 +339,8 @@ public func usernameSetupController(context: AccountContext) -> ViewController { dismissImpl?() }) - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Username_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: usernameSetupControllerEntries(presentationData: presentationData, view: view, state: state), style: .blocks, focusItemTag: UsernameEntryTag.username, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Username_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: usernameSetupControllerEntries(presentationData: presentationData, view: view, state: state), style: .blocks, focusItemTag: UsernameEntryTag.username, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/submodules/SettingsUI/Sources/Watch/WatchSettingsController.swift b/submodules/SettingsUI/Sources/Watch/WatchSettingsController.swift index ce4dbcb7c5..6e6da3a112 100644 --- a/submodules/SettingsUI/Sources/Watch/WatchSettingsController.swift +++ b/submodules/SettingsUI/Sources/Watch/WatchSettingsController.swift @@ -75,17 +75,17 @@ private enum WatchSettingsControllerEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! WatchSettingsControllerArguments switch self { case let .replyPresetsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .replyPreset(theme, strings, identifier, placeholder, value, _): - return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: ""), text: value, placeholder: placeholder, type: .regular(capitalization: true, autocorrection: true), spacing: 0.0, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: ""), text: value, placeholder: placeholder, type: .regular(capitalization: true, autocorrection: true), spacing: 0.0, sectionId: self.section, textUpdated: { updatedText in arguments.updatePreset(identifier, updatedText.trimmingCharacters(in: .whitespacesAndNewlines)) }, action: {}) case let .replyPresetsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -135,8 +135,8 @@ public func watchSettingsController(context: AccountContext) -> ViewController { |> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in let settings = (sharedData.entries[ApplicationSpecificSharedDataKeys.watchPresetSettings] as? WatchPresetSettings) ?? WatchPresetSettings.defaultSettings - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.AppleWatch_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: watchSettingsControllerEntries(presentationData: presentationData, customPresets: settings.customPresets), style: .blocks, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.AppleWatch_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: watchSettingsControllerEntries(presentationData: presentationData, customPresets: settings.customPresets), style: .blocks, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/ShareController/BUCK b/submodules/ShareController/BUCK index 9a5616a4e9..64238e15fe 100644 --- a/submodules/ShareController/BUCK +++ b/submodules/ShareController/BUCK @@ -23,7 +23,8 @@ static_library( "//submodules/ActivityIndicator:ActivityIndicator", "//submodules/AppBundle:AppBundle", "//submodules/TelegramStringFormatting:TelegramStringFormatting", - "//submodules/AppIntents:AppIntents", + "//submodules/TelegramIntents:TelegramIntents", + "//submodules/AccountContext:AccountContext", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index af8f646288..80e42a7db5 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -172,10 +172,19 @@ private func collectExternalShareItems(strings: PresentationStrings, dateTimeFor text.append("\n— \(option.text)") } let totalVoters = poll.results.totalVoters ?? 0 - if totalVoters == 0 { - text.append("\n\(strings.MessagePoll_NoVotes)") - } else { - text.append("\n\(strings.MessagePoll_VotedCount(totalVoters))") + switch poll.kind { + case .poll: + if totalVoters == 0 { + text.append("\n\(strings.MessagePoll_NoVotes)") + } else { + text.append("\n\(strings.MessagePoll_VotedCount(totalVoters))") + } + case .quiz: + if totalVoters == 0 { + text.append("\n\(strings.MessagePoll_QuizNoUsers)") + } else { + text.append("\n\(strings.MessagePoll_QuizCount(totalVoters))") + } } signals.append(.single(.done(.text(text)))) } else if let mediaReference = item.mediaReference, let contact = mediaReference.media as? TelegramMediaContact { @@ -183,7 +192,7 @@ private func collectExternalShareItems(strings: PresentationStrings, dateTimeFor if let vCard = contact.vCardData, let vCardData = vCard.data(using: .utf8), let parsed = DeviceContactExtendedData(vcard: vCardData) { contactData = parsed } else { - contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName, lastName: contact.lastName, phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: contact.phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: []) + contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName, lastName: contact.lastName, phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: contact.phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") } if let vCard = contactData.serializedVCard() { @@ -425,8 +434,8 @@ public final class ShareController: ViewController { guard let strongSelf = self else { return } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare) + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId) self.controllerNode.dismiss = { [weak self] shared in self?.presentingViewController?.dismiss(animated: false, completion: nil) self?.dismissed?(shared) @@ -480,7 +489,7 @@ public final class ShareController: ViewController { if !text.isEmpty { messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) } - messages.append(.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil)), replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])), replyToMessageId: nil, localGroupingKey: nil)) shareSignals.append(enqueueMessages(account: strongSelf.currentAccount, peerId: peerId, messages: messages)) } case let .media(mediaReference): @@ -555,7 +564,7 @@ public final class ShareController: ViewController { } if !displayedError, case .slowmodeActive = error { displayedError = true - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), text: strongSelf.presentationData.strings.Chat_SlowmodeSendError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), text: strongSelf.presentationData.strings.Chat_SlowmodeSendError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } }) } @@ -586,7 +595,7 @@ public final class ShareController: ViewController { case let .quote(text, url): collectableItems.append(CollectableExternalShareItem(url: "", text: "\"\(text)\"\n\n\(url)", author: nil, timestamp: nil, mediaReference: nil)) case let .image(representations): - let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil) + let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) collectableItems.append(CollectableExternalShareItem(url: "", text: "", author: nil, timestamp: nil, mediaReference: .standalone(media: media))) case let .media(mediaReference): collectableItems.append(CollectableExternalShareItem(url: "", text: "", author: nil, timestamp: nil, mediaReference: mediaReference)) @@ -680,7 +689,7 @@ public final class ShareController: ViewController { strongSelf.controllerNode.animateOut(shared: false, completion: {}) let presentationData = strongSelf.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) controller.dismissed = { [weak self] cancelled in if cancelled { self?.controllerNode.animateIn() @@ -691,7 +700,7 @@ public final class ShareController: ViewController { } var items: [ActionSheetItem] = [] for info in strongSelf.switchableAccounts { - items.append(ActionSheetPeerItem(account: info.account, peer: info.peer, title: info.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), isSelected: info.account.id == strongSelf.currentAccount.id, strings: presentationData.strings, theme: presentationData.theme, action: { [weak self] in + items.append(ActionSheetPeerItem(context: strongSelf.sharedContext.makeTempAccountContext(account: info.account), peer: info.peer, title: info.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), isSelected: info.account.id == strongSelf.currentAccount.id, strings: presentationData.strings, theme: presentationData.theme, action: { [weak self] in dismissAction() self?.switchToAccount(account: info.account, animateIn: true) })) @@ -704,15 +713,12 @@ public final class ShareController: ViewController { } self.displayNodeDidLoad() - if let _ = self.immediatePeerId { - } else { - self.peersDisposable.set((self.peers.get() - |> deliverOnMainQueue).start(next: { [weak self] next in - if let strongSelf = self { - strongSelf.controllerNode.updatePeers(account: strongSelf.currentAccount, switchableAccounts: strongSelf.switchableAccounts, peers: next.0, accountPeer: next.1, defaultAction: strongSelf.defaultAction) - } - })) - } + self.peersDisposable.set((self.peers.get() + |> deliverOnMainQueue).start(next: { [weak self] next in + if let strongSelf = self { + strongSelf.controllerNode.updatePeers(context: strongSelf.sharedContext.makeTempAccountContext(account: strongSelf.currentAccount), switchableAccounts: strongSelf.switchableAccounts, peers: next.0, accountPeer: next.1, defaultAction: strongSelf.defaultAction) + } + })) self._ready.set(self.controllerNode.ready.get()) } @@ -773,7 +779,7 @@ public final class ShareController: ViewController { } private func saveToCameraRoll(representations: [ImageRepresentationWithReference]) { - let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil) + let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) let context: AccountContext if self.currentContext.account.id == self.currentAccount.id { context = self.currentContext @@ -807,7 +813,7 @@ public final class ShareController: ViewController { var peers: [RenderedPeer] = [] for entry in view.0.entries.reversed() { switch entry { - case let .MessageEntry(_, _, _, _, _, renderedPeer, _, _): + case let .MessageEntry(_, _, _, _, _, renderedPeer, _, _, _): if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != accountPeer.id, canSendMessagesToPeer(peer) { peers.append(renderedPeer) } @@ -827,31 +833,23 @@ public final class ShareController: ViewController { return (resultPeers, accountPeer) } }) - if let immediatePeerId = self.immediatePeerId { - self.sendImmediately(peerId: immediatePeerId) - } else { - self.peersDisposable.set((self.peers.get() - |> deliverOnMainQueue).start(next: { [weak self] next in - if let strongSelf = self { - strongSelf.controllerNode.updatePeers(account: strongSelf.currentAccount, switchableAccounts: strongSelf.switchableAccounts, peers: next.0, accountPeer: next.1, defaultAction: strongSelf.defaultAction) - - if animateIn { - strongSelf.readyDisposable.set((strongSelf.controllerNode.ready.get() - |> filter({ $0 }) - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] _ in - guard let strongSelf = self else { - return - } - strongSelf.controllerNode.animateIn() - })) - } + self.peersDisposable.set((self.peers.get() + |> deliverOnMainQueue).start(next: { [weak self] next in + if let strongSelf = self { + strongSelf.controllerNode.updatePeers(context: strongSelf.sharedContext.makeTempAccountContext(account: strongSelf.currentAccount), switchableAccounts: strongSelf.switchableAccounts, peers: next.0, accountPeer: next.1, defaultAction: strongSelf.defaultAction) + + if animateIn { + strongSelf.readyDisposable.set((strongSelf.controllerNode.ready.get() + |> filter({ $0 }) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.animateIn() + })) } - })) - } - } - - private func sendImmediately(peerId: PeerId) { - self.controllerNode.send(peerId: peerId) + } + })) } } diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index 1f5bfd5785..2cbfe1bbe1 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -8,7 +8,7 @@ import TelegramCore import SyncCore import TelegramPresentationData import AccountContext -import AppIntents +import TelegramIntents enum ShareState { case preparing @@ -27,10 +27,11 @@ func openExternalShare(state: () -> Signal) { final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private let sharedContext: SharedAccountContext - private var account: Account? + private var context: AccountContext? private var presentationData: PresentationData private let externalShare: Bool private let immediateExternalShare: Bool + private var immediatePeerId: PeerId? private let defaultAction: ShareControllerAction? private let requestLayout: (ContainedViewLayoutTransition) -> Void @@ -75,11 +76,12 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate private var hapticFeedback: HapticFeedback? - init(sharedContext: SharedAccountContext, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool) { + init(sharedContext: SharedAccountContext, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?) { self.sharedContext = sharedContext self.presentationData = sharedContext.currentPresentationData.with { $0 } self.externalShare = externalShare self.immediateExternalShare = immediateExternalShare + self.immediatePeerId = immediatePeerId self.presentError = presentError self.defaultAction = defaultAction @@ -151,7 +153,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } super.init() - + self.controllerInteraction = ShareControllerInteraction(togglePeer: { [weak self] peer, search in if let strongSelf = self { var added = false @@ -539,8 +541,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate peerIds = self.controllerInteraction!.selectedPeers.map { $0.peerId } } - if let account = self.account { - donateSendMessageIntent(account: account, sharedContext: self.sharedContext, peerIds: peerIds) + if let context = self.context { + donateSendMessageIntent(account: context.account, sharedContext: self.sharedContext, intentContext: .share, peerIds: peerIds) } if let signal = self.share?(self.inputFieldNode.text, peerIds) { @@ -634,25 +636,36 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } } - func updatePeers(account: Account, switchableAccounts: [AccountWithInfo], peers: [(RenderedPeer, PeerPresence?)], accountPeer: Peer, defaultAction: ShareControllerAction?) { - self.account = account + func updatePeers(context: AccountContext, switchableAccounts: [AccountWithInfo], peers: [(RenderedPeer, PeerPresence?)], accountPeer: Peer, defaultAction: ShareControllerAction?) { + self.context = context if let peersContentNode = self.peersContentNode, peersContentNode.accountPeer.id == accountPeer.id { peersContentNode.peersValue.set(.single(peers)) return } + if let peerId = self.immediatePeerId { + self.immediatePeerId = nil + let _ = (context.account.postbox.transaction { transaction -> RenderedPeer? in + return transaction.getPeer(peerId).flatMap(RenderedPeer.init(peer:)) + } |> deliverOnMainQueue).start(next: { [weak self] peer in + if let strongSelf = self, let peer = peer { + strongSelf.controllerInteraction?.togglePeer(peer, peer.peerId != context.account.peerId) + } + }) + } + let animated = self.peersContentNode == nil - let peersContentNode = SharePeersContainerNode(sharedContext: self.sharedContext, account: account, switchableAccounts: switchableAccounts, theme: self.presentationData.theme, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, peers: peers, accountPeer: accountPeer, controllerInteraction: self.controllerInteraction!, externalShare: self.externalShare, switchToAnotherAccount: { [weak self] in + let peersContentNode = SharePeersContainerNode(sharedContext: self.sharedContext, context: context, switchableAccounts: switchableAccounts, theme: self.presentationData.theme, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, peers: peers, accountPeer: accountPeer, controllerInteraction: self.controllerInteraction!, externalShare: self.externalShare, switchToAnotherAccount: { [weak self] in self?.switchToAnotherAccount?() }) self.peersContentNode = peersContentNode peersContentNode.openSearch = { [weak self] in - let _ = (recentlySearchedPeers(postbox: account.postbox) + let _ = (recentlySearchedPeers(postbox: context.account.postbox) |> take(1) |> deliverOnMainQueue).start(next: { peers in if let strongSelf = self { - let searchContentNode = ShareSearchContainerNode(sharedContext: strongSelf.sharedContext, account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, controllerInteraction: strongSelf.controllerInteraction!, recentPeers: peers.filter({ $0.peer.peerId.namespace != Namespaces.Peer.SecretChat }).map({ $0.peer })) + let searchContentNode = ShareSearchContainerNode(sharedContext: strongSelf.sharedContext, context: context, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, controllerInteraction: strongSelf.controllerInteraction!, recentPeers: peers.filter({ $0.peer.peerId.namespace != Namespaces.Peer.SecretChat }).map({ $0.peer })) searchContentNode.cancel = { if let strongSelf = self, let peersContentNode = strongSelf.peersContentNode { strongSelf.transitionToContentNode(peersContentNode) diff --git a/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift b/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift index 39b093c0f1..b65267d704 100644 --- a/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift +++ b/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift @@ -10,6 +10,7 @@ import TelegramPresentationData import TelegramStringFormatting import SelectablePeerNode import PeerPresenceStatusManager +import AccountContext final class ShareControllerInteraction { var foundPeers: [RenderedPeer] = [] @@ -86,7 +87,7 @@ final class ShareControllerGridSectionNode: ASDisplayNode { } final class ShareControllerPeerGridItem: GridItem { - let account: Account + let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let peer: RenderedPeer @@ -96,8 +97,8 @@ final class ShareControllerPeerGridItem: GridItem { let section: GridSection? - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, peer: RenderedPeer, presence: PeerPresence?, controllerInteraction: ShareControllerInteraction, sectionTitle: String? = nil, search: Bool = false) { - self.account = account + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: RenderedPeer, presence: PeerPresence?, controllerInteraction: ShareControllerInteraction, sectionTitle: String? = nil, search: Bool = false) { + self.context = context self.theme = theme self.strings = strings self.peer = peer @@ -115,7 +116,7 @@ final class ShareControllerPeerGridItem: GridItem { func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { let node = ShareControllerPeerGridItemNode() node.controllerInteraction = self.controllerInteraction - node.setup(account: self.account, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, search: self.search, synchronousLoad: synchronousLoad, force: false) + node.setup(context: self.context, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, search: self.search, synchronousLoad: synchronousLoad, force: false) return node } @@ -125,12 +126,12 @@ final class ShareControllerPeerGridItem: GridItem { return } node.controllerInteraction = self.controllerInteraction - node.setup(account: self.account, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, search: self.search, synchronousLoad: false, force: false) + node.setup(context: self.context, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, search: self.search, synchronousLoad: false, force: false) } } final class ShareControllerPeerGridItemNode: GridItemNode { - private var currentState: (Account, PresentationTheme, PresentationStrings, RenderedPeer, Bool, PeerPresence?)? + private var currentState: (AccountContext, PresentationTheme, PresentationStrings, RenderedPeer, Bool, PeerPresence?)? private let peerNode: SelectablePeerNode private var presenceManager: PeerPresenceStatusManager? @@ -155,17 +156,17 @@ final class ShareControllerPeerGridItemNode: GridItemNode { guard let strongSelf = self, let currentState = strongSelf.currentState else { return } - strongSelf.setup(account: currentState.0, theme: currentState.1, strings: currentState.2, peer: currentState.3, presence: currentState.5, search: currentState.4, synchronousLoad: false, force: true) + strongSelf.setup(context: currentState.0, theme: currentState.1, strings: currentState.2, peer: currentState.3, presence: currentState.5, search: currentState.4, synchronousLoad: false, force: true) }) } - func setup(account: Account, theme: PresentationTheme, strings: PresentationStrings, peer: RenderedPeer, presence: PeerPresence?, search: Bool, synchronousLoad: Bool, force: Bool) { - if force || self.currentState == nil || self.currentState!.0 !== account || self.currentState!.3 != peer || !arePeerPresencesEqual(self.currentState!.5, presence) { + func setup(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: RenderedPeer, presence: PeerPresence?, search: Bool, synchronousLoad: Bool, force: Bool) { + if force || self.currentState == nil || self.currentState!.0 !== context || self.currentState!.3 != peer || !arePeerPresencesEqual(self.currentState!.5, presence) { let itemTheme = SelectablePeerNodeTheme(textColor: theme.actionSheet.primaryTextColor, secretTextColor: theme.chatList.secretTitleColor, selectedTextColor: theme.actionSheet.controlAccentColor, checkBackgroundColor: theme.actionSheet.opaqueItemBackgroundColor, checkFillColor: theme.actionSheet.controlAccentColor, checkColor: theme.actionSheet.checkContentColor, avatarPlaceholderColor: theme.list.mediaPlaceholderColor) let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) var online = false - if let peer = peer.peer as? TelegramUser, let presence = presence as? TelegramUserPresence, !isServicePeer(peer) && !peer.flags.contains(.isSupport) && peer.id != account.peerId { + if let peer = peer.peer as? TelegramUser, let presence = presence as? TelegramUserPresence, !isServicePeer(peer) && !peer.flags.contains(.isSupport) && peer.id != context.account.peerId { let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: timestamp) if case .online = relativeStatus { online = true @@ -173,8 +174,8 @@ final class ShareControllerPeerGridItemNode: GridItemNode { } self.peerNode.theme = itemTheme - self.peerNode.setup(account: account, theme: theme, strings: strings, peer: peer, online: online, synchronousLoad: synchronousLoad) - self.currentState = (account, theme, strings, peer, search, presence) + self.peerNode.setup(context: context, theme: theme, strings: strings, peer: peer, online: online, synchronousLoad: synchronousLoad) + self.currentState = (context, theme, strings, peer, search, presence) self.setNeedsLayout() if let presence = presence as? TelegramUserPresence { self.presenceManager?.reset(presence: presence) diff --git a/submodules/ShareController/Sources/ShareControllerRecentPeersGridItem.swift b/submodules/ShareController/Sources/ShareControllerRecentPeersGridItem.swift index d41db34ba2..59f4f620f1 100644 --- a/submodules/ShareController/Sources/ShareControllerRecentPeersGridItem.swift +++ b/submodules/ShareController/Sources/ShareControllerRecentPeersGridItem.swift @@ -8,9 +8,10 @@ import AsyncDisplayKit import Postbox import TelegramPresentationData import ChatListSearchRecentPeersNode +import AccountContext final class ShareControllerRecentPeersGridItem: GridItem { - let account: Account + let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let controllerInteraction: ShareControllerInteraction @@ -18,8 +19,8 @@ final class ShareControllerRecentPeersGridItem: GridItem { let section: GridSection? = nil let fillsRowWithHeight: CGFloat? = 130.0 - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ShareControllerInteraction) { - self.account = account + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ShareControllerInteraction) { + self.context = context self.theme = theme self.strings = strings self.controllerInteraction = controllerInteraction @@ -28,7 +29,7 @@ final class ShareControllerRecentPeersGridItem: GridItem { func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { let node = ShareControllerRecentPeersGridItemNode() node.controllerInteraction = self.controllerInteraction - node.setup(account: self.account, theme: self.theme, strings: self.strings) + node.setup(context: self.context, theme: self.theme, strings: self.strings) return node } @@ -38,12 +39,12 @@ final class ShareControllerRecentPeersGridItem: GridItem { return } node.controllerInteraction = self.controllerInteraction - node.setup(account: self.account, theme: self.theme, strings: self.strings) + node.setup(context: self.context, theme: self.theme, strings: self.strings) } } final class ShareControllerRecentPeersGridItemNode: GridItemNode { - private var currentState: (Account, PresentationTheme, PresentationStrings)? + private var currentState: (AccountContext, PresentationTheme, PresentationStrings)? var controllerInteraction: ShareControllerInteraction? @@ -53,14 +54,14 @@ final class ShareControllerRecentPeersGridItemNode: GridItemNode { super.init() } - func setup(account: Account, theme: PresentationTheme, strings: PresentationStrings) { - if self.currentState == nil || self.currentState!.0 !== account { + func setup(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { + if self.currentState == nil || self.currentState!.0 !== context { let peersNode: ChatListSearchRecentPeersNode if let currentPeersNode = self.peersNode { peersNode = currentPeersNode peersNode.updateThemeAndStrings(theme: theme, strings: strings) } else { - peersNode = ChatListSearchRecentPeersNode(account: account, theme: theme, mode: .actionSheet, strings: strings, peerSelected: { [weak self] peer in + peersNode = ChatListSearchRecentPeersNode(context: context, theme: theme, mode: .actionSheet, strings: strings, peerSelected: { [weak self] peer in self?.controllerInteraction?.togglePeer(RenderedPeer(peer: peer), true) }, peerContextAction: { _, _, gesture in gesture?.cancel() }, isPeerSelected: { [weak self] peerId in return self?.controllerInteraction?.selectedPeerIds.contains(peerId) ?? false @@ -69,7 +70,7 @@ final class ShareControllerRecentPeersGridItemNode: GridItemNode { self.addSubnode(peersNode) } - self.currentState = (account, theme, strings) + self.currentState = (context, theme, strings) } self.updateSelection(animated: false) } diff --git a/submodules/ShareController/Sources/SharePeersContainerNode.swift b/submodules/ShareController/Sources/SharePeersContainerNode.swift index 935fb5039d..1d587de8c2 100644 --- a/submodules/ShareController/Sources/SharePeersContainerNode.swift +++ b/submodules/ShareController/Sources/SharePeersContainerNode.swift @@ -48,8 +48,8 @@ private struct SharePeerEntry: Comparable, Identifiable { return lhs.index < rhs.index } - func item(account: Account, interfaceInteraction: ShareControllerInteraction) -> GridItem { - return ShareControllerPeerGridItem(account: account, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, controllerInteraction: interfaceInteraction, search: false) + func item(context: AccountContext, interfaceInteraction: ShareControllerInteraction) -> GridItem { + return ShareControllerPeerGridItem(context: context, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, controllerInteraction: interfaceInteraction, search: false) } } @@ -62,19 +62,19 @@ private struct ShareGridTransaction { private let avatarFont = avatarPlaceholderFont(size: 17.0) -private func preparedGridEntryTransition(account: Account, from fromEntries: [SharePeerEntry], to toEntries: [SharePeerEntry], interfaceInteraction: ShareControllerInteraction) -> ShareGridTransaction { +private func preparedGridEntryTransition(context: AccountContext, from fromEntries: [SharePeerEntry], to toEntries: [SharePeerEntry], interfaceInteraction: ShareControllerInteraction) -> ShareGridTransaction { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices - let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction), previousIndex: $0.2) } - let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction)) } + let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(context: context, interfaceInteraction: interfaceInteraction), previousIndex: $0.2) } + let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, interfaceInteraction: interfaceInteraction)) } return ShareGridTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: false) } final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { private let sharedContext: SharedAccountContext - private let account: Account + private let context: AccountContext private let theme: PresentationTheme private let strings: PresentationStrings private let nameDisplayOrder: PresentationPersonNameOrder @@ -107,9 +107,9 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { let peersValue = Promise<[(RenderedPeer, PeerPresence?)]>() - init(sharedContext: SharedAccountContext, account: Account, switchableAccounts: [AccountWithInfo], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(RenderedPeer, PeerPresence?)], accountPeer: Peer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void) { + init(sharedContext: SharedAccountContext, context: AccountContext, switchableAccounts: [AccountWithInfo], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(RenderedPeer, PeerPresence?)], accountPeer: Peer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void) { self.sharedContext = sharedContext - self.account = account + self.context = context self.theme = theme self.strings = strings self.nameDisplayOrder = nameDisplayOrder @@ -159,9 +159,9 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { self.contentTitleAccountNode = AvatarNode(font: avatarFont) var hasOtherAccounts = false - if switchableAccounts.count > 1, let info = switchableAccounts.first(where: { $0.account.id == account.id }) { + if switchableAccounts.count > 1, let info = switchableAccounts.first(where: { $0.account.id == context.account.id }) { hasOtherAccounts = true - self.contentTitleAccountNode.setPeer(account: account, theme: theme, peer: info.peer, emptyColor: nil, synchronousLoad: false) + self.contentTitleAccountNode.setPeer(context: context, theme: theme, peer: info.peer, emptyColor: nil, synchronousLoad: false) } else { self.contentTitleAccountNode.isHidden = true } @@ -200,7 +200,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { strongSelf.entries = entries let firstTime = previousEntries == nil - let transition = preparedGridEntryTransition(account: account, from: previousEntries ?? [], to: entries, interfaceInteraction: controllerInteraction) + let transition = preparedGridEntryTransition(context: context, from: previousEntries ?? [], to: entries, interfaceInteraction: controllerInteraction) strongSelf.enqueueTransition(transition, firstTime: firstTime) } })) diff --git a/submodules/ShareController/Sources/ShareSearchContainerNode.swift b/submodules/ShareController/Sources/ShareSearchContainerNode.swift index 96b39da92c..2cba52dc8b 100644 --- a/submodules/ShareController/Sources/ShareSearchContainerNode.swift +++ b/submodules/ShareController/Sources/ShareSearchContainerNode.swift @@ -94,17 +94,17 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable { } } - func item(account: Account, interfaceInteraction: ShareControllerInteraction) -> GridItem { + func item(context: AccountContext, interfaceInteraction: ShareControllerInteraction) -> GridItem { switch self { case let .topPeers(theme, strings): - return ShareControllerRecentPeersGridItem(account: account, theme: theme, strings: strings, controllerInteraction: interfaceInteraction) + return ShareControllerRecentPeersGridItem(context: context, theme: theme, strings: strings, controllerInteraction: interfaceInteraction) case let .peer(_, theme, peer, associatedPeer, presence, strings): var peers: [PeerId: Peer] = [peer.id: peer] if let associatedPeer = associatedPeer { peers[associatedPeer.id] = associatedPeer } let peer = RenderedPeer(peerId: peer.id, peers: SimpleDictionary(peers)) - return ShareControllerPeerGridItem(account: account, theme: theme, strings: strings, peer: peer, presence: presence, controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent, search: true) + return ShareControllerPeerGridItem(context: context, theme: theme, strings: strings, peer: peer, presence: presence, controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent, search: true) } } } @@ -134,8 +134,8 @@ private struct ShareSearchPeerEntry: Comparable, Identifiable { return lhs.index < rhs.index } - func item(account: Account, interfaceInteraction: ShareControllerInteraction) -> GridItem { - return ShareControllerPeerGridItem(account: account, theme: self.theme, strings: self.strings, peer: peer, presence: self.presence, controllerInteraction: interfaceInteraction, search: true) + func item(context: AccountContext, interfaceInteraction: ShareControllerInteraction) -> GridItem { + return ShareControllerPeerGridItem(context: context, theme: self.theme, strings: self.strings, peer: peer, presence: self.presence, controllerInteraction: interfaceInteraction, search: true) } } @@ -146,29 +146,29 @@ private struct ShareSearchGridTransaction { let animated: Bool } -private func preparedGridEntryTransition(account: Account, from fromEntries: [ShareSearchPeerEntry], to toEntries: [ShareSearchPeerEntry], interfaceInteraction: ShareControllerInteraction) -> ShareSearchGridTransaction { +private func preparedGridEntryTransition(context: AccountContext, from fromEntries: [ShareSearchPeerEntry], to toEntries: [ShareSearchPeerEntry], interfaceInteraction: ShareControllerInteraction) -> ShareSearchGridTransaction { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices - let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction), previousIndex: $0.2) } - let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction)) } + let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(context: context, interfaceInteraction: interfaceInteraction), previousIndex: $0.2) } + let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, interfaceInteraction: interfaceInteraction)) } return ShareSearchGridTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: false) } -private func preparedRecentEntryTransition(account: Account, from fromEntries: [ShareSearchRecentEntry], to toEntries: [ShareSearchRecentEntry], interfaceInteraction: ShareControllerInteraction) -> ShareSearchGridTransaction { +private func preparedRecentEntryTransition(context: AccountContext, from fromEntries: [ShareSearchRecentEntry], to toEntries: [ShareSearchRecentEntry], interfaceInteraction: ShareControllerInteraction) -> ShareSearchGridTransaction { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices - let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction), previousIndex: $0.2) } - let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction)) } + let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(context: context, interfaceInteraction: interfaceInteraction), previousIndex: $0.2) } + let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, interfaceInteraction: interfaceInteraction)) } return ShareSearchGridTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: false) } final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { private let sharedContext: SharedAccountContext - private let account: Account + private let context: AccountContext private let strings: PresentationStrings private let controllerInteraction: ShareControllerInteraction @@ -198,9 +198,9 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { private let searchQuery = ValuePromise("", ignoreRepeated: true) private let searchDisposable = MetaDisposable() - init(sharedContext: SharedAccountContext, account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ShareControllerInteraction, recentPeers recentPeerList: [RenderedPeer]) { + init(sharedContext: SharedAccountContext, context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ShareControllerInteraction, recentPeers recentPeerList: [RenderedPeer]) { self.sharedContext = sharedContext - self.account = account + self.context = context self.strings = strings self.controllerInteraction = controllerInteraction @@ -245,11 +245,11 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { let foundItems = searchQuery.get() |> mapToSignal { query -> Signal<[ShareSearchPeerEntry]?, NoError> in if !query.isEmpty { - let accountPeer = account.postbox.loadedPeerWithId(account.peerId) |> take(1) - let foundLocalPeers = account.postbox.searchPeers(query: query.lowercased()) + let accountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> take(1) + let foundLocalPeers = context.account.postbox.searchPeers(query: query.lowercased()) let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError> = .single(([], [])) |> then( - searchPeers(account: account, query: query) + searchPeers(account: context.account, query: query) |> delay(0.2, queue: Queue.concurrentDefaultQueue()) ) @@ -312,7 +312,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { strongSelf.entries = entries ?? [] let firstTime = previousEntries == nil - let transition = preparedGridEntryTransition(account: account, from: previousEntries ?? [], to: entries ?? [], interfaceInteraction: controllerInteraction) + let transition = preparedGridEntryTransition(context: context, from: previousEntries ?? [], to: entries ?? [], interfaceInteraction: controllerInteraction) strongSelf.enqueueTransition(transition, firstTime: firstTime) if (previousEntries == nil) != (entries == nil) { @@ -333,7 +333,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { self?.searchQuery.set(text) } - let hasRecentPeers = recentPeers(account: account) + let hasRecentPeers = recentPeers(account: context.account) |> map { value -> Bool in switch value { case let .peers(peers): @@ -367,7 +367,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { strongSelf.recentEntries = entries let firstTime = previousEntries == nil - let transition = preparedRecentEntryTransition(account: account, from: previousEntries ?? [], to: entries, interfaceInteraction: controllerInteraction) + let transition = preparedRecentEntryTransition(context: context, from: previousEntries ?? [], to: entries, interfaceInteraction: controllerInteraction) strongSelf.enqueueRecentTransition(transition, firstTime: firstTime) } })) diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index 3d7de90655..2bfd0b4880 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -21,7 +21,8 @@ public final class SolidRoundedButtonNode: ASDisplayNode { private let buttonBackgroundNode: ASImageNode private let buttonGlossNode: SolidRoundedButtonGlossNode private let buttonNode: HighlightTrackingButtonNode - private let labelNode: ImmediateTextNode + private let titleNode: ImmediateTextNode + private let subtitleNode: ImmediateTextNode private let iconNode: ASImageNode private let buttonHeight: CGFloat @@ -38,6 +39,14 @@ public final class SolidRoundedButtonNode: ASDisplayNode { } } + public var subtitle: String? { + didSet { + if let width = self.validLayout { + _ = self.updateLayout(width: width, previousSubtitle: oldValue, transition: .immediate) + } + } + } + public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) { self.theme = theme self.buttonHeight = height @@ -53,8 +62,11 @@ public final class SolidRoundedButtonNode: ASDisplayNode { self.buttonNode = HighlightTrackingButtonNode() - self.labelNode = ImmediateTextNode() - self.labelNode.isUserInteractionEnabled = false + self.titleNode = ImmediateTextNode() + self.titleNode.isUserInteractionEnabled = false + + self.subtitleNode = ImmediateTextNode() + self.subtitleNode.isUserInteractionEnabled = false self.iconNode = ASImageNode() self.iconNode.displaysAsynchronously = false @@ -67,7 +79,8 @@ public final class SolidRoundedButtonNode: ASDisplayNode { self.addSubnode(self.buttonGlossNode) } self.addSubnode(self.buttonNode) - self.addSubnode(self.labelNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.subtitleNode) self.addSubnode(self.iconNode) self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) @@ -76,15 +89,19 @@ public final class SolidRoundedButtonNode: ASDisplayNode { if highlighted { strongSelf.buttonBackgroundNode.layer.removeAnimation(forKey: "opacity") strongSelf.buttonBackgroundNode.alpha = 0.55 - strongSelf.labelNode.layer.removeAnimation(forKey: "opacity") - strongSelf.labelNode.alpha = 0.55 + strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") + strongSelf.titleNode.alpha = 0.55 + strongSelf.subtitleNode.layer.removeAnimation(forKey: "opacity") + strongSelf.subtitleNode.alpha = 0.55 strongSelf.iconNode.layer.removeAnimation(forKey: "opacity") strongSelf.iconNode.alpha = 0.55 } else { strongSelf.buttonBackgroundNode.alpha = 1.0 strongSelf.buttonBackgroundNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) - strongSelf.labelNode.alpha = 1.0 - strongSelf.labelNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) + strongSelf.titleNode.alpha = 1.0 + strongSelf.titleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) + strongSelf.subtitleNode.alpha = 1.0 + strongSelf.subtitleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) strongSelf.iconNode.alpha = 1.0 strongSelf.iconNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) } @@ -100,10 +117,15 @@ public final class SolidRoundedButtonNode: ASDisplayNode { self.buttonBackgroundNode.image = generateStretchableFilledCircleImage(radius: self.buttonCornerRadius, color: theme.backgroundColor) self.buttonGlossNode.color = theme.foregroundColor - self.labelNode.attributedText = NSAttributedString(string: self.title ?? "", font: Font.semibold(17.0), textColor: theme.foregroundColor) + self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: Font.semibold(17.0), textColor: theme.foregroundColor) + self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: theme.foregroundColor) } public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + return self.updateLayout(width: width, previousSubtitle: nil, transition: transition) + } + + private func updateLayout(width: CGFloat, previousSubtitle: String?, transition: ContainedViewLayoutTransition) -> CGFloat { self.validLayout = width let buttonSize = CGSize(width: width, height: self.buttonHeight) @@ -112,16 +134,16 @@ public final class SolidRoundedButtonNode: ASDisplayNode { transition.updateFrame(node: self.buttonGlossNode, frame: buttonFrame) transition.updateFrame(node: self.buttonNode, frame: buttonFrame) - if self.title != self.labelNode.attributedText?.string { - self.labelNode.attributedText = NSAttributedString(string: self.title ?? "", font: Font.semibold(17.0), textColor: self.theme.foregroundColor) + if self.title != self.titleNode.attributedText?.string { + self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: Font.semibold(17.0), textColor: self.theme.foregroundColor) } let iconSize = self.iconNode.image?.size ?? CGSize() - let labelSize = self.labelNode.updateLayout(buttonSize) + let titleSize = self.titleNode.updateLayout(buttonSize) let iconSpacing: CGFloat = 8.0 - var contentWidth: CGFloat = labelSize.width + var contentWidth: CGFloat = titleSize.width if !iconSize.width.isZero { contentWidth += iconSize.width + iconSpacing } @@ -131,8 +153,25 @@ public final class SolidRoundedButtonNode: ASDisplayNode { nextContentOrigin += iconSize.width + iconSpacing } - let labelFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: buttonFrame.minY + floor((buttonFrame.height - labelSize.height) / 2.0)), size: labelSize) - transition.updateFrame(node: self.labelNode, frame: labelFrame) + let spacingOffset: CGFloat = 9.0 + var verticalInset: CGFloat = self.subtitle == nil ? floor((buttonFrame.height - titleSize.height) / 2.0) : floor((buttonFrame.height - titleSize.height) / 2.0) - spacingOffset + + let titleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: buttonFrame.minY + verticalInset), size: titleSize) + transition.updateFrame(node: self.titleNode, frame: titleFrame) + + if self.subtitle != self.subtitleNode.attributedText?.string { + self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: self.theme.foregroundColor) + } + + let subtitleSize = self.subtitleNode.updateLayout(buttonSize) + let subtitleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - subtitleSize.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - titleSize.height) / 2.0) + spacingOffset + 2.0), size: subtitleSize) + transition.updateFrame(node: self.subtitleNode, frame: subtitleFrame) + + if previousSubtitle == nil && self.subtitle != nil { + self.titleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: spacingOffset / 2.0), to: CGPoint(), duration: 0.3, additive: true) + self.subtitleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -spacingOffset / 2.0), to: CGPoint(), duration: 0.3, additive: true) + self.subtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } return buttonSize.height } @@ -162,7 +201,7 @@ public final class SolidRoundedButtonGlossNode: ASDisplayNode { } } private var progress: CGFloat = 0.0 - private var displayLink: CADisplayLink? + private var animator: ConstantDisplayLinkAnimator? private let buttonCornerRadius: CGFloat private var gradientColors: NSArray? @@ -175,28 +214,33 @@ public final class SolidRoundedButtonGlossNode: ASDisplayNode { self.isOpaque = false self.isLayerBacked = true - class DisplayLinkProxy: NSObject { - weak var target: SolidRoundedButtonGlossNode? - init(target: SolidRoundedButtonGlossNode) { - self.target = target + var previousTime: CFAbsoluteTime? + self.animator = ConstantDisplayLinkAnimator(update: { [weak self] in + guard let strongSelf = self else { + return } - - @objc func displayLinkEvent() { - self.target?.displayLinkEvent() + let currentTime = CFAbsoluteTimeGetCurrent() + if let previousTime = previousTime { + var delta: CGFloat + if strongSelf.progress < 0.05 || strongSelf.progress > 0.95 { + delta = 0.001 + } else { + delta = 0.009 + } + delta *= CGFloat(currentTime - previousTime) * 60.0 + var newProgress = strongSelf.progress + delta + if newProgress > 1.0 { + newProgress = 0.0 + } + strongSelf.progress = newProgress + strongSelf.setNeedsDisplay() } - } - - self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent)) - self.displayLink?.isPaused = true - self.displayLink?.add(to: RunLoop.main, forMode: .common) + previousTime = currentTime + }) self.updateGradientColors() } - deinit { - self.displayLink?.invalidate() - } - private func updateGradientColors() { let transparentColor = self.color.withAlphaComponent(0.0).cgColor self.gradientColors = [transparentColor, transparentColor, self.color.withAlphaComponent(0.12).cgColor, transparentColor, transparentColor] @@ -204,27 +248,12 @@ public final class SolidRoundedButtonGlossNode: ASDisplayNode { override public func willEnterHierarchy() { super.willEnterHierarchy() - self.displayLink?.isPaused = false + self.animator?.isPaused = false } override public func didExitHierarchy() { super.didExitHierarchy() - self.displayLink?.isPaused = true - } - - private func displayLinkEvent() { - let delta: CGFloat - if self.progress < 0.05 || self.progress > 0.95 { - delta = 0.001 - } else { - delta = 0.009 - } - var newProgress = self.progress + delta - if newProgress > 1.0 { - newProgress = 0.0 - } - self.progress = newProgress - self.setNeedsDisplay() + self.animator?.isPaused = true } override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { diff --git a/submodules/SpotlightSupport/BUCK b/submodules/SpotlightSupport/BUCK new file mode 100644 index 0000000000..abda309429 --- /dev/null +++ b/submodules/SpotlightSupport/BUCK @@ -0,0 +1,19 @@ +load("//Config:buck_rule_macros.bzl", "framework") + +framework( + name = "SpotlightSupport", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared", + "//submodules/Postbox:Postbox#shared", + "//submodules/SyncCore:SyncCore#shared", + "//submodules/TelegramCore:TelegramCore#shared", + "//submodules/Display:Display#shared", + ], + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/CoreSpotlight.framework", + ], +) diff --git a/submodules/StatisticsUI/BUCK b/submodules/StatisticsUI/BUCK new file mode 100644 index 0000000000..693dcc8cbc --- /dev/null +++ b/submodules/StatisticsUI/BUCK @@ -0,0 +1,30 @@ +load("//Config:buck_rule_macros.bzl", "static_library") + +static_library( + name = "StatisticsUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared", + "//submodules/AsyncDisplayKit:AsyncDisplayKit#shared", + "//submodules/Display:Display#shared", + "//submodules/Postbox:Postbox#shared", + "//submodules/TelegramCore:TelegramCore#shared", + "//submodules/SyncCore:SyncCore#shared", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/AccountContext:AccountContext", + "//submodules/ItemListUI:ItemListUI", + "//submodules/AvatarNode:AvatarNode", + "//submodules/TelegramStringFormatting:TelegramStringFormatting", + "//submodules/AlertUI:AlertUI", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/MergeLists:MergeLists", + "//submodules/Charts:Charts", + ], + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/UIKit.framework", + ], +) diff --git a/submodules/StatisticsUI/Info.plist b/submodules/StatisticsUI/Info.plist new file mode 100644 index 0000000000..e1fe4cfb7b --- /dev/null +++ b/submodules/StatisticsUI/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/submodules/StatisticsUI/Sources/StatisticsUI.h b/submodules/StatisticsUI/Sources/StatisticsUI.h new file mode 100644 index 0000000000..89573b3f16 --- /dev/null +++ b/submodules/StatisticsUI/Sources/StatisticsUI.h @@ -0,0 +1,19 @@ +// +// StatisticsUI.h +// StatisticsUI +// +// Created by Peter on 8/13/19. +// Copyright © 2019 Telegram Messenger LLP. All rights reserved. +// + +#import + +//! Project version number for StatisticsUI. +FOUNDATION_EXPORT double StatisticsUIVersionNumber; + +//! Project version string for StatisticsUI. +FOUNDATION_EXPORT const unsigned char StatisticsUIVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/submodules/StatisticsUI/Sources/StatsController.swift b/submodules/StatisticsUI/Sources/StatsController.swift new file mode 100644 index 0000000000..e24f2dc32b --- /dev/null +++ b/submodules/StatisticsUI/Sources/StatsController.swift @@ -0,0 +1,349 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import SyncCore +import MapKit +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import PresentationDataUtils +import AppBundle + +private final class StatsArguments { + init() { + } +} + +private enum StatsSection: Int32 { + case overview + case growth + case followers + case notifications + case viewsByHour + case postInteractions + case viewsBySource + case followersBySource + case languages +} + +private enum StatsEntry: ItemListNodeEntry { + case overviewHeader(PresentationTheme, String) + case overview(PresentationTheme, ChannelStats) + + case growthTitle(PresentationTheme, String) + case growthGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, String, ChannelStatsGraph) + + case followersTitle(PresentationTheme, String) + case followersGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, String, ChannelStatsGraph) + + case notificationsTitle(PresentationTheme, String) + case notificationsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, String, ChannelStatsGraph) + + case viewsByHourTitle(PresentationTheme, String) + case viewsByHourGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, String, ChannelStatsGraph) + + case postInteractionsTitle(PresentationTheme, String) + case postInteractionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, String, ChannelStatsGraph) + + case viewsBySourceTitle(PresentationTheme, String) + case viewsBySourceGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, String, ChannelStatsGraph) + + case followersBySourceTitle(PresentationTheme, String) + case followersBySourceGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, String, ChannelStatsGraph) + + case languagesTitle(PresentationTheme, String) + case languagesGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, String, ChannelStatsGraph) + + var section: ItemListSectionId { + switch self { + case .overviewHeader, .overview: + return StatsSection.overview.rawValue + case .growthTitle, .growthGraph: + return StatsSection.growth.rawValue + case .followersTitle, .followersGraph: + return StatsSection.followers.rawValue + case .notificationsTitle, .notificationsGraph: + return StatsSection.notifications.rawValue + case .viewsByHourTitle, .viewsByHourGraph: + return StatsSection.viewsByHour.rawValue + case .postInteractionsTitle, .postInteractionsGraph: + return StatsSection.postInteractions.rawValue + case .viewsBySourceTitle, .viewsBySourceGraph: + return StatsSection.viewsBySource.rawValue + case .followersBySourceTitle, .followersBySourceGraph: + return StatsSection.followersBySource.rawValue + case .languagesTitle, .languagesGraph: + return StatsSection.languages.rawValue + } + } + + var stableId: Int32 { + switch self { + case .overviewHeader: + return 0 + case .overview: + return 1 + case .growthTitle: + return 2 + case .growthGraph: + return 3 + case .followersTitle: + return 4 + case .followersGraph: + return 5 + case .notificationsTitle: + return 6 + case .notificationsGraph: + return 7 + case .viewsByHourTitle: + return 8 + case .viewsByHourGraph: + return 9 + case .postInteractionsTitle: + return 10 + case .postInteractionsGraph: + return 11 + case .viewsBySourceTitle: + return 12 + case .viewsBySourceGraph: + return 13 + case .followersBySourceTitle: + return 14 + case .followersBySourceGraph: + return 15 + case .languagesTitle: + return 16 + case .languagesGraph: + return 17 + } + } + + static func ==(lhs: StatsEntry, rhs: StatsEntry) -> Bool { + switch lhs { + case let .overviewHeader(lhsTheme, lhsText): + if case let .overviewHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .overview(lhsTheme, lhsStats): + if case let .overview(rhsTheme, rhsStats) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats { + return true + } else { + return false + } + case let .growthTitle(lhsTheme, lhsText): + if case let .growthTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .growthGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsText, lhsGraph): + if case let .growthGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsText, rhsGraph) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsText == rhsText, lhsGraph == rhsGraph { + return true + } else { + return false + } + case let .followersTitle(lhsTheme, lhsText): + if case let .followersTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .followersGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsText, lhsGraph): + if case let .followersGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsText, rhsGraph) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsText == rhsText, lhsGraph == rhsGraph { + return true + } else { + return false + } + case let .notificationsTitle(lhsTheme, lhsText): + if case let .notificationsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .notificationsGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsText, lhsGraph): + if case let .notificationsGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsText, rhsGraph) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsText == rhsText, lhsGraph == rhsGraph { + return true + } else { + return false + } + case let .viewsByHourTitle(lhsTheme, lhsText): + if case let .viewsByHourTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .viewsByHourGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsText, lhsGraph): + if case let .viewsByHourGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsText, rhsGraph) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsText == rhsText, lhsGraph == rhsGraph { + return true + } else { + return false + } + case let .postInteractionsTitle(lhsTheme, lhsText): + if case let .postInteractionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .postInteractionsGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsText, lhsGraph): + if case let .postInteractionsGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsText, rhsGraph) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsText == rhsText, lhsGraph == rhsGraph { + return true + } else { + return false + } + case let .viewsBySourceTitle(lhsTheme, lhsText): + if case let .viewsBySourceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .viewsBySourceGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsText, lhsGraph): + if case let .viewsBySourceGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsText, rhsGraph) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsText == rhsText, lhsGraph == rhsGraph { + return true + } else { + return false + } + case let .followersBySourceTitle(lhsTheme, lhsText): + if case let .followersBySourceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .followersBySourceGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsText, lhsGraph): + if case let .followersBySourceGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsText, rhsGraph) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsText == rhsText, lhsGraph == rhsGraph { + return true + } else { + return false + } + case let .languagesTitle(lhsTheme, lhsText): + if case let .languagesTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .languagesGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsText, lhsGraph): + if case let .languagesGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsText, rhsGraph) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsText == rhsText, lhsGraph == rhsGraph { + return true + } else { + return false + } + } + } + + static func <(lhs: StatsEntry, rhs: StatsEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + switch self { + case let .overviewHeader(theme, text), + let .growthTitle(theme, text), + let .followersTitle(theme, text), + let .notificationsTitle(theme, text), + let .viewsByHourTitle(theme, text), + let .postInteractionsTitle(theme, text), + let .viewsBySourceTitle(theme, text), + let .followersBySourceTitle(theme, text), + let .languagesTitle(theme, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .overview(theme, stats): + return StatsOverviewItem(presentationData: presentationData, stats: stats, sectionId: self.section, style: .blocks) + case let .growthGraph(theme, strings, dateTimeFormat, title, graph), + let .followersGraph(theme, strings, dateTimeFormat, title, graph), + let .notificationsGraph(theme, strings, dateTimeFormat, title, graph), + let .viewsByHourGraph(theme, strings, dateTimeFormat, title, graph), + let .postInteractionsGraph(theme, strings, dateTimeFormat, title, graph), + let .viewsBySourceGraph(theme, strings, dateTimeFormat, title, graph), + let .followersBySourceGraph(theme, strings, dateTimeFormat, title, graph), + let .languagesGraph(theme, strings, dateTimeFormat, title, graph): + return StatsGraphItem(presentationData: presentationData, title: title, graph: graph, sectionId: self.section, style: .blocks) + } + } +} + +private func statsControllerEntries(data: ChannelStats?, presentationData: PresentationData) -> [StatsEntry] { + var entries: [StatsEntry] = [] + + if let data = data { + entries.append(.overviewHeader(presentationData.theme, "OVERVIEW")) + entries.append(.overview(presentationData.theme, data)) + + entries.append(.growthTitle(presentationData.theme, "GROWTH")) + entries.append(.growthGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, "Growth", data.growthGraph)) + + entries.append(.followersTitle(presentationData.theme, "FOLLOWERS")) + entries.append(.followersGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, "Followers", data.followersGraph)) + + entries.append(.notificationsTitle(presentationData.theme, "NOTIFICATIONS")) + entries.append(.notificationsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, "Notifications", data.muteGraph)) + } + + return entries +} + +public func channelStatsController(context: AccountContext, peer: Peer, cachedPeerData: CachedPeerData) -> ViewController { + var pushControllerImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var navigateToChatImpl: ((Peer) -> Void)? + + let actionsDisposable = DisposableSet() + let checkCreationAvailabilityDisposable = MetaDisposable() + actionsDisposable.add(checkCreationAvailabilityDisposable) + + let dataPromise = Promise(nil) + + var datacenterId: Int32 = 0 + if let cachedData = cachedPeerData as? CachedChannelData { + datacenterId = cachedData.statsDatacenterId + } + + let statsContext = ChannelStatsContext(network: context.account.network, datacenterId: datacenterId, peer: peer) + let dataSignal: Signal = statsContext.state + |> map { state in + return state.stats + } |> afterNext({ [weak statsContext] a in + if let w = statsContext, let a = a { + if case .OnDemand = a.interactionsGraph { + w.loadInteractionsGraph() + } + } + }) + dataPromise.set(.single(nil) |> then(dataSignal)) + + let arguments = StatsArguments() + + let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get()) + |> deliverOnMainQueue + |> map { presentationData, data -> (ItemListControllerState, (ItemListNodeState, Any)) in + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChannelInfo_Stats), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: statsControllerEntries(data: data, presentationData: presentationData), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: false) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + let _ = statsContext.state + } + + let controller = ItemListController(context: context, state: signal) + controller.didDisappear = { [weak controller] _ in + controller?.clearItemNodesHighlight(animated: true) + } + pushControllerImpl = { [weak controller] c in + if let controller = controller { + (controller.navigationController as? NavigationController)?.pushViewController(c, animated: true) + } + } + presentControllerImpl = { [weak controller] c, a in + if let controller = controller { + controller.present(c, in: .window(.root), with: a) + } + } + return controller +} diff --git a/submodules/StatisticsUI/Sources/StatsGraphItem.swift b/submodules/StatisticsUI/Sources/StatsGraphItem.swift new file mode 100644 index 0000000000..a800baf8e9 --- /dev/null +++ b/submodules/StatisticsUI/Sources/StatsGraphItem.swift @@ -0,0 +1,205 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import SyncCore +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import Charts + +class StatsGraphItem: ListViewItem, ItemListItem { + let presentationData: ItemListPresentationData + let title: String + let graph: ChannelStatsGraph + let sectionId: ItemListSectionId + let style: ItemListStyle + + init(presentationData: ItemListPresentationData, title: String, graph: ChannelStatsGraph, sectionId: ItemListSectionId, style: ItemListStyle) { + self.presentationData = presentationData + self.title = title + self.graph = graph + self.sectionId = sectionId + self.style = style + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = StatsGraphItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? StatsGraphItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } + + var selectable: Bool = false +} + +class StatsGraphItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + + let chartNode: ChartNode + + private var item: StatsGraphItem? + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.chartNode = ChartNode() + + super.init(layerBacked: false, dynamicBounce: false) + + self.clipsToBounds = true + + self.addSubnode(self.chartNode) + } + + func asyncLayout() -> (_ item: StatsGraphItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + + return { item, params, neighbors in + let leftInset = params.leftInset + let rightInset: CGFloat = params.rightInset + var updatedTheme: PresentationTheme? + var updatedGraph: ChannelStatsGraph? + + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + if currentItem?.graph != item.graph { + updatedGraph = item.graph + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: 320.0) + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: 320.0) + insets = itemListNeighborsGroupedInsets(neighbors) + } + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + } + + if let updatedGraph = updatedGraph, case let .Loaded(data) = updatedGraph { + strongSelf.chartNode.setup(data) + } + + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + strongSelf.topStripeNode.isHidden = false + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + default: + bottomStripeInset = 0.0 + } + + strongSelf.chartNode.frame = CGRect(origin: CGPoint(x: leftInset, y: -30.0), size: CGSize(width: layout.size.width - leftInset - rightInset, height: 350.0)) + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) + } + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} + diff --git a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift new file mode 100644 index 0000000000..5e85186e30 --- /dev/null +++ b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift @@ -0,0 +1,292 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import SyncCore +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import Charts + +class StatsOverviewItem: ListViewItem, ItemListItem { + let presentationData: ItemListPresentationData + let stats: ChannelStats + let sectionId: ItemListSectionId + let style: ItemListStyle + + init(presentationData: ItemListPresentationData, stats: ChannelStats, sectionId: ItemListSectionId, style: ItemListStyle) { + self.presentationData = presentationData + self.stats = stats + self.sectionId = sectionId + self.style = style + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = StatsOverviewItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? StatsOverviewItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } + + var selectable: Bool = false +} + +class StatsOverviewItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + + private let followersValueLabel: ImmediateTextNode + private let viewsPerPostValueLabel: ImmediateTextNode + private let sharesPerPostValueLabel: ImmediateTextNode + private let enabledNotificationsValueLabel: ImmediateTextNode + + private let followersTitleLabel: ImmediateTextNode + private let viewsPerPostTitleLabel: ImmediateTextNode + private let sharesPerPostTitleLabel: ImmediateTextNode + private let enabledNotificationsTitleLabel: ImmediateTextNode + + private let followersDeltaLabel: ImmediateTextNode + private let viewsPerPostDeltaLabel: ImmediateTextNode + private let sharesPerPostDeltaLabel: ImmediateTextNode + + private var item: StatsOverviewItem? + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.followersValueLabel = ImmediateTextNode() + self.viewsPerPostValueLabel = ImmediateTextNode() + self.sharesPerPostValueLabel = ImmediateTextNode() + self.enabledNotificationsValueLabel = ImmediateTextNode() + + self.followersTitleLabel = ImmediateTextNode() + self.viewsPerPostTitleLabel = ImmediateTextNode() + self.sharesPerPostTitleLabel = ImmediateTextNode() + self.enabledNotificationsTitleLabel = ImmediateTextNode() + + self.followersDeltaLabel = ImmediateTextNode() + self.viewsPerPostDeltaLabel = ImmediateTextNode() + self.sharesPerPostDeltaLabel = ImmediateTextNode() + + super.init(layerBacked: false, dynamicBounce: false) + + self.clipsToBounds = true + + self.addSubnode(self.followersValueLabel) + self.addSubnode(self.viewsPerPostValueLabel) + self.addSubnode(self.sharesPerPostValueLabel) + self.addSubnode(self.enabledNotificationsValueLabel) + + self.addSubnode(self.followersTitleLabel) + self.addSubnode(self.viewsPerPostTitleLabel) + self.addSubnode(self.sharesPerPostTitleLabel) + self.addSubnode(self.enabledNotificationsTitleLabel) + + self.addSubnode(self.followersDeltaLabel) + self.addSubnode(self.viewsPerPostDeltaLabel) + self.addSubnode(self.sharesPerPostDeltaLabel) + } + + func asyncLayout() -> (_ item: StatsOverviewItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeFollowersValueLabelLayout = TextNode.asyncLayout(self.followersValueLabel) + let makeViewsPerPostValueLabelLayout = TextNode.asyncLayout(self.viewsPerPostValueLabel) + let makeSharesPerPostValueLabelLayout = TextNode.asyncLayout(self.sharesPerPostValueLabel) + let makeEnabledNotificationsValueLabelLayout = TextNode.asyncLayout(self.enabledNotificationsValueLabel) + + let makeFollowersTitleLabelLayout = TextNode.asyncLayout(self.followersTitleLabel) + let makeViewsPerPostTitleLabelLayout = TextNode.asyncLayout(self.viewsPerPostTitleLabel) + let makeSharesPerPostTitleLabelLayout = TextNode.asyncLayout(self.sharesPerPostTitleLabel) + let makeEnabledNotificationsTitleLabelLayout = TextNode.asyncLayout(self.enabledNotificationsTitleLabel) + + let makeFollowersDeltaLabelLayout = TextNode.asyncLayout(self.followersDeltaLabel) + let makeViewsPerPostDeltaLabelLayout = TextNode.asyncLayout(self.viewsPerPostDeltaLabel) + let makeSharesPerPostDeltaLabelLayout = TextNode.asyncLayout(self.sharesPerPostDeltaLabel) + + let currentItem = self.item + + return { item, params, neighbors in + let leftInset = params.leftInset + let rightInset: CGFloat = params.rightInset + var updatedTheme: PresentationTheme? + var updatedGraph: ChannelStatsGraph? + + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + let valueFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize) + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize) + let deltaFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize) + + let (followersValueLabelLayout, followersValueLabelApply) = makeFollowersValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "221K", font: valueFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (viewsPerPostValueLabelLayout, viewsPerPostValueLabelApply) = makeViewsPerPostValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "120K", font: valueFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (sharesPerPostValueLabelLayout, sharesPerPostValueLabelApply) = makeSharesPerPostValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "350", font: valueFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (enabledNotificationsValueLabelLayout, enabledNotificationsValueLabelApply) = makeEnabledNotificationsValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "22.77%", font: valueFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + + let (followersTitleLabelLayout, followersTitleLabelApply) = makeFollowersTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Followers", font: valueFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (viewsPerPostTitleLabelLayout, viewsPerPostTitleLabelApply) = makeViewsPerPostTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Views Per Post", font: valueFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (sharesPerPostTitleLabelLayout, sharesPerPostTitleLabelApply) = makeSharesPerPostTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Shares Per Post", font: valueFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (enabledNotificationsTitleLabelLayout, enabledNotificationsTitleLabelApply) = makeEnabledNotificationsTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Enabled Notifications", font: valueFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (followersDeltaLabelLayout, followersDeltaLabelApply) = makeFollowersDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "+474 (0.21%)", font: valueFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (viewsPerPostDeltaLabelLayout, viewsPerPostDeltaLabelApply) = makeViewsPerPostDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "-14K (10.68%)", font: valueFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (sharesPerPostDeltaLabelLayout, sharesPerPostDeltaLabelApply) = makeSharesPerPostDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "-134 (27.68%)", font: valueFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: 120.0) + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: 120.0) + insets = itemListNeighborsGroupedInsets(neighbors) + } + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + let _ = followersValueLabelApply() + let _ = viewsPerPostValueLabelApply() + let _ = sharesPerPostValueLabelApply() + let _ = enabledNotificationsValueLabelApply() + + let _ = followersTitleLabelApply() + let _ = viewsPerPostTitleLabelApply() + let _ = sharesPerPostTitleLabelApply() + let _ = enabledNotificationsTitleLabelApply() + + let _ = followersDeltaLabelApply() + let _ = viewsPerPostDeltaLabelApply() + let _ = sharesPerPostDeltaLabelApply() + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + } + + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + strongSelf.topStripeNode.isHidden = false + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + default: + bottomStripeInset = 0.0 + } + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) + } + + strongSelf.followersValueLabel.frame = CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: followersValueLabelLayout.size) + + strongSelf.viewsPerPostValueLabel.frame = CGRect(origin: CGPoint(x: leftInset, y: 44.0), size: viewsPerPostValueLabelLayout.size) + + strongSelf.sharesPerPostValueLabel.frame = CGRect(origin: CGPoint(x: layout.size.width / 2.0, y: 44.0), size: sharesPerPostValueLabelLayout.size) + + strongSelf.enabledNotificationsValueLabel.frame = CGRect(origin: CGPoint(x: layout.size.width / 2.0, y: 7.0), size: enabledNotificationsValueLabelLayout.size) + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} + diff --git a/submodules/StickerPackPreviewUI/BUCK b/submodules/StickerPackPreviewUI/BUCK index d9156bceac..4b0f121d0a 100644 --- a/submodules/StickerPackPreviewUI/BUCK +++ b/submodules/StickerPackPreviewUI/BUCK @@ -24,6 +24,7 @@ static_library( "//submodules/ActivityIndicator:ActivityIndicator", "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", + "//submodules/ArchivedStickerPacksNotice:ArchivedStickerPacksNotice", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift index f2c7181e6a..2572caf4d5 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift @@ -63,10 +63,13 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese } } - public init(context: AccountContext, stickerPack: StickerPackReference, mode: StickerPackPreviewControllerMode = .default, parentNavigationController: NavigationController?) { + private let actionPerformed: ((StickerPackCollectionInfo, [ItemCollectionItem], StickerPackScreenPerformedAction) -> Void)? + + public init(context: AccountContext, stickerPack: StickerPackReference, mode: StickerPackPreviewControllerMode = .default, parentNavigationController: NavigationController?, actionPerformed: ((StickerPackCollectionInfo, [ItemCollectionItem], StickerPackScreenPerformedAction) -> Void)? = nil) { self.context = context self.mode = mode self.parentNavigationController = parentNavigationController + self.actionPerformed = actionPerformed self.stickerPack = stickerPack @@ -133,7 +136,7 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: parentNavigationController, context: strongSelf.context, chatLocation: .peer(peer.id), animated: true)) } })) - }) + }, actionPerformed: self.actionPerformed) self.controllerNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) } @@ -163,8 +166,28 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese } switch next { - case let .result(_, items, _): + case let .result(info, items, _): var preloadSignals: [Signal] = [] + + if let thumbnail = info.thumbnail { + let signal = Signal { subscriber in + let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource)).start() + let data = account.postbox.mediaBox.resourceData(thumbnail.resource, option: .incremental(waitUntilFetchStatus: false)).start(next: { data in + if data.complete { + subscriber.putNext(true) + subscriber.putCompletion() + } else { + subscriber.putNext(false) + } + }) + return ActionDisposable { + fetched.dispose() + data.dispose() + } + } + preloadSignals.append(signal) + } + let topItems = items.prefix(16) for item in topItems { if let item = item as? StickerPackItem, item.file.isAnimatedSticker { @@ -243,3 +266,78 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } } + +public func preloadedStickerPackThumbnail(account: Account, info: StickerPackCollectionInfo, items: [ItemCollectionItem]) -> Signal { + if let thumbnail = info.thumbnail { + let signal = Signal { subscriber in + let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource)).start() + let dataDisposable: Disposable + if info.flags.contains(.isAnimated) { + dataDisposable = chatMessageAnimationData(postbox: account.postbox, resource: thumbnail.resource, width: 80, height: 80, synchronousLoad: false).start(next: { data in + if data.complete { + subscriber.putNext(true) + subscriber.putCompletion() + } else { + subscriber.putNext(false) + } + }) + } else { + dataDisposable = account.postbox.mediaBox.resourceData(thumbnail.resource, option: .incremental(waitUntilFetchStatus: false)).start(next: { data in + if data.complete { + subscriber.putNext(true) + subscriber.putCompletion() + } else { + subscriber.putNext(false) + } + }) + } + return ActionDisposable { + fetched.dispose() + dataDisposable.dispose() + } + } + return signal + } + + if let item = items.first as? StickerPackItem { + if item.file.isAnimatedSticker { + let signal = Signal { subscriber in + let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: FileMediaReference.standalone(media: item.file).resourceReference(item.file.resource)).start() + let data = account.postbox.mediaBox.resourceData(item.file.resource).start() + let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512) + let fetchedRepresentation = chatMessageAnimatedStickerDatas(postbox: account.postbox, file: item.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)), fetched: true, onlyFullSize: false, synchronousLoad: false).start(next: { next in + let hasContent = next._0 != nil || next._1 != nil + subscriber.putNext(hasContent) + if hasContent { + subscriber.putCompletion() + } + }) + return ActionDisposable { + fetched.dispose() + data.dispose() + fetchedRepresentation.dispose() + } + } + return signal + } else { + let signal = Signal { subscriber in + let data = account.postbox.mediaBox.resourceData(item.file.resource).start() + let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512) + let fetchedRepresentation = chatMessageAnimatedStickerDatas(postbox: account.postbox, file: item.file, small: true, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)), fetched: true, onlyFullSize: false, synchronousLoad: false).start(next: { next in + let hasContent = next._0 != nil || next._1 != nil + subscriber.putNext(hasContent) + if hasContent { + subscriber.putCompletion() + } + }) + return ActionDisposable { + data.dispose() + fetchedRepresentation.dispose() + } + } + return signal + } + } + + return .single(true) +} diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift index 25fd550957..2230f24959 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift @@ -74,6 +74,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol var dismiss: (() -> Void)? var cancel: (() -> Void)? var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + private let actionPerformed: ((StickerPackCollectionInfo, [ItemCollectionItem], StickerPackScreenPerformedAction) -> Void)? let ready = Promise() private var didSetReady = false @@ -87,10 +88,11 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol private var hapticFeedback: HapticFeedback? - init(context: AccountContext, openShare: (() -> Void)?, openMention: @escaping (String) -> Void) { + init(context: AccountContext, openShare: (() -> Void)?, openMention: @escaping (String) -> Void, actionPerformed: ((StickerPackCollectionInfo, [ItemCollectionItem], StickerPackScreenPerformedAction) -> Void)?) { self.context = context self.openShare = openShare self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.actionPerformed = actionPerformed self.wrappingScrollNode = ASScrollNode() self.wrappingScrollNode.view.alwaysBounceVertical = true @@ -223,7 +225,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol } return true })) - menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { _, _ in return true })) + menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true })) } return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) } else { @@ -509,23 +511,27 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol } @objc func installActionButtonPressed() { - let dismissOnAction: Bool - if let initiallyInstalled = self.stickerPackInitiallyInstalled, initiallyInstalled { - dismissOnAction = false - } else { - dismissOnAction = true - } + let dismissOnAction = true if let stickerPack = self.stickerPack, let stickerSettings = self.stickerSettings { switch stickerPack { case let .result(info, items, installed): if installed { - let _ = removeStickerPackInteractively(postbox: self.context.account.postbox, id: info.id, option: .delete).start() - self.updateStickerPack(.result(info: info, items: items, installed: false), stickerSettings: stickerSettings) + let _ = (removeStickerPackInteractively(postbox: self.context.account.postbox, id: info.id, option: .delete) + |> deliverOnMainQueue).start(next: { [weak self] indexAndItems in + guard let strongSelf = self, let (positionInList, _) = indexAndItems else { + return + } + strongSelf.actionPerformed?(info, items, .remove(positionInList: positionInList)) + }) + if !dismissOnAction { + self.updateStickerPack(.result(info: info, items: items, installed: false), stickerSettings: stickerSettings) + } } else { let _ = addStickerPackInteractively(postbox: self.context.account.postbox, info: info, items: items).start() if !dismissOnAction { self.updateStickerPack(.result(info: info, items: items, installed: true), stickerSettings: stickerSettings) } + self.actionPerformed?(info, items, .add) } if dismissOnAction { self.cancelButtonPressed() diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewUI.h b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewUI.h deleted file mode 100644 index f1eca381ba..0000000000 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewUI.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// StickerPackPreviewUI.h -// StickerPackPreviewUI -// -// Created by Peter on 8/15/19. -// Copyright © 2019 Telegram Messenger LLP. All rights reserved. -// - -#import - -//! Project version number for StickerPackPreviewUI. -FOUNDATION_EXPORT double StickerPackPreviewUIVersionNumber; - -//! Project version string for StickerPackPreviewUI. -FOUNDATION_EXPORT const unsigned char StickerPackPreviewUIVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift new file mode 100644 index 0000000000..38e9a5ce70 --- /dev/null +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -0,0 +1,854 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SyncCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import TelegramUIPreferences +import MergeLists + +private struct StickerPackPreviewGridEntry: Comparable, Identifiable { + let index: Int + let stickerItem: StickerPackItem + + var stableId: MediaId { + return self.stickerItem.file.fileId + } + + static func <(lhs: StickerPackPreviewGridEntry, rhs: StickerPackPreviewGridEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(account: Account, interaction: StickerPackPreviewInteraction) -> StickerPackPreviewGridItem { + return StickerPackPreviewGridItem(account: account, stickerItem: self.stickerItem, interaction: interaction) + } +} + +private struct StickerPackPreviewGridTransaction { + let deletions: [Int] + let insertions: [GridNodeInsertItem] + let updates: [GridNodeUpdateItem] + + init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], account: Account, interaction: StickerPackPreviewInteraction) { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list) + + self.deletions = deleteIndices + self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction), previousIndex: $0.2) } + self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction)) } + } +} + +private enum StickerPackAction { + case add + case remove +} + +private enum StickerPackNextAction { + case navigatedNext + case dismiss +} + +private final class StickerPackContainer: ASDisplayNode { + let index: Int + private let context: AccountContext + private var presentationData: PresentationData + private let stickerPack: StickerPackReference + private let decideNextAction: (StickerPackContainer, StickerPackAction) -> StickerPackNextAction + private let requestDismiss: () -> Void + private let presentInGlobalOverlay: (ViewController, Any?) -> Void + private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + private let backgroundNode: ASImageNode + private let gridNode: GridNode + private let actionAreaBackgroundNode: ASDisplayNode + private let actionAreaSeparatorNode: ASDisplayNode + private let buttonNode: HighlightableButtonNode + private let titleNode: ImmediateTextNode + private let titleContainer: ASDisplayNode + private let titleSeparatorNode: ASDisplayNode + + private(set) var validLayout: (ContainerViewLayout, CGRect, CGFloat, UIEdgeInsets)? + + private var currentEntries: [StickerPackPreviewGridEntry] = [] + private var enqueuedTransactions: [StickerPackPreviewGridTransaction] = [] + + private var itemsDisposable: Disposable? + private(set) var currentStickerPack: (StickerPackCollectionInfo, [ItemCollectionItem], Bool)? + + private let isReadyValue = Promise() + private var didSetReady = false + var isReady: Signal { + return self.isReadyValue.get() + } + + var expandProgress: CGFloat = 0.0 + var modalProgress: CGFloat = 0.0 + let expandProgressUpdated: (StickerPackContainer, ContainedViewLayoutTransition) -> Void + + private var isDismissed: Bool = false + + private let interaction: StickerPackPreviewInteraction + + init(index: Int, context: AccountContext, presentationData: PresentationData, stickerPack: StickerPackReference, decideNextAction: @escaping (StickerPackContainer, StickerPackAction) -> StickerPackNextAction, requestDismiss: @escaping () -> Void, expandProgressUpdated: @escaping (StickerPackContainer, ContainedViewLayoutTransition) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?) { + self.index = index + self.context = context + self.presentationData = presentationData + self.stickerPack = stickerPack + self.decideNextAction = decideNextAction + self.requestDismiss = requestDismiss + self.presentInGlobalOverlay = presentInGlobalOverlay + self.expandProgressUpdated = expandProgressUpdated + self.sendSticker = sendSticker + + self.backgroundNode = ASImageNode() + self.backgroundNode.displaysAsynchronously = true + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) + + self.gridNode = GridNode() + self.gridNode.scrollView.alwaysBounceVertical = true + self.gridNode.scrollView.showsVerticalScrollIndicator = false + + self.actionAreaBackgroundNode = ASDisplayNode() + self.actionAreaBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor + + self.actionAreaSeparatorNode = ASDisplayNode() + self.actionAreaSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor + + self.buttonNode = HighlightableButtonNode() + self.titleNode = ImmediateTextNode() + self.titleContainer = ASDisplayNode() + self.titleSeparatorNode = ASDisplayNode() + self.titleSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor + + self.interaction = StickerPackPreviewInteraction(playAnimatedStickers: true) + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.gridNode) + self.addSubnode(self.actionAreaBackgroundNode) + self.addSubnode(self.actionAreaSeparatorNode) + self.addSubnode(self.buttonNode) + + self.titleContainer.addSubnode(self.titleNode) + self.addSubnode(self.titleContainer) + self.addSubnode(self.titleSeparatorNode) + + self.gridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in + self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) + } + + self.gridNode.interactiveScrollingEnded = { [weak self] in + guard let strongSelf = self, !strongSelf.isDismissed else { + return + } + let contentOffset = strongSelf.gridNode.scrollView.contentOffset + let insets = strongSelf.gridNode.scrollView.contentInset + + if contentOffset.y <= -insets.top - 30.0 { + strongSelf.isDismissed = true + DispatchQueue.main.async { + self?.requestDismiss() + } + } + } + + self.gridNode.interactiveScrollingWillBeEnded = { [weak self] velocity, targetOffset in + guard let strongSelf = self, !strongSelf.isDismissed else { + return + } + DispatchQueue.main.async { + let contentOffset = targetOffset + let insets = strongSelf.gridNode.scrollView.contentInset + var modalProgress: CGFloat = 0.0 + + if contentOffset.y < 0.0 && contentOffset.y >= -insets.top { + strongSelf.gridNode.scrollView.stopScrollingAnimation() + if contentOffset.y > -insets.top / 2.0 || velocity.y <= -100.0 { + strongSelf.gridNode.scrollView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true) + modalProgress = 1.0 + } else { + strongSelf.gridNode.scrollView.setContentOffset(CGPoint(x: 0.0, y: -insets.top), animated: true) + } + } else if contentOffset.y >= 0.0 { + modalProgress = 1.0 + } + + if abs(strongSelf.modalProgress - modalProgress) > CGFloat.ulpOfOne { + strongSelf.modalProgress = modalProgress + strongSelf.expandProgressUpdated(strongSelf, .animated(duration: 0.4, curve: .spring)) + } + } + } + + self.itemsDisposable = (loadedStickerPack(postbox: context.account.postbox, network: context.account.network, reference: stickerPack, forceActualized: false) + |> deliverOnMainQueue).start(next: { [weak self] contents in + guard let strongSelf = self else { + return + } + strongSelf.updateStickerPackContents(contents) + }) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity") + strongSelf.buttonNode.alpha = 0.8 + } else { + strongSelf.buttonNode.alpha = 1.0 + strongSelf.buttonNode.layer.animateAlpha(from: 0.8, to: 1.0, duration: 0.3) + } + } + } + } + + deinit { + self.itemsDisposable?.dispose() + } + + override func didLoad() { + super.didLoad() + + self.gridNode.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>? in + if let strongSelf = self { + if let itemNode = strongSelf.gridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, let item = itemNode.stickerPackItem { + return strongSelf.context.account.postbox.transaction { transaction -> Bool in + return getIsStickerSaved(transaction: transaction, fileId: item.file.fileId) + } + |> deliverOnMainQueue + |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in + if let strongSelf = self { + var menuItems: [PeekControllerMenuItem] = [] + if let (info, _, _) = strongSelf.currentStickerPack, info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { + if strongSelf.sendSticker != nil { + menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, font: .bold, action: { node, rect in + if let strongSelf = self { + return strongSelf.sendSticker?(.standalone(media: item.file), node, rect) ?? false + } else { + return false + } + })) + } + menuItems.append(PeekControllerMenuItem(title: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in + if let strongSelf = self { + if isStarred { + let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() + } else { + let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() + } + } + return true + })) + menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true })) + } + return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) + } else { + return nil + } + } + } + } + return nil + }, present: { [weak self] content, sourceNode in + if let strongSelf = self { + let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.presentationData.theme), content: content, sourceNode: { + return sourceNode + }) + strongSelf.presentInGlobalOverlay(controller, nil) + return controller + } + return nil + }, updateContent: { [weak self] content in + if let strongSelf = self { + var item: StickerPreviewPeekItem? + if let content = content as? StickerPreviewPeekContent { + item = content.item + } + strongSelf.updatePreviewingItem(item: item, animated: true) + } + }, activateBySingleTap: true)) + } + + @objc func buttonPressed() { + guard let (info, items, installed) = currentStickerPack else { + return + } + + let _ = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings]) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] sharedData in + guard let strongSelf = self else { + return + } + var stickerSettings = StickerSettings.defaultSettings + if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings] as? StickerSettings { + stickerSettings = value + } + + if installed { + let _ = removeStickerPackInteractively(postbox: strongSelf.context.account.postbox, id: info.id, option: .delete).start() + } else { + let _ = addStickerPackInteractively(postbox: strongSelf.context.account.postbox, info: info, items: items).start() + } + + switch strongSelf.decideNextAction(strongSelf, installed ? .remove : .add) { + case .dismiss: + strongSelf.requestDismiss() + case .navigatedNext: + strongSelf.updateStickerPackContents(.result(info: info, items: items, installed: !installed)) + } + }) + } + + private func updateStickerPackContents(_ contents: LoadedStickerPack) { + var entries: [StickerPackPreviewGridEntry] = [] + + var updateLayout = false + + switch contents { + case .fetching: + entries = [] + case .none: + entries = [] + case let .result(info, items, installed): + self.currentStickerPack = (info, items, installed) + + if installed { + let text: String + if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { + text = self.presentationData.strings.StickerPack_RemoveStickerCount(info.count) + } else { + text = self.presentationData.strings.StickerPack_RemoveMaskCount(info.count) + } + self.buttonNode.setTitle(text.uppercased(), with: Font.semibold(17.0), with: self.presentationData.theme.list.itemDestructiveColor, for: .normal) + self.buttonNode.setBackgroundImage(nil, for: []) + } else { + let text: String + if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { + text = self.presentationData.strings.StickerPack_AddStickerCount(info.count) + } else { + text = self.presentationData.strings.StickerPack_AddMaskCount(info.count) + } + self.buttonNode.setTitle(text.uppercased(), with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal) + let roundedAccentBackground = generateImage(CGSize(width: 50.0, height: 50.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(self.presentationData.theme.list.itemCheckColors.fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + })?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25) + self.buttonNode.setBackgroundImage(roundedAccentBackground, for: []) + } + + self.titleNode.attributedText = NSAttributedString(string: info.title, font: Font.semibold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) + updateLayout = true + + for item in items { + guard let item = item as? StickerPackItem else { + continue + } + entries.append(StickerPackPreviewGridEntry(index: entries.count, stickerItem: item)) + } + } + let previousEntries = self.currentEntries + self.currentEntries = entries + + if updateLayout, let (layout, _, _, _) = self.validLayout { + let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - 12.0 * 2.0, height: .greatestFiniteMagnitude)) + self.titleNode.frame = CGRect(origin: CGPoint(x: floor((-titleSize.width) / 2.0), y: floor((-titleSize.height) / 2.0)), size: titleSize) + + self.updateLayout(layout: layout, transition: .immediate) + } + + let transaction = StickerPackPreviewGridTransaction(previousList: previousEntries, list: entries, account: self.context.account, interaction: self.interaction) + self.enqueueTransaction(transaction) + } + + var topContentInset: CGFloat { + guard let (_, gridFrame, titleAreaInset, gridInsets) = self.validLayout else { + return 0.0 + } + return min(self.backgroundNode.frame.minY, gridFrame.minY + gridInsets.top - titleAreaInset) + } + + func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + var insets = layout.insets(options: [.statusBar]) + insets.top += 10.0 + + let buttonHeight: CGFloat = 50.0 + let actionAreaTopInset: CGFloat = 12.0 + let buttonSideInset: CGFloat = 10.0 + let titleAreaInset: CGFloat = 50.0 + + var actionAreaHeight: CGFloat = 0.0 + actionAreaHeight += insets.bottom + 12.0 + + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: layout.size.height - actionAreaHeight - buttonHeight), size: CGSize(width: layout.size.width - buttonSideInset * 2.0, height: buttonHeight))) + actionAreaHeight += buttonHeight + + actionAreaHeight += actionAreaTopInset + + transition.updateFrame(node: self.actionAreaBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: actionAreaHeight))) + transition.updateFrame(node: self.actionAreaSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + + let gridFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top + titleAreaInset), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - titleAreaInset)) + + let itemsPerRow = 4 + let fillingWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) + let itemWidth = floor(fillingWidth / CGFloat(itemsPerRow)) + let gridLeftInset = floor((layout.size.width - fillingWidth) / 2.0) + let contentHeight: CGFloat + if let (_, items, _) = self.currentStickerPack { + let rowCount = items.count / itemsPerRow + ((items.count % itemsPerRow) == 0 ? 0 : 1) + contentHeight = itemWidth * CGFloat(rowCount) + } else { + contentHeight = gridFrame.size.height + } + + let initialRevealedRowCount: CGFloat = 4.5 + + let topInset = max(0.0, layout.size.height - floor(initialRevealedRowCount * itemWidth) - insets.top - actionAreaHeight - titleAreaInset) + + let additionalGridBottomInset = max(0.0, gridFrame.size.height - actionAreaHeight - contentHeight) + + let gridInsets = UIEdgeInsets(top: insets.top + topInset, left: gridLeftInset, bottom: actionAreaHeight + additionalGridBottomInset, right: layout.size.width - fillingWidth - gridLeftInset) + + let firstTime = self.validLayout == nil + self.validLayout = (layout, gridFrame, titleAreaInset, gridInsets) + + transition.updateFrame(node: self.gridNode, frame: gridFrame) + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridFrame.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: 200.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf.isReadyValue.set(.single(true)) + } + }) + + if firstTime { + while !self.enqueuedTransactions.isEmpty { + self.dequeueTransaction() + } + } + } + + private func gridPresentationLayoutUpdated(_ presentationLayout: GridNodeCurrentPresentationLayout, transition: ContainedViewLayoutTransition) { + guard let (layout, gridFrame, titleAreaInset, gridInsets) = self.validLayout else { + return + } + + let minBackgroundY = gridFrame.minY - titleAreaInset + let unclippedBackgroundY = gridFrame.minY - presentationLayout.contentOffset.y - titleAreaInset + + let offsetFromInitialPosition = presentationLayout.contentOffset.y + gridInsets.top + let expandHeight: CGFloat = 100.0 + let expandProgress = max(0.0, min(1.0, offsetFromInitialPosition / expandHeight)) + + var expandProgressTransition = transition + var expandUpdated = false + + if abs(self.expandProgress - expandProgress) > CGFloat.ulpOfOne { + self.expandProgress = expandProgress + expandUpdated = true + } + + if expandUpdated { + self.expandProgressUpdated(self, expandProgressTransition) + } + + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: max(minBackgroundY, unclippedBackgroundY)), size: CGSize(width: layout.size.width, height: layout.size.height)) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + transition.updateFrame(node: self.titleContainer, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: backgroundFrame.minY + floor((50.0) / 2.0)), size: CGSize())) + transition.updateFrame(node: self.titleSeparatorNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY + 50.0 - UIScreenPixel), size: CGSize(width: backgroundFrame.width, height: UIScreenPixel))) + self.titleSeparatorNode.alpha = unclippedBackgroundY < minBackgroundY ? 1.0 : 0.0 + } + + private func enqueueTransaction(_ transaction: StickerPackPreviewGridTransaction) { + self.enqueuedTransactions.append(transaction) + + if let _ = self.validLayout { + self.dequeueTransaction() + } + } + + private func dequeueTransaction() { + if self.enqueuedTransactions.isEmpty { + return + } + let transaction = self.enqueuedTransactions.removeFirst() + + self.gridNode.transaction(GridNodeTransaction(deleteItems: transaction.deletions, insertItems: transaction.insertions, updateItems: transaction.updates, scrollToItem: nil, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.bounds.contains(point) { + if !self.backgroundNode.bounds.contains(self.convert(point, to: self.backgroundNode)) { + return nil + } + } + + let result = super.hitTest(point, with: event) + return result + } + + private func updatePreviewingItem(item: StickerPreviewPeekItem?, animated: Bool) { + if self.interaction.previewedItem != item { + self.interaction.previewedItem = item + + self.gridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? StickerPackPreviewGridItemNode { + itemNode.updatePreviewing(animated: animated) + } + } + } + } +} + +private final class StickerPackScreenNode: ViewControllerTracingNode { + private let context: AccountContext + private var presentationData: PresentationData + private let stickerPacks: [StickerPackReference] + private let modalProgressUpdated: (CGFloat, ContainedViewLayoutTransition) -> Void + private let dismissed: () -> Void + private let presentInGlobalOverlay: (ViewController, Any?) -> Void + private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + + private let dimNode: ASDisplayNode + private let containerContainingNode: ASDisplayNode + + private var containers: [Int: StickerPackContainer] = [:] + private var selectedStickerPackIndex: Int + private var relativeToSelectedStickerPackTransition: CGFloat = 0.0 + + private var validLayout: ContainerViewLayout? + private var isDismissed: Bool = false + + private let _ready = Promise() + var ready: Promise { + return self._ready + } + + init(context: AccountContext, stickerPacks: [StickerPackReference], initialSelectedStickerPackIndex: Int, modalProgressUpdated: @escaping (CGFloat, ContainedViewLayoutTransition) -> Void, dismissed: @escaping () -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?) { + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.stickerPacks = stickerPacks + self.selectedStickerPackIndex = initialSelectedStickerPackIndex + self.modalProgressUpdated = modalProgressUpdated + self.dismissed = dismissed + self.presentInGlobalOverlay = presentInGlobalOverlay + self.sendSticker = sendSticker + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.dimNode.alpha = 0.0 + + self.containerContainingNode = ASDisplayNode() + + super.init() + + self.addSubnode(self.dimNode) + + self.addSubnode(self.containerContainingNode) + } + + override func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTapGesture(_:)))) + self.containerContainingNode.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + let firstTime = self.validLayout == nil + + self.validLayout = layout + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.containerContainingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + let expandProgress: CGFloat + if self.stickerPacks.count == 1 { + expandProgress = 1.0 + } else { + expandProgress = self.containers[self.selectedStickerPackIndex]?.expandProgress ?? 0.0 + } + let scaledInset: CGFloat = 12.0 + let scaledDistance: CGFloat = 4.0 + let minScale = (layout.size.width - scaledInset * 2.0) / layout.size.width + let containerScale = expandProgress * 1.0 + (1.0 - expandProgress) * minScale + + let containerVerticalOffset: CGFloat = (1.0 - expandProgress) * scaledInset * 2.0 + + for i in 0 ..< self.stickerPacks.count { + let indexOffset = i - self.selectedStickerPackIndex + var scaledOffset: CGFloat = 0.0 + scaledOffset = -CGFloat(indexOffset) * (1.0 - expandProgress) * (scaledInset * 2.0) + CGFloat(indexOffset) * scaledDistance + + if abs(indexOffset) <= 1 { + let containerTransition: ContainedViewLayoutTransition + let container: StickerPackContainer + if let current = self.containers[i] { + containerTransition = transition + container = current + } else { + containerTransition = .immediate + let index = i + container = StickerPackContainer(index: index, context: context, presentationData: self.presentationData, stickerPack: self.stickerPacks[i], decideNextAction: { [weak self] container, action in + guard let strongSelf = self, let layout = strongSelf.validLayout else { + return .dismiss + } + if index == strongSelf.stickerPacks.count - 1 { + return .dismiss + } else { + switch action { + case .add: + var allAdded = true + for i in index + 1 ..< strongSelf.stickerPacks.count { + if let container = strongSelf.containers[index], let (_, _, installed) = container.currentStickerPack { + if !installed { + allAdded = false + } + } else { + allAdded = false + } + } + if allAdded { + return .dismiss + } + case .remove: + if strongSelf.stickerPacks.count == 1 { + return .dismiss + } + } + } + + strongSelf.selectedStickerPackIndex = strongSelf.selectedStickerPackIndex + 1 + strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.3, curve: .spring)) + return .navigatedNext + }, requestDismiss: { [weak self] in + self?.dismiss() + }, expandProgressUpdated: { [weak self] container, transition in + guard let strongSelf = self, let layout = strongSelf.validLayout else { + return + } + if index == strongSelf.selectedStickerPackIndex, let container = strongSelf.containers[strongSelf.selectedStickerPackIndex] { + let modalProgress = container.modalProgress + strongSelf.modalProgressUpdated(modalProgress, transition) + strongSelf.containerLayoutUpdated(layout, transition: .immediate) + } + }, presentInGlobalOverlay: presentInGlobalOverlay, + sendSticker: sendSticker) + self.containerContainingNode.addSubnode(container) + self.containers[i] = container + } + + containerTransition.updateFrame(node: container, frame: CGRect(origin: CGPoint(x: CGFloat(indexOffset) * layout.size.width + self.relativeToSelectedStickerPackTransition + scaledOffset, y: containerVerticalOffset), size: layout.size), beginWithCurrentState: true) + containerTransition.updateSublayerTransformScaleAndOffset(node: container, scale: containerScale, offset: CGPoint(), beginWithCurrentState: true) + if container.validLayout?.0 != layout { + container.updateLayout(layout: layout, transition: containerTransition) + } + } else { + if let container = self.containers[i] { + container.removeFromSupernode() + self.containers.removeValue(forKey: i) + } + } + } + + if firstTime { + if !self.containers.isEmpty { + self._ready.set(combineLatest(self.containers.map { (_, container) in container.isReady }) + |> map { values -> Bool in + for value in values { + if !value { + return false + } + } + return true + }) + } else { + self._ready.set(.single(true)) + } + } + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + break + case .changed: + let translation = recognizer.translation(in: self.view) + self.relativeToSelectedStickerPackTransition = translation.x + if self.selectedStickerPackIndex == 0 { + self.relativeToSelectedStickerPackTransition = min(0.0, self.relativeToSelectedStickerPackTransition) + } + if self.selectedStickerPackIndex == self.stickerPacks.count - 1 { + self.relativeToSelectedStickerPackTransition = max(0.0, self.relativeToSelectedStickerPackTransition) + } + if let layout = self.validLayout { + self.containerLayoutUpdated(layout, transition: .immediate) + } + case .ended, .cancelled: + let translation = recognizer.translation(in: self.view) + let velocity = recognizer.velocity(in: self.view) + if abs(translation.x) > 30.0 { + let deltaIndex = translation.x > 0 ? -1 : 1 + self.selectedStickerPackIndex = max(0, min(self.stickerPacks.count - 1, Int(self.selectedStickerPackIndex + deltaIndex))) + } else if abs(velocity.x) > 100.0 { + let deltaIndex = velocity.x > 0 ? -1 : 1 + self.selectedStickerPackIndex = max(0, min(self.stickerPacks.count - 1, Int(self.selectedStickerPackIndex + deltaIndex))) + } + self.relativeToSelectedStickerPackTransition = 0.0 + if let layout = self.validLayout { + self.containerLayoutUpdated(layout, transition: .animated(duration: 0.35, curve: .spring)) + } + default: + break + } + } + + func animateIn() { + self.dimNode.alpha = 1.0 + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + + let minInset: CGFloat = (self.containers.map { (_, container) -> CGFloat in container.topContentInset }).max() ?? 0.0 + self.containerContainingNode.layer.animatePosition(from: CGPoint(x: 0.0, y: self.containerContainingNode.bounds.height - minInset), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + + func animateOut(completion: @escaping () -> Void) { + self.dimNode.alpha = 0.0 + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + + let minInset: CGFloat = (self.containers.map { (_, container) -> CGFloat in container.topContentInset }).max() ?? 0.0 + self.containerContainingNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.containerContainingNode.bounds.height - minInset), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + + self.modalProgressUpdated(0.0, .animated(duration: 0.2, curve: .easeInOut)) + } + + func dismiss() { + if self.isDismissed { + return + } + self.isDismissed = true + self.animateOut(completion: { [weak self] in + self?.dismissed() + }) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + + if let selectedContainer = self.containers[self.selectedStickerPackIndex] { + if selectedContainer.hitTest(self.view.convert(point, to: selectedContainer.view), with: event) == nil { + return self.dimNode.view + } + } + + let result = super.hitTest(point, with: event) + return result + } + + @objc private func dimNodeTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.dismiss() + } + } +} + +public final class StickerPackScreenImpl: ViewController { + private let context: AccountContext + private let stickerPacks: [StickerPackReference] + private let initialSelectedStickerPackIndex: Int + private weak var parentNavigationController: NavigationController? + private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + + private var controllerNode: StickerPackScreenNode { + return self.displayNode as! StickerPackScreenNode + } + + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + + private var alreadyDidAppear: Bool = false + + public init(context: AccountContext, stickerPacks: [StickerPackReference], selectedStickerPackIndex: Int = 0, parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil) { + self.context = context + self.stickerPacks = stickerPacks + self.initialSelectedStickerPackIndex = selectedStickerPackIndex + self.parentNavigationController = parentNavigationController + self.sendSticker = sendSticker + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = StickerPackScreenNode(context: self.context, stickerPacks: self.stickerPacks, initialSelectedStickerPackIndex: self.initialSelectedStickerPackIndex, modalProgressUpdated: { [weak self] value, transition in + DispatchQueue.main.async { + guard let strongSelf = self else { + return + } + strongSelf.updateModalStyleOverlayTransitionFactor(value, transition: transition) + } + }, dismissed: { [weak self] in + self?.dismiss() + }, presentInGlobalOverlay: { [weak self] c, a in + self?.presentInGlobalOverlay(c, with: a) + }, sendSticker: self.sendSticker.flatMap { [weak self] sendSticker in + return { file, sourceNode, sourceRect in + if sendSticker(file, sourceNode, sourceRect) { + self?.dismiss() + return true + } else { + return false + } + } + }) + + self._ready.set(self.controllerNode.ready.get()) + + super.displayNodeDidLoad() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.alreadyDidAppear { + self.alreadyDidAppear = true + self.controllerNode.animateIn() + } + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, transition: transition) + } +} + +public enum StickerPackScreenPerformedAction { + case add + case remove(positionInList: Int) +} + +public func StickerPackScreen(context: AccountContext, mode: StickerPackPreviewControllerMode = .default, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil, actionPerformed: ((StickerPackCollectionInfo, [ItemCollectionItem], StickerPackScreenPerformedAction) -> Void)? = nil) -> ViewController { + let controller = StickerPackPreviewController(context: context, stickerPack: mainStickerPack, mode: mode, parentNavigationController: parentNavigationController, actionPerformed: actionPerformed) + controller.sendSticker = sendSticker + return controller +} diff --git a/submodules/Svg/BUCK b/submodules/Svg/BUCK new file mode 100644 index 0000000000..f8b2a7b112 --- /dev/null +++ b/submodules/Svg/BUCK @@ -0,0 +1,17 @@ +load("//Config:buck_rule_macros.bzl", "static_library") + +static_library( + name = "Svg", + srcs = glob([ + "Sources/**/*.m", + "Sources/**/*.mm", + "Sources/**/*.c", + "Sources/**/*.cpp", + ]), + headers = glob([ + "Sources/**/*.h", + ]), + exported_headers = [ + "Sources/Svg.h", + ], +) diff --git a/submodules/Svg/Sources/Svg.h b/submodules/Svg/Sources/Svg.h new file mode 100755 index 0000000000..440279afcf --- /dev/null +++ b/submodules/Svg/Sources/Svg.h @@ -0,0 +1,9 @@ +#ifndef Lottie_h +#define Lottie_h + +#import +#import + +UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, UIColor *foregroundColor); + +#endif /* Lottie_h */ diff --git a/submodules/Svg/Sources/Svg.m b/submodules/Svg/Sources/Svg.m new file mode 100755 index 0000000000..def1d2b873 --- /dev/null +++ b/submodules/Svg/Sources/Svg.m @@ -0,0 +1,233 @@ +#import "Svg.h" + +#import "nanosvg.h" + +#define UIColorRGBA(rgb,a) ([[UIColor alloc] initWithRed:(((rgb >> 16) & 0xff) / 255.0f) green:(((rgb >> 8) & 0xff) / 255.0f) blue:(((rgb) & 0xff) / 255.0f) alpha:a]) + +CGSize aspectFillSize(CGSize size, CGSize bounds) { + CGFloat scale = MAX(bounds.width / MAX(1.0, size.width), bounds.height / MAX(1.0, size.height)); + return CGSizeMake(floor(size.width * scale), floor(size.height * scale)); +} + +@interface SvgXMLParsingDelegate : NSObject { + NSString *_elementName; + NSString *_currentStyleString; +} + +@property (nonatomic, strong, readonly) NSMutableDictionary *styles; + +@end + +@implementation SvgXMLParsingDelegate + +- (instancetype)init { + self = [super init]; + if (self != nil) { + _styles = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { + _elementName = elementName; +} + +- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { + if ([_elementName isEqualToString:@"style"]) { + int currentClassNameStartIndex = -1; + int currentClassContentsStartIndex = -1; + + NSString *currentClassName = nil; + + NSCharacterSet *alphanumeric = [NSCharacterSet alphanumericCharacterSet]; + + for (int i = 0; i < _currentStyleString.length; i++) { + unichar c = [_currentStyleString characterAtIndex:i]; + if (currentClassNameStartIndex != -1) { + if (![alphanumeric characterIsMember:c]) { + currentClassName = [_currentStyleString substringWithRange:NSMakeRange(currentClassNameStartIndex, i - currentClassNameStartIndex)]; + currentClassNameStartIndex = -1; + } + } else if (currentClassContentsStartIndex != -1) { + if (c == '}') { + NSString *classContents = [_currentStyleString substringWithRange:NSMakeRange(currentClassContentsStartIndex, i - currentClassContentsStartIndex)]; + if (currentClassName != nil && classContents != nil) { + _styles[currentClassName] = classContents; + currentClassName = nil; + } + currentClassContentsStartIndex = -1; + } + } + + if (currentClassNameStartIndex == -1 && currentClassContentsStartIndex == -1) { + if (c == '.') { + currentClassNameStartIndex = i + 1; + } else if (c == '{') { + currentClassContentsStartIndex = i + 1; + } + } + } + } + _elementName = nil; +} + +- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string { + if ([_elementName isEqualToString:@"style"]) { + if (_currentStyleString == nil) { + _currentStyleString = string; + } else { + _currentStyleString = [_currentStyleString stringByAppendingString:string]; + } + } +} + +@end + +UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, UIColor *foregroundColor) { + NSDate *startTime = [NSDate date]; + + NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]; + if (parser == nil) { + return nil; + } + SvgXMLParsingDelegate *delegate = [[SvgXMLParsingDelegate alloc] init]; + parser.delegate = delegate; + [parser parse]; + + NSMutableString *xmlString = [[NSMutableString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + if (xmlString == nil) { + return nil; + } + + for (NSString *styleName in delegate.styles) { + NSString *styleValue = delegate.styles[styleName]; + [xmlString replaceOccurrencesOfString:[NSString stringWithFormat:@"class=\"%@\"", styleName] withString:[NSString stringWithFormat:@"style=\"%@\"", styleValue] options:0 range:NSMakeRange(0, xmlString.length)]; + } + + char *zeroTerminatedData = xmlString.UTF8String; + + NSVGimage *image = nsvgParse(zeroTerminatedData, "px", 96); + if (image == nil || image->width < 1.0f || image->height < 1.0f) { + return nil; + } + + double deltaTime = -1.0f * [startTime timeIntervalSinceNow]; + printf("parseTime = %f\n", deltaTime); + + startTime = [NSDate date]; + + UIGraphicsBeginImageContextWithOptions(size, true, 1.0); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetFillColorWithColor(context, backgroundColor.CGColor); + CGContextFillRect(context, CGRectMake(0.0f, 0.0f, size.width, size.height)); + + CGSize svgSize = CGSizeMake(image->width, image->height); + CGSize drawingSize = aspectFillSize(svgSize, size); + + CGFloat scale = MAX(size.width / MAX(1.0, svgSize.width), size.height / MAX(1.0, svgSize.height)); + + CGContextScaleCTM(context, scale, scale); + CGContextTranslateCTM(context, (size.width - drawingSize.width) / 2.0, (size.height - drawingSize.height) / 2.0); + + for (NSVGshape *shape = image->shapes; shape != NULL; shape = shape->next) { + if (!(shape->flags & NSVG_FLAGS_VISIBLE)) { + continue; + } + + if (shape->fill.type != NSVG_PAINT_NONE) { + //CGContextSetFillColorWithColor(context, UIColorRGBA(shape->fill.color, shape->opacity).CGColor); + CGContextSetFillColorWithColor(context, [foregroundColor colorWithAlphaComponent:shape->opacity].CGColor); + + bool isFirst = true; + bool hasStartPoint = false; + CGPoint startPoint; + for (NSVGpath *path = shape->paths; path != NULL; path = path->next) { + if (isFirst) { + CGContextBeginPath(context); + isFirst = false; + hasStartPoint = true; + startPoint.x = path->pts[0]; + startPoint.y = path->pts[1]; + } + CGContextMoveToPoint(context, path->pts[0], path->pts[1]); + for (int i = 0; i < path->npts - 1; i += 3) { + float *p = &path->pts[i * 2]; + CGContextAddCurveToPoint(context, p[2], p[3], p[4], p[5], p[6], p[7]); + } + + if (path->closed) { + if (hasStartPoint) { + hasStartPoint = false; + CGContextAddLineToPoint(context, startPoint.x, startPoint.y); + } + } + } + switch (shape->fillRule) { + case NSVG_FILLRULE_EVENODD: + CGContextEOFillPath(context); + break; + default: + CGContextFillPath(context); + break; + } + } + + if (shape->stroke.type != NSVG_PAINT_NONE) { + //CGContextSetStrokeColorWithColor(context, UIColorRGBA(shape->fill.color, shape->opacity).CGColor); + CGContextSetStrokeColorWithColor(context, [foregroundColor colorWithAlphaComponent:shape->opacity].CGColor); + CGContextSetMiterLimit(context, shape->miterLimit); + + CGContextSetLineWidth(context, shape->strokeWidth); + switch (shape->strokeLineCap) { + case NSVG_CAP_BUTT: + CGContextSetLineCap(context, kCGLineCapButt); + break; + case NSVG_CAP_ROUND: + CGContextSetLineCap(context, kCGLineCapRound); + break; + case NSVG_CAP_SQUARE: + CGContextSetLineCap(context, kCGLineCapSquare); + break; + default: + break; + } + switch (shape->strokeLineJoin) { + case NSVG_JOIN_BEVEL: + CGContextSetLineJoin(context, kCGLineJoinBevel); + break; + case NSVG_JOIN_MITER: + CGContextSetLineCap(context, kCGLineJoinMiter); + break; + case NSVG_JOIN_ROUND: + CGContextSetLineCap(context, kCGLineJoinRound); + break; + default: + break; + } + + for (NSVGpath *path = shape->paths; path != NULL; path = path->next) { + CGContextBeginPath(context); + CGContextMoveToPoint(context, path->pts[0], path->pts[1]); + for (int i = 0; i < path->npts - 1; i += 3) { + float *p = &path->pts[i * 2]; + CGContextAddCurveToPoint(context, p[2], p[3], p[4], p[5], p[6], p[7]); + } + + if (path->closed) { + CGContextClosePath(context); + } + CGContextStrokePath(context); + } + } + } + + UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + deltaTime = -1.0f * [startTime timeIntervalSinceNow]; + printf("drawingTime %fx%f = %f\n", size.width, size.height, deltaTime); + + nsvgDelete(image); + + return resultImage; +} diff --git a/submodules/Svg/Sources/nanosvg.c b/submodules/Svg/Sources/nanosvg.c new file mode 100755 index 0000000000..aa3b4d429b --- /dev/null +++ b/submodules/Svg/Sources/nanosvg.c @@ -0,0 +1,6 @@ +#include +#include +#include +#include +#define NANOSVG_IMPLEMENTATION +#include "nanosvg.h" diff --git a/submodules/Svg/Sources/nanosvg.h b/submodules/Svg/Sources/nanosvg.h new file mode 100644 index 0000000000..4f36f72cdc --- /dev/null +++ b/submodules/Svg/Sources/nanosvg.h @@ -0,0 +1,2982 @@ +/* + * Copyright (c) 2013-14 Mikko Mononen memon@inside.org + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + * + * The SVG parser is based on Anti-Grain Geometry 2.4 SVG example + * Copyright (C) 2002-2004 Maxim Shemanarev (McSeem) (http://www.antigrain.com/) + * + * Arc calculation code based on canvg (https://code.google.com/p/canvg/) + * + * Bounding box calculation based on http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html + * + */ + +#ifndef NANOSVG_H +#define NANOSVG_H + +#ifndef NANOSVG_CPLUSPLUS +#ifdef __cplusplus +extern "C" { +#endif +#endif + +// NanoSVG is a simple stupid single-header-file SVG parse. The output of the parser is a list of cubic bezier shapes. +// +// The library suits well for anything from rendering scalable icons in your editor application to prototyping a game. +// +// NanoSVG supports a wide range of SVG features, but something may be missing, feel free to create a pull request! +// +// The shapes in the SVG images are transformed by the viewBox and converted to specified units. +// That is, you should get the same looking data as your designed in your favorite app. +// +// NanoSVG can return the paths in few different units. For example if you want to render an image, you may choose +// to get the paths in pixels, or if you are feeding the data into a CNC-cutter, you may want to use millimeters. +// +// The units passed to NanoSVG should be one of: 'px', 'pt', 'pc' 'mm', 'cm', or 'in'. +// DPI (dots-per-inch) controls how the unit conversion is done. +// +// If you don't know or care about the units stuff, "px" and 96 should get you going. + + +/* Example Usage: + // Load SVG + NSVGimage* image; + image = nsvgParseFromFile("test.svg", "px", 96); + printf("size: %f x %f\n", image->width, image->height); + // Use... + for (NSVGshape *shape = image->shapes; shape != NULL; shape = shape->next) { + for (NSVGpath *path = shape->paths; path != NULL; path = path->next) { + for (int i = 0; i < path->npts-1; i += 3) { + float* p = &path->pts[i*2]; + drawCubicBez(p[0],p[1], p[2],p[3], p[4],p[5], p[6],p[7]); + } + } + } + // Delete + nsvgDelete(image); +*/ + +enum NSVGpaintType { + NSVG_PAINT_NONE = 0, + NSVG_PAINT_COLOR = 1, + NSVG_PAINT_LINEAR_GRADIENT = 2, + NSVG_PAINT_RADIAL_GRADIENT = 3 +}; + +enum NSVGspreadType { + NSVG_SPREAD_PAD = 0, + NSVG_SPREAD_REFLECT = 1, + NSVG_SPREAD_REPEAT = 2 +}; + +enum NSVGlineJoin { + NSVG_JOIN_MITER = 0, + NSVG_JOIN_ROUND = 1, + NSVG_JOIN_BEVEL = 2 +}; + +enum NSVGlineCap { + NSVG_CAP_BUTT = 0, + NSVG_CAP_ROUND = 1, + NSVG_CAP_SQUARE = 2 +}; + +enum NSVGfillRule { + NSVG_FILLRULE_NONZERO = 0, + NSVG_FILLRULE_EVENODD = 1 +}; + +enum NSVGflags { + NSVG_FLAGS_VISIBLE = 0x01 +}; + +typedef struct NSVGgradientStop { + unsigned int color; + float offset; +} NSVGgradientStop; + +typedef struct NSVGgradient { + float xform[6]; + char spread; + float fx, fy; + int nstops; + NSVGgradientStop stops[1]; +} NSVGgradient; + +typedef struct NSVGpaint { + char type; + union { + unsigned int color; + NSVGgradient* gradient; + }; +} NSVGpaint; + +typedef struct NSVGpath +{ + float* pts; // Cubic bezier points: x0,y0, [cpx1,cpx1,cpx2,cpy2,x1,y1], ... + int npts; // Total number of bezier points. + char closed; // Flag indicating if shapes should be treated as closed. + float bounds[4]; // Tight bounding box of the shape [minx,miny,maxx,maxy]. + struct NSVGpath* next; // Pointer to next path, or NULL if last element. +} NSVGpath; + +typedef struct NSVGshape +{ + char id[64]; // Optional 'id' attr of the shape or its group + NSVGpaint fill; // Fill paint + NSVGpaint stroke; // Stroke paint + float opacity; // Opacity of the shape. + float strokeWidth; // Stroke width (scaled). + float strokeDashOffset; // Stroke dash offset (scaled). + float strokeDashArray[8]; // Stroke dash array (scaled). + char strokeDashCount; // Number of dash values in dash array. + char strokeLineJoin; // Stroke join type. + char strokeLineCap; // Stroke cap type. + float miterLimit; // Miter limit + char fillRule; // Fill rule, see NSVGfillRule. + unsigned char flags; // Logical or of NSVG_FLAGS_* flags + float bounds[4]; // Tight bounding box of the shape [minx,miny,maxx,maxy]. + NSVGpath* paths; // Linked list of paths in the image. + struct NSVGshape* next; // Pointer to next shape, or NULL if last element. +} NSVGshape; + +typedef struct NSVGimage +{ + float width; // Width of the image. + float height; // Height of the image. + NSVGshape* shapes; // Linked list of shapes in the image. +} NSVGimage; + +// Parses SVG file from a file, returns SVG image as paths. +NSVGimage* nsvgParseFromFile(const char* filename, const char* units, float dpi); + +// Parses SVG file from a null terminated string, returns SVG image as paths. +// Important note: changes the string. +NSVGimage* nsvgParse(char* input, const char* units, float dpi); + +// Duplicates a path. +NSVGpath* nsvgDuplicatePath(NSVGpath* p); + +// Deletes an image. +void nsvgDelete(NSVGimage* image); + +#ifndef NANOSVG_CPLUSPLUS +#ifdef __cplusplus +} +#endif +#endif + +#endif // NANOSVG_H + +#ifdef NANOSVG_IMPLEMENTATION + +#include +#include +#include + +#define NSVG_PI (3.14159265358979323846264338327f) +#define NSVG_KAPPA90 (0.5522847493f) // Length proportional to radius of a cubic bezier handle for 90deg arcs. + +#define NSVG_ALIGN_MIN 0 +#define NSVG_ALIGN_MID 1 +#define NSVG_ALIGN_MAX 2 +#define NSVG_ALIGN_NONE 0 +#define NSVG_ALIGN_MEET 1 +#define NSVG_ALIGN_SLICE 2 + +#define NSVG_NOTUSED(v) do { (void)(1 ? (void)0 : ( (void)(v) ) ); } while(0) +#define NSVG_RGB(r, g, b) (((unsigned int)r) | ((unsigned int)g << 8) | ((unsigned int)b << 16)) + +#ifdef _MSC_VER + #pragma warning (disable: 4996) // Switch off security warnings + #pragma warning (disable: 4100) // Switch off unreferenced formal parameter warnings + #ifdef __cplusplus + #define NSVG_INLINE inline + #else + #define NSVG_INLINE + #endif +#else + #define NSVG_INLINE inline +#endif + + +static int nsvg__isspace(char c) +{ + return strchr(" \t\n\v\f\r", c) != 0; +} + +static int nsvg__isdigit(char c) +{ + return c >= '0' && c <= '9'; +} + +static int nsvg__isnum(char c) +{ + return strchr("0123456789+-.eE", c) != 0; +} + +static NSVG_INLINE float nsvg__minf(float a, float b) { return a < b ? a : b; } +static NSVG_INLINE float nsvg__maxf(float a, float b) { return a > b ? a : b; } + + +// Simple XML parser + +#define NSVG_XML_TAG 1 +#define NSVG_XML_CONTENT 2 +#define NSVG_XML_MAX_ATTRIBS 256 + +static void nsvg__parseContent(char* s, + void (*contentCb)(void* ud, const char* s), + void* ud) +{ + // Trim start white spaces + while (*s && nsvg__isspace(*s)) s++; + if (!*s) return; + + if (contentCb) + (*contentCb)(ud, s); +} + +static void nsvg__parseElement(char* s, + void (*startelCb)(void* ud, const char* el, const char** attr), + void (*endelCb)(void* ud, const char* el), + void* ud) +{ + const char* attr[NSVG_XML_MAX_ATTRIBS]; + int nattr = 0; + char* name; + int start = 0; + int end = 0; + char quote; + + // Skip white space after the '<' + while (*s && nsvg__isspace(*s)) s++; + + // Check if the tag is end tag + if (*s == '/') { + s++; + end = 1; + } else { + start = 1; + } + + // Skip comments, data and preprocessor stuff. + if (!*s || *s == '?' || *s == '!') + return; + + // Get tag name + name = s; + while (*s && !nsvg__isspace(*s)) s++; + if (*s) { *s++ = '\0'; } + + // Get attribs + while (!end && *s && nattr < NSVG_XML_MAX_ATTRIBS-3) { + char* name = NULL; + char* value = NULL; + + // Skip white space before the attrib name + while (*s && nsvg__isspace(*s)) s++; + if (!*s) break; + if (*s == '/') { + end = 1; + break; + } + name = s; + // Find end of the attrib name. + while (*s && !nsvg__isspace(*s) && *s != '=') s++; + if (*s) { *s++ = '\0'; } + // Skip until the beginning of the value. + while (*s && *s != '\"' && *s != '\'') s++; + if (!*s) break; + quote = *s; + s++; + // Store value and find the end of it. + value = s; + while (*s && *s != quote) s++; + if (*s) { *s++ = '\0'; } + + // Store only well formed attributes + if (name && value) { + attr[nattr++] = name; + attr[nattr++] = value; + } + } + + // List terminator + attr[nattr++] = 0; + attr[nattr++] = 0; + + // Call callbacks. + if (start && startelCb) + (*startelCb)(ud, name, attr); + if (end && endelCb) + (*endelCb)(ud, name); +} + +int nsvg__parseXML(char* input, + void (*startelCb)(void* ud, const char* el, const char** attr), + void (*endelCb)(void* ud, const char* el), + void (*contentCb)(void* ud, const char* s), + void* ud) +{ + char* s = input; + char* mark = s; + int state = NSVG_XML_CONTENT; + while (*s) { + if (*s == '<' && state == NSVG_XML_CONTENT) { + // Start of a tag + *s++ = '\0'; + nsvg__parseContent(mark, contentCb, ud); + mark = s; + state = NSVG_XML_TAG; + } else if (*s == '>' && state == NSVG_XML_TAG) { + // Start of a content or new tag. + *s++ = '\0'; + nsvg__parseElement(mark, startelCb, endelCb, ud); + mark = s; + state = NSVG_XML_CONTENT; + } else { + s++; + } + } + + return 1; +} + + +/* Simple SVG parser. */ + +#define NSVG_MAX_ATTR 128 + +enum NSVGgradientUnits { + NSVG_USER_SPACE = 0, + NSVG_OBJECT_SPACE = 1 +}; + +#define NSVG_MAX_DASHES 8 + +enum NSVGunits { + NSVG_UNITS_USER, + NSVG_UNITS_PX, + NSVG_UNITS_PT, + NSVG_UNITS_PC, + NSVG_UNITS_MM, + NSVG_UNITS_CM, + NSVG_UNITS_IN, + NSVG_UNITS_PERCENT, + NSVG_UNITS_EM, + NSVG_UNITS_EX +}; + +typedef struct NSVGcoordinate { + float value; + int units; +} NSVGcoordinate; + +typedef struct NSVGlinearData { + NSVGcoordinate x1, y1, x2, y2; +} NSVGlinearData; + +typedef struct NSVGradialData { + NSVGcoordinate cx, cy, r, fx, fy; +} NSVGradialData; + +typedef struct NSVGgradientData +{ + char id[64]; + char ref[64]; + char type; + union { + NSVGlinearData linear; + NSVGradialData radial; + }; + char spread; + char units; + float xform[6]; + int nstops; + NSVGgradientStop* stops; + struct NSVGgradientData* next; +} NSVGgradientData; + +typedef struct NSVGattrib +{ + char id[64]; + float xform[6]; + unsigned int fillColor; + unsigned int strokeColor; + float opacity; + float fillOpacity; + float strokeOpacity; + char fillGradient[64]; + char strokeGradient[64]; + float strokeWidth; + float strokeDashOffset; + float strokeDashArray[NSVG_MAX_DASHES]; + int strokeDashCount; + char strokeLineJoin; + char strokeLineCap; + float miterLimit; + char fillRule; + float fontSize; + unsigned int stopColor; + float stopOpacity; + float stopOffset; + char hasFill; + char hasStroke; + char visible; +} NSVGattrib; + +typedef struct NSVGparser +{ + NSVGattrib attr[NSVG_MAX_ATTR]; + int attrHead; + float* pts; + int npts; + int cpts; + NSVGpath* plist; + NSVGimage* image; + NSVGgradientData* gradients; + NSVGshape* shapesTail; + float viewMinx, viewMiny, viewWidth, viewHeight; + int alignX, alignY, alignType; + float dpi; + char pathFlag; + char defsFlag; +} NSVGparser; + +static void nsvg__xformIdentity(float* t) +{ + t[0] = 1.0f; t[1] = 0.0f; + t[2] = 0.0f; t[3] = 1.0f; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformSetTranslation(float* t, float tx, float ty) +{ + t[0] = 1.0f; t[1] = 0.0f; + t[2] = 0.0f; t[3] = 1.0f; + t[4] = tx; t[5] = ty; +} + +static void nsvg__xformSetScale(float* t, float sx, float sy) +{ + t[0] = sx; t[1] = 0.0f; + t[2] = 0.0f; t[3] = sy; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformSetSkewX(float* t, float a) +{ + t[0] = 1.0f; t[1] = 0.0f; + t[2] = tanf(a); t[3] = 1.0f; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformSetSkewY(float* t, float a) +{ + t[0] = 1.0f; t[1] = tanf(a); + t[2] = 0.0f; t[3] = 1.0f; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformSetRotation(float* t, float a) +{ + float cs = cosf(a), sn = sinf(a); + t[0] = cs; t[1] = sn; + t[2] = -sn; t[3] = cs; + t[4] = 0.0f; t[5] = 0.0f; +} + +static void nsvg__xformMultiply(float* t, float* s) +{ + float t0 = t[0] * s[0] + t[1] * s[2]; + float t2 = t[2] * s[0] + t[3] * s[2]; + float t4 = t[4] * s[0] + t[5] * s[2] + s[4]; + t[1] = t[0] * s[1] + t[1] * s[3]; + t[3] = t[2] * s[1] + t[3] * s[3]; + t[5] = t[4] * s[1] + t[5] * s[3] + s[5]; + t[0] = t0; + t[2] = t2; + t[4] = t4; +} + +static void nsvg__xformInverse(float* inv, float* t) +{ + double invdet, det = (double)t[0] * t[3] - (double)t[2] * t[1]; + if (det > -1e-6 && det < 1e-6) { + nsvg__xformIdentity(t); + return; + } + invdet = 1.0 / det; + inv[0] = (float)(t[3] * invdet); + inv[2] = (float)(-t[2] * invdet); + inv[4] = (float)(((double)t[2] * t[5] - (double)t[3] * t[4]) * invdet); + inv[1] = (float)(-t[1] * invdet); + inv[3] = (float)(t[0] * invdet); + inv[5] = (float)(((double)t[1] * t[4] - (double)t[0] * t[5]) * invdet); +} + +static void nsvg__xformPremultiply(float* t, float* s) +{ + float s2[6]; + memcpy(s2, s, sizeof(float)*6); + nsvg__xformMultiply(s2, t); + memcpy(t, s2, sizeof(float)*6); +} + +static void nsvg__xformPoint(float* dx, float* dy, float x, float y, float* t) +{ + *dx = x*t[0] + y*t[2] + t[4]; + *dy = x*t[1] + y*t[3] + t[5]; +} + +static void nsvg__xformVec(float* dx, float* dy, float x, float y, float* t) +{ + *dx = x*t[0] + y*t[2]; + *dy = x*t[1] + y*t[3]; +} + +#define NSVG_EPSILON (1e-12) + +static int nsvg__ptInBounds(float* pt, float* bounds) +{ + return pt[0] >= bounds[0] && pt[0] <= bounds[2] && pt[1] >= bounds[1] && pt[1] <= bounds[3]; +} + + +static double nsvg__evalBezier(double t, double p0, double p1, double p2, double p3) +{ + double it = 1.0-t; + return it*it*it*p0 + 3.0*it*it*t*p1 + 3.0*it*t*t*p2 + t*t*t*p3; +} + +static void nsvg__curveBounds(float* bounds, float* curve) +{ + int i, j, count; + double roots[2], a, b, c, b2ac, t, v; + float* v0 = &curve[0]; + float* v1 = &curve[2]; + float* v2 = &curve[4]; + float* v3 = &curve[6]; + + // Start the bounding box by end points + bounds[0] = nsvg__minf(v0[0], v3[0]); + bounds[1] = nsvg__minf(v0[1], v3[1]); + bounds[2] = nsvg__maxf(v0[0], v3[0]); + bounds[3] = nsvg__maxf(v0[1], v3[1]); + + // Bezier curve fits inside the convex hull of it's control points. + // If control points are inside the bounds, we're done. + if (nsvg__ptInBounds(v1, bounds) && nsvg__ptInBounds(v2, bounds)) + return; + + // Add bezier curve inflection points in X and Y. + for (i = 0; i < 2; i++) { + a = -3.0 * v0[i] + 9.0 * v1[i] - 9.0 * v2[i] + 3.0 * v3[i]; + b = 6.0 * v0[i] - 12.0 * v1[i] + 6.0 * v2[i]; + c = 3.0 * v1[i] - 3.0 * v0[i]; + count = 0; + if (fabs(a) < NSVG_EPSILON) { + if (fabs(b) > NSVG_EPSILON) { + t = -c / b; + if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) + roots[count++] = t; + } + } else { + b2ac = b*b - 4.0*c*a; + if (b2ac > NSVG_EPSILON) { + t = (-b + sqrt(b2ac)) / (2.0 * a); + if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) + roots[count++] = t; + t = (-b - sqrt(b2ac)) / (2.0 * a); + if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) + roots[count++] = t; + } + } + for (j = 0; j < count; j++) { + v = nsvg__evalBezier(roots[j], v0[i], v1[i], v2[i], v3[i]); + bounds[0+i] = nsvg__minf(bounds[0+i], (float)v); + bounds[2+i] = nsvg__maxf(bounds[2+i], (float)v); + } + } +} + +static NSVGparser* nsvg__createParser() +{ + NSVGparser* p; + p = (NSVGparser*)malloc(sizeof(NSVGparser)); + if (p == NULL) goto error; + memset(p, 0, sizeof(NSVGparser)); + + p->image = (NSVGimage*)malloc(sizeof(NSVGimage)); + if (p->image == NULL) goto error; + memset(p->image, 0, sizeof(NSVGimage)); + + // Init style + nsvg__xformIdentity(p->attr[0].xform); + memset(p->attr[0].id, 0, sizeof p->attr[0].id); + p->attr[0].fillColor = NSVG_RGB(0,0,0); + p->attr[0].strokeColor = NSVG_RGB(0,0,0); + p->attr[0].opacity = 1; + p->attr[0].fillOpacity = 1; + p->attr[0].strokeOpacity = 1; + p->attr[0].stopOpacity = 1; + p->attr[0].strokeWidth = 1; + p->attr[0].strokeLineJoin = NSVG_JOIN_MITER; + p->attr[0].strokeLineCap = NSVG_CAP_BUTT; + p->attr[0].miterLimit = 4; + p->attr[0].fillRule = NSVG_FILLRULE_NONZERO; + p->attr[0].hasFill = 1; + p->attr[0].visible = 1; + + return p; + +error: + if (p) { + if (p->image) free(p->image); + free(p); + } + return NULL; +} + +static void nsvg__deletePaths(NSVGpath* path) +{ + while (path) { + NSVGpath *next = path->next; + if (path->pts != NULL) + free(path->pts); + free(path); + path = next; + } +} + +static void nsvg__deletePaint(NSVGpaint* paint) +{ + if (paint->type == NSVG_PAINT_LINEAR_GRADIENT || paint->type == NSVG_PAINT_RADIAL_GRADIENT) + free(paint->gradient); +} + +static void nsvg__deleteGradientData(NSVGgradientData* grad) +{ + NSVGgradientData* next; + while (grad != NULL) { + next = grad->next; + free(grad->stops); + free(grad); + grad = next; + } +} + +static void nsvg__deleteParser(NSVGparser* p) +{ + if (p != NULL) { + nsvg__deletePaths(p->plist); + nsvg__deleteGradientData(p->gradients); + nsvgDelete(p->image); + free(p->pts); + free(p); + } +} + +static void nsvg__resetPath(NSVGparser* p) +{ + p->npts = 0; +} + +static void nsvg__addPoint(NSVGparser* p, float x, float y) +{ + if (p->npts+1 > p->cpts) { + p->cpts = p->cpts ? p->cpts*2 : 8; + p->pts = (float*)realloc(p->pts, p->cpts*2*sizeof(float)); + if (!p->pts) return; + } + p->pts[p->npts*2+0] = x; + p->pts[p->npts*2+1] = y; + p->npts++; +} + +static void nsvg__moveTo(NSVGparser* p, float x, float y) +{ + if (p->npts > 0) { + p->pts[(p->npts-1)*2+0] = x; + p->pts[(p->npts-1)*2+1] = y; + } else { + nsvg__addPoint(p, x, y); + } +} + +static void nsvg__lineTo(NSVGparser* p, float x, float y) +{ + float px,py, dx,dy; + if (p->npts > 0) { + px = p->pts[(p->npts-1)*2+0]; + py = p->pts[(p->npts-1)*2+1]; + dx = x - px; + dy = y - py; + nsvg__addPoint(p, px + dx/3.0f, py + dy/3.0f); + nsvg__addPoint(p, x - dx/3.0f, y - dy/3.0f); + nsvg__addPoint(p, x, y); + } +} + +static void nsvg__cubicBezTo(NSVGparser* p, float cpx1, float cpy1, float cpx2, float cpy2, float x, float y) +{ + nsvg__addPoint(p, cpx1, cpy1); + nsvg__addPoint(p, cpx2, cpy2); + nsvg__addPoint(p, x, y); +} + +static NSVGattrib* nsvg__getAttr(NSVGparser* p) +{ + return &p->attr[p->attrHead]; +} + +static void nsvg__pushAttr(NSVGparser* p) +{ + if (p->attrHead < NSVG_MAX_ATTR-1) { + p->attrHead++; + memcpy(&p->attr[p->attrHead], &p->attr[p->attrHead-1], sizeof(NSVGattrib)); + } +} + +static void nsvg__popAttr(NSVGparser* p) +{ + if (p->attrHead > 0) + p->attrHead--; +} + +static float nsvg__actualOrigX(NSVGparser* p) +{ + return p->viewMinx; +} + +static float nsvg__actualOrigY(NSVGparser* p) +{ + return p->viewMiny; +} + +static float nsvg__actualWidth(NSVGparser* p) +{ + return p->viewWidth; +} + +static float nsvg__actualHeight(NSVGparser* p) +{ + return p->viewHeight; +} + +static float nsvg__actualLength(NSVGparser* p) +{ + float w = nsvg__actualWidth(p), h = nsvg__actualHeight(p); + return sqrtf(w*w + h*h) / sqrtf(2.0f); +} + +static float nsvg__convertToPixels(NSVGparser* p, NSVGcoordinate c, float orig, float length) +{ + NSVGattrib* attr = nsvg__getAttr(p); + switch (c.units) { + case NSVG_UNITS_USER: return c.value; + case NSVG_UNITS_PX: return c.value; + case NSVG_UNITS_PT: return c.value / 72.0f * p->dpi; + case NSVG_UNITS_PC: return c.value / 6.0f * p->dpi; + case NSVG_UNITS_MM: return c.value / 25.4f * p->dpi; + case NSVG_UNITS_CM: return c.value / 2.54f * p->dpi; + case NSVG_UNITS_IN: return c.value * p->dpi; + case NSVG_UNITS_EM: return c.value * attr->fontSize; + case NSVG_UNITS_EX: return c.value * attr->fontSize * 0.52f; // x-height of Helvetica. + case NSVG_UNITS_PERCENT: return orig + c.value / 100.0f * length; + default: return c.value; + } + return c.value; +} + +static NSVGgradientData* nsvg__findGradientData(NSVGparser* p, const char* id) +{ + NSVGgradientData* grad = p->gradients; + while (grad) { + if (strcmp(grad->id, id) == 0) + return grad; + grad = grad->next; + } + return NULL; +} + +static NSVGgradient* nsvg__createGradient(NSVGparser* p, const char* id, const float* localBounds, char* paintType) +{ + NSVGattrib* attr = nsvg__getAttr(p); + NSVGgradientData* data = NULL; + NSVGgradientData* ref = NULL; + NSVGgradientStop* stops = NULL; + NSVGgradient* grad; + float ox, oy, sw, sh, sl; + int nstops = 0; + + data = nsvg__findGradientData(p, id); + if (data == NULL) return NULL; + + // TODO: use ref to fill in all unset values too. + ref = data; + while (ref != NULL) { + if (stops == NULL && ref->stops != NULL) { + stops = ref->stops; + nstops = ref->nstops; + break; + } + ref = nsvg__findGradientData(p, ref->ref); + } + if (stops == NULL) return NULL; + + grad = (NSVGgradient*)malloc(sizeof(NSVGgradient) + sizeof(NSVGgradientStop)*(nstops-1)); + if (grad == NULL) return NULL; + + // The shape width and height. + if (data->units == NSVG_OBJECT_SPACE) { + ox = localBounds[0]; + oy = localBounds[1]; + sw = localBounds[2] - localBounds[0]; + sh = localBounds[3] - localBounds[1]; + } else { + ox = nsvg__actualOrigX(p); + oy = nsvg__actualOrigY(p); + sw = nsvg__actualWidth(p); + sh = nsvg__actualHeight(p); + } + sl = sqrtf(sw*sw + sh*sh) / sqrtf(2.0f); + + if (data->type == NSVG_PAINT_LINEAR_GRADIENT) { + float x1, y1, x2, y2, dx, dy; + x1 = nsvg__convertToPixels(p, data->linear.x1, ox, sw); + y1 = nsvg__convertToPixels(p, data->linear.y1, oy, sh); + x2 = nsvg__convertToPixels(p, data->linear.x2, ox, sw); + y2 = nsvg__convertToPixels(p, data->linear.y2, oy, sh); + // Calculate transform aligned to the line + dx = x2 - x1; + dy = y2 - y1; + grad->xform[0] = dy; grad->xform[1] = -dx; + grad->xform[2] = dx; grad->xform[3] = dy; + grad->xform[4] = x1; grad->xform[5] = y1; + } else { + float cx, cy, fx, fy, r; + cx = nsvg__convertToPixels(p, data->radial.cx, ox, sw); + cy = nsvg__convertToPixels(p, data->radial.cy, oy, sh); + fx = nsvg__convertToPixels(p, data->radial.fx, ox, sw); + fy = nsvg__convertToPixels(p, data->radial.fy, oy, sh); + r = nsvg__convertToPixels(p, data->radial.r, 0, sl); + // Calculate transform aligned to the circle + grad->xform[0] = r; grad->xform[1] = 0; + grad->xform[2] = 0; grad->xform[3] = r; + grad->xform[4] = cx; grad->xform[5] = cy; + grad->fx = fx / r; + grad->fy = fy / r; + } + + nsvg__xformMultiply(grad->xform, data->xform); + nsvg__xformMultiply(grad->xform, attr->xform); + + grad->spread = data->spread; + memcpy(grad->stops, stops, nstops*sizeof(NSVGgradientStop)); + grad->nstops = nstops; + + *paintType = data->type; + + return grad; +} + +static float nsvg__getAverageScale(float* t) +{ + float sx = sqrtf(t[0]*t[0] + t[2]*t[2]); + float sy = sqrtf(t[1]*t[1] + t[3]*t[3]); + return (sx + sy) * 0.5f; +} + +static void nsvg__getLocalBounds(float* bounds, NSVGshape *shape, float* xform) +{ + NSVGpath* path; + float curve[4*2], curveBounds[4]; + int i, first = 1; + for (path = shape->paths; path != NULL; path = path->next) { + nsvg__xformPoint(&curve[0], &curve[1], path->pts[0], path->pts[1], xform); + for (i = 0; i < path->npts-1; i += 3) { + nsvg__xformPoint(&curve[2], &curve[3], path->pts[(i+1)*2], path->pts[(i+1)*2+1], xform); + nsvg__xformPoint(&curve[4], &curve[5], path->pts[(i+2)*2], path->pts[(i+2)*2+1], xform); + nsvg__xformPoint(&curve[6], &curve[7], path->pts[(i+3)*2], path->pts[(i+3)*2+1], xform); + nsvg__curveBounds(curveBounds, curve); + if (first) { + bounds[0] = curveBounds[0]; + bounds[1] = curveBounds[1]; + bounds[2] = curveBounds[2]; + bounds[3] = curveBounds[3]; + first = 0; + } else { + bounds[0] = nsvg__minf(bounds[0], curveBounds[0]); + bounds[1] = nsvg__minf(bounds[1], curveBounds[1]); + bounds[2] = nsvg__maxf(bounds[2], curveBounds[2]); + bounds[3] = nsvg__maxf(bounds[3], curveBounds[3]); + } + curve[0] = curve[6]; + curve[1] = curve[7]; + } + } +} + +static void nsvg__addShape(NSVGparser* p) +{ + NSVGattrib* attr = nsvg__getAttr(p); + float scale = 1.0f; + NSVGshape* shape; + NSVGpath* path; + int i; + + if (p->plist == NULL) + return; + + shape = (NSVGshape*)malloc(sizeof(NSVGshape)); + if (shape == NULL) goto error; + memset(shape, 0, sizeof(NSVGshape)); + + memcpy(shape->id, attr->id, sizeof shape->id); + scale = nsvg__getAverageScale(attr->xform); + shape->strokeWidth = attr->strokeWidth * scale; + shape->strokeDashOffset = attr->strokeDashOffset * scale; + shape->strokeDashCount = (char)attr->strokeDashCount; + for (i = 0; i < attr->strokeDashCount; i++) + shape->strokeDashArray[i] = attr->strokeDashArray[i] * scale; + shape->strokeLineJoin = attr->strokeLineJoin; + shape->strokeLineCap = attr->strokeLineCap; + shape->miterLimit = attr->miterLimit; + shape->fillRule = attr->fillRule; + shape->opacity = attr->opacity; + + shape->paths = p->plist; + p->plist = NULL; + + // Calculate shape bounds + shape->bounds[0] = shape->paths->bounds[0]; + shape->bounds[1] = shape->paths->bounds[1]; + shape->bounds[2] = shape->paths->bounds[2]; + shape->bounds[3] = shape->paths->bounds[3]; + for (path = shape->paths->next; path != NULL; path = path->next) { + shape->bounds[0] = nsvg__minf(shape->bounds[0], path->bounds[0]); + shape->bounds[1] = nsvg__minf(shape->bounds[1], path->bounds[1]); + shape->bounds[2] = nsvg__maxf(shape->bounds[2], path->bounds[2]); + shape->bounds[3] = nsvg__maxf(shape->bounds[3], path->bounds[3]); + } + + // Set fill + if (attr->hasFill == 0) { + shape->fill.type = NSVG_PAINT_NONE; + } else if (attr->hasFill == 1) { + shape->fill.type = NSVG_PAINT_COLOR; + shape->fill.color = attr->fillColor; + shape->fill.color |= (unsigned int)(attr->fillOpacity*255) << 24; + } else if (attr->hasFill == 2) { + float inv[6], localBounds[4]; + nsvg__xformInverse(inv, attr->xform); + nsvg__getLocalBounds(localBounds, shape, inv); + shape->fill.gradient = nsvg__createGradient(p, attr->fillGradient, localBounds, &shape->fill.type); + if (shape->fill.gradient == NULL) { + shape->fill.type = NSVG_PAINT_NONE; + } + } + + // Set stroke + if (attr->hasStroke == 0) { + shape->stroke.type = NSVG_PAINT_NONE; + } else if (attr->hasStroke == 1) { + shape->stroke.type = NSVG_PAINT_COLOR; + shape->stroke.color = attr->strokeColor; + shape->stroke.color |= (unsigned int)(attr->strokeOpacity*255) << 24; + } else if (attr->hasStroke == 2) { + float inv[6], localBounds[4]; + nsvg__xformInverse(inv, attr->xform); + nsvg__getLocalBounds(localBounds, shape, inv); + shape->stroke.gradient = nsvg__createGradient(p, attr->strokeGradient, localBounds, &shape->stroke.type); + if (shape->stroke.gradient == NULL) + shape->stroke.type = NSVG_PAINT_NONE; + } + + // Set flags + shape->flags = (attr->visible ? NSVG_FLAGS_VISIBLE : 0x00); + + // Add to tail + if (p->image->shapes == NULL) + p->image->shapes = shape; + else + p->shapesTail->next = shape; + p->shapesTail = shape; + + return; + +error: + if (shape) free(shape); +} + +static void nsvg__addPath(NSVGparser* p, char closed) +{ + NSVGattrib* attr = nsvg__getAttr(p); + NSVGpath* path = NULL; + float bounds[4]; + float* curve; + int i; + + if (p->npts < 4) + return; + + /*if (closed) + nsvg__lineTo(p, p->pts[0], p->pts[1]);*/ + + path = (NSVGpath*)malloc(sizeof(NSVGpath)); + if (path == NULL) goto error; + memset(path, 0, sizeof(NSVGpath)); + + path->pts = (float*)malloc(p->npts*2*sizeof(float)); + if (path->pts == NULL) goto error; + path->closed = closed; + path->npts = p->npts; + + // Transform path. + for (i = 0; i < p->npts; ++i) + nsvg__xformPoint(&path->pts[i*2], &path->pts[i*2+1], p->pts[i*2], p->pts[i*2+1], attr->xform); + + // Find bounds + for (i = 0; i < path->npts-1; i += 3) { + curve = &path->pts[i*2]; + nsvg__curveBounds(bounds, curve); + if (i == 0) { + path->bounds[0] = bounds[0]; + path->bounds[1] = bounds[1]; + path->bounds[2] = bounds[2]; + path->bounds[3] = bounds[3]; + } else { + path->bounds[0] = nsvg__minf(path->bounds[0], bounds[0]); + path->bounds[1] = nsvg__minf(path->bounds[1], bounds[1]); + path->bounds[2] = nsvg__maxf(path->bounds[2], bounds[2]); + path->bounds[3] = nsvg__maxf(path->bounds[3], bounds[3]); + } + } + + path->next = p->plist; + p->plist = path; + + return; + +error: + if (path != NULL) { + if (path->pts != NULL) free(path->pts); + free(path); + } +} + +// We roll our own string to float because the std library one uses locale and messes things up. +static double nsvg__atof(const char* s) +{ + char* cur = (char*)s; + char* end = NULL; + double res = 0.0, sign = 1.0; + long long intPart = 0, fracPart = 0; + char hasIntPart = 0, hasFracPart = 0; + + // Parse optional sign + if (*cur == '+') { + cur++; + } else if (*cur == '-') { + sign = -1; + cur++; + } + + // Parse integer part + if (nsvg__isdigit(*cur)) { + // Parse digit sequence + intPart = strtoll(cur, &end, 10); + if (cur != end) { + res = (double)intPart; + hasIntPart = 1; + cur = end; + } + } + + // Parse fractional part. + if (*cur == '.') { + cur++; // Skip '.' + if (nsvg__isdigit(*cur)) { + // Parse digit sequence + fracPart = strtoll(cur, &end, 10); + if (cur != end) { + res += (double)fracPart / pow(10.0, (double)(end - cur)); + hasFracPart = 1; + cur = end; + } + } + } + + // A valid number should have integer or fractional part. + if (!hasIntPart && !hasFracPart) + return 0.0; + + // Parse optional exponent + if (*cur == 'e' || *cur == 'E') { + long expPart = 0; + cur++; // skip 'E' + expPart = strtol(cur, &end, 10); // Parse digit sequence with sign + if (cur != end) { + res *= pow(10.0, (double)expPart); + } + } + + return res * sign; +} + + +static const char* nsvg__parseNumber(const char* s, char* it, const int size) +{ + const int last = size-1; + int i = 0; + + // sign + if (*s == '-' || *s == '+') { + if (i < last) it[i++] = *s; + s++; + } + // integer part + while (*s && nsvg__isdigit(*s)) { + if (i < last) it[i++] = *s; + s++; + } + if (*s == '.') { + // decimal point + if (i < last) it[i++] = *s; + s++; + // fraction part + while (*s && nsvg__isdigit(*s)) { + if (i < last) it[i++] = *s; + s++; + } + } + // exponent + if ((*s == 'e' || *s == 'E') && (s[1] != 'm' && s[1] != 'x')) { + if (i < last) it[i++] = *s; + s++; + if (*s == '-' || *s == '+') { + if (i < last) it[i++] = *s; + s++; + } + while (*s && nsvg__isdigit(*s)) { + if (i < last) it[i++] = *s; + s++; + } + } + it[i] = '\0'; + + return s; +} + +static const char* nsvg__getNextPathItem(const char* s, char* it) +{ + it[0] = '\0'; + // Skip white spaces and commas + while (*s && (nsvg__isspace(*s) || *s == ',')) s++; + if (!*s) return s; + if (*s == '-' || *s == '+' || *s == '.' || nsvg__isdigit(*s)) { + s = nsvg__parseNumber(s, it, 64); + } else { + // Parse command + it[0] = *s++; + it[1] = '\0'; + return s; + } + + return s; +} + +static unsigned int nsvg__parseColorHex(const char* str) +{ + unsigned int c = 0, r = 0, g = 0, b = 0; + int n = 0; + str++; // skip # + // Calculate number of characters. + while(str[n] && !nsvg__isspace(str[n])) + n++; + if (n == 6) { + sscanf(str, "%x", &c); + } else if (n == 3) { + sscanf(str, "%x", &c); + c = (c&0xf) | ((c&0xf0) << 4) | ((c&0xf00) << 8); + c |= c<<4; + } + r = (c >> 16) & 0xff; + g = (c >> 8) & 0xff; + b = c & 0xff; + return NSVG_RGB(r,g,b); +} + +static unsigned int nsvg__parseColorRGB(const char* str) +{ + int r = -1, g = -1, b = -1; + char s1[32]="", s2[32]=""; + sscanf(str + 4, "%d%[%%, \t]%d%[%%, \t]%d", &r, s1, &g, s2, &b); + if (strchr(s1, '%')) { + return NSVG_RGB((r*255)/100,(g*255)/100,(b*255)/100); + } else { + return NSVG_RGB(r,g,b); + } +} + +typedef struct NSVGNamedColor { + const char* name; + unsigned int color; +} NSVGNamedColor; + +NSVGNamedColor nsvg__colors[] = { + + { "red", NSVG_RGB(255, 0, 0) }, + { "green", NSVG_RGB( 0, 128, 0) }, + { "blue", NSVG_RGB( 0, 0, 255) }, + { "yellow", NSVG_RGB(255, 255, 0) }, + { "cyan", NSVG_RGB( 0, 255, 255) }, + { "magenta", NSVG_RGB(255, 0, 255) }, + { "black", NSVG_RGB( 0, 0, 0) }, + { "grey", NSVG_RGB(128, 128, 128) }, + { "gray", NSVG_RGB(128, 128, 128) }, + { "white", NSVG_RGB(255, 255, 255) }, + +#ifdef NANOSVG_ALL_COLOR_KEYWORDS + { "aliceblue", NSVG_RGB(240, 248, 255) }, + { "antiquewhite", NSVG_RGB(250, 235, 215) }, + { "aqua", NSVG_RGB( 0, 255, 255) }, + { "aquamarine", NSVG_RGB(127, 255, 212) }, + { "azure", NSVG_RGB(240, 255, 255) }, + { "beige", NSVG_RGB(245, 245, 220) }, + { "bisque", NSVG_RGB(255, 228, 196) }, + { "blanchedalmond", NSVG_RGB(255, 235, 205) }, + { "blueviolet", NSVG_RGB(138, 43, 226) }, + { "brown", NSVG_RGB(165, 42, 42) }, + { "burlywood", NSVG_RGB(222, 184, 135) }, + { "cadetblue", NSVG_RGB( 95, 158, 160) }, + { "chartreuse", NSVG_RGB(127, 255, 0) }, + { "chocolate", NSVG_RGB(210, 105, 30) }, + { "coral", NSVG_RGB(255, 127, 80) }, + { "cornflowerblue", NSVG_RGB(100, 149, 237) }, + { "cornsilk", NSVG_RGB(255, 248, 220) }, + { "crimson", NSVG_RGB(220, 20, 60) }, + { "darkblue", NSVG_RGB( 0, 0, 139) }, + { "darkcyan", NSVG_RGB( 0, 139, 139) }, + { "darkgoldenrod", NSVG_RGB(184, 134, 11) }, + { "darkgray", NSVG_RGB(169, 169, 169) }, + { "darkgreen", NSVG_RGB( 0, 100, 0) }, + { "darkgrey", NSVG_RGB(169, 169, 169) }, + { "darkkhaki", NSVG_RGB(189, 183, 107) }, + { "darkmagenta", NSVG_RGB(139, 0, 139) }, + { "darkolivegreen", NSVG_RGB( 85, 107, 47) }, + { "darkorange", NSVG_RGB(255, 140, 0) }, + { "darkorchid", NSVG_RGB(153, 50, 204) }, + { "darkred", NSVG_RGB(139, 0, 0) }, + { "darksalmon", NSVG_RGB(233, 150, 122) }, + { "darkseagreen", NSVG_RGB(143, 188, 143) }, + { "darkslateblue", NSVG_RGB( 72, 61, 139) }, + { "darkslategray", NSVG_RGB( 47, 79, 79) }, + { "darkslategrey", NSVG_RGB( 47, 79, 79) }, + { "darkturquoise", NSVG_RGB( 0, 206, 209) }, + { "darkviolet", NSVG_RGB(148, 0, 211) }, + { "deeppink", NSVG_RGB(255, 20, 147) }, + { "deepskyblue", NSVG_RGB( 0, 191, 255) }, + { "dimgray", NSVG_RGB(105, 105, 105) }, + { "dimgrey", NSVG_RGB(105, 105, 105) }, + { "dodgerblue", NSVG_RGB( 30, 144, 255) }, + { "firebrick", NSVG_RGB(178, 34, 34) }, + { "floralwhite", NSVG_RGB(255, 250, 240) }, + { "forestgreen", NSVG_RGB( 34, 139, 34) }, + { "fuchsia", NSVG_RGB(255, 0, 255) }, + { "gainsboro", NSVG_RGB(220, 220, 220) }, + { "ghostwhite", NSVG_RGB(248, 248, 255) }, + { "gold", NSVG_RGB(255, 215, 0) }, + { "goldenrod", NSVG_RGB(218, 165, 32) }, + { "greenyellow", NSVG_RGB(173, 255, 47) }, + { "honeydew", NSVG_RGB(240, 255, 240) }, + { "hotpink", NSVG_RGB(255, 105, 180) }, + { "indianred", NSVG_RGB(205, 92, 92) }, + { "indigo", NSVG_RGB( 75, 0, 130) }, + { "ivory", NSVG_RGB(255, 255, 240) }, + { "khaki", NSVG_RGB(240, 230, 140) }, + { "lavender", NSVG_RGB(230, 230, 250) }, + { "lavenderblush", NSVG_RGB(255, 240, 245) }, + { "lawngreen", NSVG_RGB(124, 252, 0) }, + { "lemonchiffon", NSVG_RGB(255, 250, 205) }, + { "lightblue", NSVG_RGB(173, 216, 230) }, + { "lightcoral", NSVG_RGB(240, 128, 128) }, + { "lightcyan", NSVG_RGB(224, 255, 255) }, + { "lightgoldenrodyellow", NSVG_RGB(250, 250, 210) }, + { "lightgray", NSVG_RGB(211, 211, 211) }, + { "lightgreen", NSVG_RGB(144, 238, 144) }, + { "lightgrey", NSVG_RGB(211, 211, 211) }, + { "lightpink", NSVG_RGB(255, 182, 193) }, + { "lightsalmon", NSVG_RGB(255, 160, 122) }, + { "lightseagreen", NSVG_RGB( 32, 178, 170) }, + { "lightskyblue", NSVG_RGB(135, 206, 250) }, + { "lightslategray", NSVG_RGB(119, 136, 153) }, + { "lightslategrey", NSVG_RGB(119, 136, 153) }, + { "lightsteelblue", NSVG_RGB(176, 196, 222) }, + { "lightyellow", NSVG_RGB(255, 255, 224) }, + { "lime", NSVG_RGB( 0, 255, 0) }, + { "limegreen", NSVG_RGB( 50, 205, 50) }, + { "linen", NSVG_RGB(250, 240, 230) }, + { "maroon", NSVG_RGB(128, 0, 0) }, + { "mediumaquamarine", NSVG_RGB(102, 205, 170) }, + { "mediumblue", NSVG_RGB( 0, 0, 205) }, + { "mediumorchid", NSVG_RGB(186, 85, 211) }, + { "mediumpurple", NSVG_RGB(147, 112, 219) }, + { "mediumseagreen", NSVG_RGB( 60, 179, 113) }, + { "mediumslateblue", NSVG_RGB(123, 104, 238) }, + { "mediumspringgreen", NSVG_RGB( 0, 250, 154) }, + { "mediumturquoise", NSVG_RGB( 72, 209, 204) }, + { "mediumvioletred", NSVG_RGB(199, 21, 133) }, + { "midnightblue", NSVG_RGB( 25, 25, 112) }, + { "mintcream", NSVG_RGB(245, 255, 250) }, + { "mistyrose", NSVG_RGB(255, 228, 225) }, + { "moccasin", NSVG_RGB(255, 228, 181) }, + { "navajowhite", NSVG_RGB(255, 222, 173) }, + { "navy", NSVG_RGB( 0, 0, 128) }, + { "oldlace", NSVG_RGB(253, 245, 230) }, + { "olive", NSVG_RGB(128, 128, 0) }, + { "olivedrab", NSVG_RGB(107, 142, 35) }, + { "orange", NSVG_RGB(255, 165, 0) }, + { "orangered", NSVG_RGB(255, 69, 0) }, + { "orchid", NSVG_RGB(218, 112, 214) }, + { "palegoldenrod", NSVG_RGB(238, 232, 170) }, + { "palegreen", NSVG_RGB(152, 251, 152) }, + { "paleturquoise", NSVG_RGB(175, 238, 238) }, + { "palevioletred", NSVG_RGB(219, 112, 147) }, + { "papayawhip", NSVG_RGB(255, 239, 213) }, + { "peachpuff", NSVG_RGB(255, 218, 185) }, + { "peru", NSVG_RGB(205, 133, 63) }, + { "pink", NSVG_RGB(255, 192, 203) }, + { "plum", NSVG_RGB(221, 160, 221) }, + { "powderblue", NSVG_RGB(176, 224, 230) }, + { "purple", NSVG_RGB(128, 0, 128) }, + { "rosybrown", NSVG_RGB(188, 143, 143) }, + { "royalblue", NSVG_RGB( 65, 105, 225) }, + { "saddlebrown", NSVG_RGB(139, 69, 19) }, + { "salmon", NSVG_RGB(250, 128, 114) }, + { "sandybrown", NSVG_RGB(244, 164, 96) }, + { "seagreen", NSVG_RGB( 46, 139, 87) }, + { "seashell", NSVG_RGB(255, 245, 238) }, + { "sienna", NSVG_RGB(160, 82, 45) }, + { "silver", NSVG_RGB(192, 192, 192) }, + { "skyblue", NSVG_RGB(135, 206, 235) }, + { "slateblue", NSVG_RGB(106, 90, 205) }, + { "slategray", NSVG_RGB(112, 128, 144) }, + { "slategrey", NSVG_RGB(112, 128, 144) }, + { "snow", NSVG_RGB(255, 250, 250) }, + { "springgreen", NSVG_RGB( 0, 255, 127) }, + { "steelblue", NSVG_RGB( 70, 130, 180) }, + { "tan", NSVG_RGB(210, 180, 140) }, + { "teal", NSVG_RGB( 0, 128, 128) }, + { "thistle", NSVG_RGB(216, 191, 216) }, + { "tomato", NSVG_RGB(255, 99, 71) }, + { "turquoise", NSVG_RGB( 64, 224, 208) }, + { "violet", NSVG_RGB(238, 130, 238) }, + { "wheat", NSVG_RGB(245, 222, 179) }, + { "whitesmoke", NSVG_RGB(245, 245, 245) }, + { "yellowgreen", NSVG_RGB(154, 205, 50) }, +#endif +}; + +static unsigned int nsvg__parseColorName(const char* str) +{ + int i, ncolors = sizeof(nsvg__colors) / sizeof(NSVGNamedColor); + + for (i = 0; i < ncolors; i++) { + if (strcmp(nsvg__colors[i].name, str) == 0) { + return nsvg__colors[i].color; + } + } + + return NSVG_RGB(128, 128, 128); +} + +static unsigned int nsvg__parseColor(const char* str) +{ + size_t len = 0; + while(*str == ' ') ++str; + len = strlen(str); + if (len >= 1 && *str == '#') + return nsvg__parseColorHex(str); + else if (len >= 4 && str[0] == 'r' && str[1] == 'g' && str[2] == 'b' && str[3] == '(') + return nsvg__parseColorRGB(str); + return nsvg__parseColorName(str); +} + +static float nsvg__parseOpacity(const char* str) +{ + float val = nsvg__atof(str); + if (val < 0.0f) val = 0.0f; + if (val > 1.0f) val = 1.0f; + return val; +} + +static float nsvg__parseMiterLimit(const char* str) +{ + float val = nsvg__atof(str); + if (val < 0.0f) val = 0.0f; + return val; +} + +static int nsvg__parseUnits(const char* units) +{ + if (units[0] == 'p' && units[1] == 'x') + return NSVG_UNITS_PX; + else if (units[0] == 'p' && units[1] == 't') + return NSVG_UNITS_PT; + else if (units[0] == 'p' && units[1] == 'c') + return NSVG_UNITS_PC; + else if (units[0] == 'm' && units[1] == 'm') + return NSVG_UNITS_MM; + else if (units[0] == 'c' && units[1] == 'm') + return NSVG_UNITS_CM; + else if (units[0] == 'i' && units[1] == 'n') + return NSVG_UNITS_IN; + else if (units[0] == '%') + return NSVG_UNITS_PERCENT; + else if (units[0] == 'e' && units[1] == 'm') + return NSVG_UNITS_EM; + else if (units[0] == 'e' && units[1] == 'x') + return NSVG_UNITS_EX; + return NSVG_UNITS_USER; +} + +static NSVGcoordinate nsvg__parseCoordinateRaw(const char* str) +{ + NSVGcoordinate coord = {0, NSVG_UNITS_USER}; + char buf[64]; + coord.units = nsvg__parseUnits(nsvg__parseNumber(str, buf, 64)); + coord.value = nsvg__atof(buf); + return coord; +} + +static NSVGcoordinate nsvg__coord(float v, int units) +{ + NSVGcoordinate coord = {v, units}; + return coord; +} + +static float nsvg__parseCoordinate(NSVGparser* p, const char* str, float orig, float length) +{ + NSVGcoordinate coord = nsvg__parseCoordinateRaw(str); + return nsvg__convertToPixels(p, coord, orig, length); +} + +static int nsvg__parseTransformArgs(const char* str, float* args, int maxNa, int* na) +{ + const char* end; + const char* ptr; + char it[64]; + + *na = 0; + ptr = str; + while (*ptr && *ptr != '(') ++ptr; + if (*ptr == 0) + return 1; + end = ptr; + while (*end && *end != ')') ++end; + if (*end == 0) + return 1; + + while (ptr < end) { + if (*ptr == '-' || *ptr == '+' || *ptr == '.' || nsvg__isdigit(*ptr)) { + if (*na >= maxNa) return 0; + ptr = nsvg__parseNumber(ptr, it, 64); + args[(*na)++] = (float)nsvg__atof(it); + } else { + ++ptr; + } + } + return (int)(end - str); +} + + +static int nsvg__parseMatrix(float* xform, const char* str) +{ + float t[6]; + int na = 0; + int len = nsvg__parseTransformArgs(str, t, 6, &na); + if (na != 6) return len; + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseTranslate(float* xform, const char* str) +{ + float args[2]; + float t[6]; + int na = 0; + int len = nsvg__parseTransformArgs(str, args, 2, &na); + if (na == 1) args[1] = 0.0; + + nsvg__xformSetTranslation(t, args[0], args[1]); + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseScale(float* xform, const char* str) +{ + float args[2]; + int na = 0; + float t[6]; + int len = nsvg__parseTransformArgs(str, args, 2, &na); + if (na == 1) args[1] = args[0]; + nsvg__xformSetScale(t, args[0], args[1]); + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseSkewX(float* xform, const char* str) +{ + float args[1]; + int na = 0; + float t[6]; + int len = nsvg__parseTransformArgs(str, args, 1, &na); + nsvg__xformSetSkewX(t, args[0]/180.0f*NSVG_PI); + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseSkewY(float* xform, const char* str) +{ + float args[1]; + int na = 0; + float t[6]; + int len = nsvg__parseTransformArgs(str, args, 1, &na); + nsvg__xformSetSkewY(t, args[0]/180.0f*NSVG_PI); + memcpy(xform, t, sizeof(float)*6); + return len; +} + +static int nsvg__parseRotate(float* xform, const char* str) +{ + float args[3]; + int na = 0; + float m[6]; + float t[6]; + int len = nsvg__parseTransformArgs(str, args, 3, &na); + if (na == 1) + args[1] = args[2] = 0.0f; + nsvg__xformIdentity(m); + + if (na > 1) { + nsvg__xformSetTranslation(t, -args[1], -args[2]); + nsvg__xformMultiply(m, t); + } + + nsvg__xformSetRotation(t, args[0]/180.0f*NSVG_PI); + nsvg__xformMultiply(m, t); + + if (na > 1) { + nsvg__xformSetTranslation(t, args[1], args[2]); + nsvg__xformMultiply(m, t); + } + + memcpy(xform, m, sizeof(float)*6); + + return len; +} + +static void nsvg__parseTransform(float* xform, const char* str) +{ + float t[6]; + nsvg__xformIdentity(xform); + while (*str) + { + if (strncmp(str, "matrix", 6) == 0) + str += nsvg__parseMatrix(t, str); + else if (strncmp(str, "translate", 9) == 0) + str += nsvg__parseTranslate(t, str); + else if (strncmp(str, "scale", 5) == 0) + str += nsvg__parseScale(t, str); + else if (strncmp(str, "rotate", 6) == 0) + str += nsvg__parseRotate(t, str); + else if (strncmp(str, "skewX", 5) == 0) + str += nsvg__parseSkewX(t, str); + else if (strncmp(str, "skewY", 5) == 0) + str += nsvg__parseSkewY(t, str); + else{ + ++str; + continue; + } + + nsvg__xformPremultiply(xform, t); + } +} + +static void nsvg__parseUrl(char* id, const char* str) +{ + int i = 0; + str += 4; // "url("; + if (*str == '#') + str++; + while (i < 63 && *str != ')') { + id[i] = *str++; + i++; + } + id[i] = '\0'; +} + +static char nsvg__parseLineCap(const char* str) +{ + if (strcmp(str, "butt") == 0) + return NSVG_CAP_BUTT; + else if (strcmp(str, "round") == 0) + return NSVG_CAP_ROUND; + else if (strcmp(str, "square") == 0) + return NSVG_CAP_SQUARE; + // TODO: handle inherit. + return NSVG_CAP_BUTT; +} + +static char nsvg__parseLineJoin(const char* str) +{ + if (strcmp(str, "miter") == 0) + return NSVG_JOIN_MITER; + else if (strcmp(str, "round") == 0) + return NSVG_JOIN_ROUND; + else if (strcmp(str, "bevel") == 0) + return NSVG_JOIN_BEVEL; + // TODO: handle inherit. + return NSVG_JOIN_MITER; +} + +static char nsvg__parseFillRule(const char* str) +{ + if (strcmp(str, "nonzero") == 0) + return NSVG_FILLRULE_NONZERO; + else if (strcmp(str, "evenodd") == 0) + return NSVG_FILLRULE_EVENODD; + // TODO: handle inherit. + return NSVG_FILLRULE_NONZERO; +} + +static const char* nsvg__getNextDashItem(const char* s, char* it) +{ + int n = 0; + it[0] = '\0'; + // Skip white spaces and commas + while (*s && (nsvg__isspace(*s) || *s == ',')) s++; + // Advance until whitespace, comma or end. + while (*s && (!nsvg__isspace(*s) && *s != ',')) { + if (n < 63) + it[n++] = *s; + s++; + } + it[n++] = '\0'; + return s; +} + +static int nsvg__parseStrokeDashArray(NSVGparser* p, const char* str, float* strokeDashArray) +{ + char item[64]; + int count = 0, i; + float sum = 0.0f; + + // Handle "none" + if (str[0] == 'n') + return 0; + + // Parse dashes + while (*str) { + str = nsvg__getNextDashItem(str, item); + if (!*item) break; + if (count < NSVG_MAX_DASHES) + strokeDashArray[count++] = fabsf(nsvg__parseCoordinate(p, item, 0.0f, nsvg__actualLength(p))); + } + + for (i = 0; i < count; i++) + sum += strokeDashArray[i]; + if (sum <= 1e-6f) + count = 0; + + return count; +} + +static void nsvg__parseStyle(NSVGparser* p, const char* str); + +static int nsvg__parseAttr(NSVGparser* p, const char* name, const char* value) +{ + float xform[6]; + NSVGattrib* attr = nsvg__getAttr(p); + if (!attr) return 0; + + if (strcmp(name, "style") == 0) { + nsvg__parseStyle(p, value); + } else if (strcmp(name, "display") == 0) { + if (strcmp(value, "none") == 0) + attr->visible = 0; + // Don't reset ->visible on display:inline, one display:none hides the whole subtree + + } else if (strcmp(name, "fill") == 0) { + if (strcmp(value, "none") == 0) { + attr->hasFill = 0; + } else if (strncmp(value, "url(", 4) == 0) { + attr->hasFill = 2; + nsvg__parseUrl(attr->fillGradient, value); + } else { + attr->hasFill = 1; + attr->fillColor = nsvg__parseColor(value); + } + } else if (strcmp(name, "opacity") == 0) { + attr->opacity = nsvg__parseOpacity(value); + } else if (strcmp(name, "fill-opacity") == 0) { + attr->fillOpacity = nsvg__parseOpacity(value); + } else if (strcmp(name, "stroke") == 0) { + if (strcmp(value, "none") == 0) { + attr->hasStroke = 0; + } else if (strncmp(value, "url(", 4) == 0) { + attr->hasStroke = 2; + nsvg__parseUrl(attr->strokeGradient, value); + } else { + attr->hasStroke = 1; + attr->strokeColor = nsvg__parseColor(value); + } + } else if (strcmp(name, "stroke-width") == 0) { + attr->strokeWidth = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); + } else if (strcmp(name, "stroke-dasharray") == 0) { + attr->strokeDashCount = nsvg__parseStrokeDashArray(p, value, attr->strokeDashArray); + } else if (strcmp(name, "stroke-dashoffset") == 0) { + attr->strokeDashOffset = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); + } else if (strcmp(name, "stroke-opacity") == 0) { + attr->strokeOpacity = nsvg__parseOpacity(value); + } else if (strcmp(name, "stroke-linecap") == 0) { + attr->strokeLineCap = nsvg__parseLineCap(value); + } else if (strcmp(name, "stroke-linejoin") == 0) { + attr->strokeLineJoin = nsvg__parseLineJoin(value); + } else if (strcmp(name, "stroke-miterlimit") == 0) { + attr->miterLimit = nsvg__parseMiterLimit(value); + } else if (strcmp(name, "fill-rule") == 0) { + attr->fillRule = nsvg__parseFillRule(value); + } else if (strcmp(name, "font-size") == 0) { + attr->fontSize = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); + } else if (strcmp(name, "transform") == 0) { + nsvg__parseTransform(xform, value); + nsvg__xformPremultiply(attr->xform, xform); + } else if (strcmp(name, "stop-color") == 0) { + attr->stopColor = nsvg__parseColor(value); + } else if (strcmp(name, "stop-opacity") == 0) { + attr->stopOpacity = nsvg__parseOpacity(value); + } else if (strcmp(name, "offset") == 0) { + attr->stopOffset = nsvg__parseCoordinate(p, value, 0.0f, 1.0f); + } else if (strcmp(name, "id") == 0) { + strncpy(attr->id, value, 63); + attr->id[63] = '\0'; + } else { + return 0; + } + return 1; +} + +static int nsvg__parseNameValue(NSVGparser* p, const char* start, const char* end) +{ + const char* str; + const char* val; + char name[512]; + char value[512]; + int n; + + str = start; + while (str < end && *str != ':') ++str; + + val = str; + + // Right Trim + while (str > start && (*str == ':' || nsvg__isspace(*str))) --str; + ++str; + + n = (int)(str - start); + if (n > 511) n = 511; + if (n) memcpy(name, start, n); + name[n] = 0; + + while (val < end && (*val == ':' || nsvg__isspace(*val))) ++val; + + n = (int)(end - val); + if (n > 511) n = 511; + if (n) memcpy(value, val, n); + value[n] = 0; + + return nsvg__parseAttr(p, name, value); +} + +static void nsvg__parseStyle(NSVGparser* p, const char* str) +{ + const char* start; + const char* end; + + while (*str) { + // Left Trim + while(*str && nsvg__isspace(*str)) ++str; + start = str; + while(*str && *str != ';') ++str; + end = str; + + // Right Trim + while (end > start && (*end == ';' || nsvg__isspace(*end))) --end; + ++end; + + nsvg__parseNameValue(p, start, end); + if (*str) ++str; + } +} + +static void nsvg__parseAttribs(NSVGparser* p, const char** attr) +{ + int i; + for (i = 0; attr[i]; i += 2) + { + if (strcmp(attr[i], "style") == 0) + nsvg__parseStyle(p, attr[i + 1]); + else + nsvg__parseAttr(p, attr[i], attr[i + 1]); + } +} + +static int nsvg__getArgsPerElement(char cmd) +{ + switch (cmd) { + case 'v': + case 'V': + case 'h': + case 'H': + return 1; + case 'm': + case 'M': + case 'l': + case 'L': + case 't': + case 'T': + return 2; + case 'q': + case 'Q': + case 's': + case 'S': + return 4; + case 'c': + case 'C': + return 6; + case 'a': + case 'A': + return 7; + } + return 0; +} + +static void nsvg__pathMoveTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + if (rel) { + *cpx += args[0]; + *cpy += args[1]; + } else { + *cpx = args[0]; + *cpy = args[1]; + } + nsvg__moveTo(p, *cpx, *cpy); +} + +static void nsvg__pathLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + if (rel) { + *cpx += args[0]; + *cpy += args[1]; + } else { + *cpx = args[0]; + *cpy = args[1]; + } + nsvg__lineTo(p, *cpx, *cpy); +} + +static void nsvg__pathHLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + if (rel) + *cpx += args[0]; + else + *cpx = args[0]; + nsvg__lineTo(p, *cpx, *cpy); +} + +static void nsvg__pathVLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + if (rel) + *cpy += args[0]; + else + *cpy = args[0]; + nsvg__lineTo(p, *cpx, *cpy); +} + +static void nsvg__pathCubicBezTo(NSVGparser* p, float* cpx, float* cpy, + float* cpx2, float* cpy2, float* args, int rel) +{ + float x2, y2, cx1, cy1, cx2, cy2; + + if (rel) { + cx1 = *cpx + args[0]; + cy1 = *cpy + args[1]; + cx2 = *cpx + args[2]; + cy2 = *cpy + args[3]; + x2 = *cpx + args[4]; + y2 = *cpy + args[5]; + } else { + cx1 = args[0]; + cy1 = args[1]; + cx2 = args[2]; + cy2 = args[3]; + x2 = args[4]; + y2 = args[5]; + } + + nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); + + *cpx2 = cx2; + *cpy2 = cy2; + *cpx = x2; + *cpy = y2; +} + +static void nsvg__pathCubicBezShortTo(NSVGparser* p, float* cpx, float* cpy, + float* cpx2, float* cpy2, float* args, int rel) +{ + float x1, y1, x2, y2, cx1, cy1, cx2, cy2; + + x1 = *cpx; + y1 = *cpy; + if (rel) { + cx2 = *cpx + args[0]; + cy2 = *cpy + args[1]; + x2 = *cpx + args[2]; + y2 = *cpy + args[3]; + } else { + cx2 = args[0]; + cy2 = args[1]; + x2 = args[2]; + y2 = args[3]; + } + + cx1 = 2*x1 - *cpx2; + cy1 = 2*y1 - *cpy2; + + nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); + + *cpx2 = cx2; + *cpy2 = cy2; + *cpx = x2; + *cpy = y2; +} + +static void nsvg__pathQuadBezTo(NSVGparser* p, float* cpx, float* cpy, + float* cpx2, float* cpy2, float* args, int rel) +{ + float x1, y1, x2, y2, cx, cy; + float cx1, cy1, cx2, cy2; + + x1 = *cpx; + y1 = *cpy; + if (rel) { + cx = *cpx + args[0]; + cy = *cpy + args[1]; + x2 = *cpx + args[2]; + y2 = *cpy + args[3]; + } else { + cx = args[0]; + cy = args[1]; + x2 = args[2]; + y2 = args[3]; + } + + // Convert to cubic bezier + cx1 = x1 + 2.0f/3.0f*(cx - x1); + cy1 = y1 + 2.0f/3.0f*(cy - y1); + cx2 = x2 + 2.0f/3.0f*(cx - x2); + cy2 = y2 + 2.0f/3.0f*(cy - y2); + + nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); + + *cpx2 = cx; + *cpy2 = cy; + *cpx = x2; + *cpy = y2; +} + +static void nsvg__pathQuadBezShortTo(NSVGparser* p, float* cpx, float* cpy, + float* cpx2, float* cpy2, float* args, int rel) +{ + float x1, y1, x2, y2, cx, cy; + float cx1, cy1, cx2, cy2; + + x1 = *cpx; + y1 = *cpy; + if (rel) { + x2 = *cpx + args[0]; + y2 = *cpy + args[1]; + } else { + x2 = args[0]; + y2 = args[1]; + } + + cx = 2*x1 - *cpx2; + cy = 2*y1 - *cpy2; + + // Convert to cubix bezier + cx1 = x1 + 2.0f/3.0f*(cx - x1); + cy1 = y1 + 2.0f/3.0f*(cy - y1); + cx2 = x2 + 2.0f/3.0f*(cx - x2); + cy2 = y2 + 2.0f/3.0f*(cy - y2); + + nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); + + *cpx2 = cx; + *cpy2 = cy; + *cpx = x2; + *cpy = y2; +} + +static float nsvg__sqr(float x) { return x*x; } +static float nsvg__vmag(float x, float y) { return sqrtf(x*x + y*y); } + +static float nsvg__vecrat(float ux, float uy, float vx, float vy) +{ + return (ux*vx + uy*vy) / (nsvg__vmag(ux,uy) * nsvg__vmag(vx,vy)); +} + +static float nsvg__vecang(float ux, float uy, float vx, float vy) +{ + float r = nsvg__vecrat(ux,uy, vx,vy); + if (r < -1.0f) r = -1.0f; + if (r > 1.0f) r = 1.0f; + return ((ux*vy < uy*vx) ? -1.0f : 1.0f) * acosf(r); +} + +static void nsvg__pathArcTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) +{ + // Ported from canvg (https://code.google.com/p/canvg/) + float rx, ry, rotx; + float x1, y1, x2, y2, cx, cy, dx, dy, d; + float x1p, y1p, cxp, cyp, s, sa, sb; + float ux, uy, vx, vy, a1, da; + float x, y, tanx, tany, a, px = 0, py = 0, ptanx = 0, ptany = 0, t[6]; + float sinrx, cosrx; + int fa, fs; + int i, ndivs; + float hda, kappa; + + rx = fabsf(args[0]); // y radius + ry = fabsf(args[1]); // x radius + rotx = args[2] / 180.0f * NSVG_PI; // x rotation angle + fa = fabsf(args[3]) > 1e-6 ? 1 : 0; // Large arc + fs = fabsf(args[4]) > 1e-6 ? 1 : 0; // Sweep direction + x1 = *cpx; // start point + y1 = *cpy; + if (rel) { // end point + x2 = *cpx + args[5]; + y2 = *cpy + args[6]; + } else { + x2 = args[5]; + y2 = args[6]; + } + + dx = x1 - x2; + dy = y1 - y2; + d = sqrtf(dx*dx + dy*dy); + if (d < 1e-6f || rx < 1e-6f || ry < 1e-6f) { + // The arc degenerates to a line + nsvg__lineTo(p, x2, y2); + *cpx = x2; + *cpy = y2; + return; + } + + sinrx = sinf(rotx); + cosrx = cosf(rotx); + + // Convert to center point parameterization. + // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes + // 1) Compute x1', y1' + x1p = cosrx * dx / 2.0f + sinrx * dy / 2.0f; + y1p = -sinrx * dx / 2.0f + cosrx * dy / 2.0f; + d = nsvg__sqr(x1p)/nsvg__sqr(rx) + nsvg__sqr(y1p)/nsvg__sqr(ry); + if (d > 1) { + d = sqrtf(d); + rx *= d; + ry *= d; + } + // 2) Compute cx', cy' + s = 0.0f; + sa = nsvg__sqr(rx)*nsvg__sqr(ry) - nsvg__sqr(rx)*nsvg__sqr(y1p) - nsvg__sqr(ry)*nsvg__sqr(x1p); + sb = nsvg__sqr(rx)*nsvg__sqr(y1p) + nsvg__sqr(ry)*nsvg__sqr(x1p); + if (sa < 0.0f) sa = 0.0f; + if (sb > 0.0f) + s = sqrtf(sa / sb); + if (fa == fs) + s = -s; + cxp = s * rx * y1p / ry; + cyp = s * -ry * x1p / rx; + + // 3) Compute cx,cy from cx',cy' + cx = (x1 + x2)/2.0f + cosrx*cxp - sinrx*cyp; + cy = (y1 + y2)/2.0f + sinrx*cxp + cosrx*cyp; + + // 4) Calculate theta1, and delta theta. + ux = (x1p - cxp) / rx; + uy = (y1p - cyp) / ry; + vx = (-x1p - cxp) / rx; + vy = (-y1p - cyp) / ry; + a1 = nsvg__vecang(1.0f,0.0f, ux,uy); // Initial angle + da = nsvg__vecang(ux,uy, vx,vy); // Delta angle + +// if (vecrat(ux,uy,vx,vy) <= -1.0f) da = NSVG_PI; +// if (vecrat(ux,uy,vx,vy) >= 1.0f) da = 0; + + if (fs == 0 && da > 0) + da -= 2 * NSVG_PI; + else if (fs == 1 && da < 0) + da += 2 * NSVG_PI; + + // Approximate the arc using cubic spline segments. + t[0] = cosrx; t[1] = sinrx; + t[2] = -sinrx; t[3] = cosrx; + t[4] = cx; t[5] = cy; + + // Split arc into max 90 degree segments. + // The loop assumes an iteration per end point (including start and end), this +1. + ndivs = (int)(fabsf(da) / (NSVG_PI*0.5f) + 1.0f); + hda = (da / (float)ndivs) / 2.0f; + kappa = fabsf(4.0f / 3.0f * (1.0f - cosf(hda)) / sinf(hda)); + if (da < 0.0f) + kappa = -kappa; + + for (i = 0; i <= ndivs; i++) { + a = a1 + da * ((float)i/(float)ndivs); + dx = cosf(a); + dy = sinf(a); + nsvg__xformPoint(&x, &y, dx*rx, dy*ry, t); // position + nsvg__xformVec(&tanx, &tany, -dy*rx * kappa, dx*ry * kappa, t); // tangent + if (i > 0) + nsvg__cubicBezTo(p, px+ptanx,py+ptany, x-tanx, y-tany, x, y); + px = x; + py = y; + ptanx = tanx; + ptany = tany; + } + + *cpx = x2; + *cpy = y2; +} + +static void nsvg__parsePath(NSVGparser* p, const char** attr) +{ + const char* s = NULL; + char cmd = '\0'; + float args[10]; + int nargs; + int rargs = 0; + float cpx, cpy, cpx2, cpy2; + const char* tmp[4]; + char closedFlag; + int i; + char item[64]; + + for (i = 0; attr[i]; i += 2) { + if (strcmp(attr[i], "d") == 0) { + s = attr[i + 1]; + } else { + tmp[0] = attr[i]; + tmp[1] = attr[i + 1]; + tmp[2] = 0; + tmp[3] = 0; + nsvg__parseAttribs(p, tmp); + } + } + + if (s) { + nsvg__resetPath(p); + cpx = 0; cpy = 0; + cpx2 = 0; cpy2 = 0; + closedFlag = 0; + nargs = 0; + + while (*s) { + s = nsvg__getNextPathItem(s, item); + if (!*item) break; + if (nsvg__isnum(item[0])) { + if (nargs < 10) + args[nargs++] = (float)nsvg__atof(item); + if (nargs >= rargs) { + switch (cmd) { + case 'm': + case 'M': + nsvg__pathMoveTo(p, &cpx, &cpy, args, cmd == 'm' ? 1 : 0); + // Moveto can be followed by multiple coordinate pairs, + // which should be treated as linetos. + cmd = (cmd == 'm') ? 'l' : 'L'; + rargs = nsvg__getArgsPerElement(cmd); + cpx2 = cpx; cpy2 = cpy; + break; + case 'l': + case 'L': + nsvg__pathLineTo(p, &cpx, &cpy, args, cmd == 'l' ? 1 : 0); + cpx2 = cpx; cpy2 = cpy; + break; + case 'H': + case 'h': + nsvg__pathHLineTo(p, &cpx, &cpy, args, cmd == 'h' ? 1 : 0); + cpx2 = cpx; cpy2 = cpy; + break; + case 'V': + case 'v': + nsvg__pathVLineTo(p, &cpx, &cpy, args, cmd == 'v' ? 1 : 0); + cpx2 = cpx; cpy2 = cpy; + break; + case 'C': + case 'c': + nsvg__pathCubicBezTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 'c' ? 1 : 0); + break; + case 'S': + case 's': + nsvg__pathCubicBezShortTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 's' ? 1 : 0); + break; + case 'Q': + case 'q': + nsvg__pathQuadBezTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 'q' ? 1 : 0); + break; + case 'T': + case 't': + nsvg__pathQuadBezShortTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 't' ? 1 : 0); + break; + case 'A': + case 'a': + nsvg__pathArcTo(p, &cpx, &cpy, args, cmd == 'a' ? 1 : 0); + cpx2 = cpx; cpy2 = cpy; + break; + default: + if (nargs >= 2) { + cpx = args[nargs-2]; + cpy = args[nargs-1]; + cpx2 = cpx; cpy2 = cpy; + } + break; + } + nargs = 0; + } + } else { + cmd = item[0]; + rargs = nsvg__getArgsPerElement(cmd); + if (cmd == 'M' || cmd == 'm') { + // Commit path. + if (p->npts > 0) + nsvg__addPath(p, closedFlag); + // Start new subpath. + nsvg__resetPath(p); + closedFlag = 0; + nargs = 0; + } else if (cmd == 'Z' || cmd == 'z') { + closedFlag = 1; + // Commit path. + if (p->npts > 0) { + // Move current point to first point + cpx = p->pts[0]; + cpy = p->pts[1]; + cpx2 = cpx; cpy2 = cpy; + nsvg__addPath(p, closedFlag); + } + // Start new subpath. + nsvg__resetPath(p); + nsvg__moveTo(p, cpx, cpy); + closedFlag = 0; + nargs = 0; + } + } + } + // Commit path. + if (p->npts) + nsvg__addPath(p, closedFlag); + } + + nsvg__addShape(p); +} + +static void nsvg__parseRect(NSVGparser* p, const char** attr) +{ + float x = 0.0f; + float y = 0.0f; + float w = 0.0f; + float h = 0.0f; + float rx = -1.0f; // marks not set + float ry = -1.0f; + int i; + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "x") == 0) x = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "y") == 0) y = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + if (strcmp(attr[i], "width") == 0) w = nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p)); + if (strcmp(attr[i], "height") == 0) h = nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p)); + if (strcmp(attr[i], "rx") == 0) rx = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p))); + if (strcmp(attr[i], "ry") == 0) ry = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p))); + } + } + + if (rx < 0.0f && ry > 0.0f) rx = ry; + if (ry < 0.0f && rx > 0.0f) ry = rx; + if (rx < 0.0f) rx = 0.0f; + if (ry < 0.0f) ry = 0.0f; + if (rx > w/2.0f) rx = w/2.0f; + if (ry > h/2.0f) ry = h/2.0f; + + if (w != 0.0f && h != 0.0f) { + nsvg__resetPath(p); + + if (rx < 0.00001f || ry < 0.0001f) { + nsvg__moveTo(p, x, y); + nsvg__lineTo(p, x+w, y); + nsvg__lineTo(p, x+w, y+h); + nsvg__lineTo(p, x, y+h); + } else { + // Rounded rectangle + nsvg__moveTo(p, x+rx, y); + nsvg__lineTo(p, x+w-rx, y); + nsvg__cubicBezTo(p, x+w-rx*(1-NSVG_KAPPA90), y, x+w, y+ry*(1-NSVG_KAPPA90), x+w, y+ry); + nsvg__lineTo(p, x+w, y+h-ry); + nsvg__cubicBezTo(p, x+w, y+h-ry*(1-NSVG_KAPPA90), x+w-rx*(1-NSVG_KAPPA90), y+h, x+w-rx, y+h); + nsvg__lineTo(p, x+rx, y+h); + nsvg__cubicBezTo(p, x+rx*(1-NSVG_KAPPA90), y+h, x, y+h-ry*(1-NSVG_KAPPA90), x, y+h-ry); + nsvg__lineTo(p, x, y+ry); + nsvg__cubicBezTo(p, x, y+ry*(1-NSVG_KAPPA90), x+rx*(1-NSVG_KAPPA90), y, x+rx, y); + } + + nsvg__addPath(p, 1); + + nsvg__addShape(p); + } +} + +static void nsvg__parseCircle(NSVGparser* p, const char** attr) +{ + float cx = 0.0f; + float cy = 0.0f; + float r = 0.0f; + int i; + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "cx") == 0) cx = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "cy") == 0) cy = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + if (strcmp(attr[i], "r") == 0) r = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualLength(p))); + } + } + + if (r > 0.0f) { + nsvg__resetPath(p); + + nsvg__moveTo(p, cx+r, cy); + nsvg__cubicBezTo(p, cx+r, cy+r*NSVG_KAPPA90, cx+r*NSVG_KAPPA90, cy+r, cx, cy+r); + nsvg__cubicBezTo(p, cx-r*NSVG_KAPPA90, cy+r, cx-r, cy+r*NSVG_KAPPA90, cx-r, cy); + nsvg__cubicBezTo(p, cx-r, cy-r*NSVG_KAPPA90, cx-r*NSVG_KAPPA90, cy-r, cx, cy-r); + nsvg__cubicBezTo(p, cx+r*NSVG_KAPPA90, cy-r, cx+r, cy-r*NSVG_KAPPA90, cx+r, cy); + + nsvg__addPath(p, 1); + + nsvg__addShape(p); + } +} + +static void nsvg__parseEllipse(NSVGparser* p, const char** attr) +{ + float cx = 0.0f; + float cy = 0.0f; + float rx = 0.0f; + float ry = 0.0f; + int i; + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "cx") == 0) cx = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "cy") == 0) cy = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + if (strcmp(attr[i], "rx") == 0) rx = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p))); + if (strcmp(attr[i], "ry") == 0) ry = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p))); + } + } + + if (rx > 0.0f && ry > 0.0f) { + + nsvg__resetPath(p); + + nsvg__moveTo(p, cx+rx, cy); + nsvg__cubicBezTo(p, cx+rx, cy+ry*NSVG_KAPPA90, cx+rx*NSVG_KAPPA90, cy+ry, cx, cy+ry); + nsvg__cubicBezTo(p, cx-rx*NSVG_KAPPA90, cy+ry, cx-rx, cy+ry*NSVG_KAPPA90, cx-rx, cy); + nsvg__cubicBezTo(p, cx-rx, cy-ry*NSVG_KAPPA90, cx-rx*NSVG_KAPPA90, cy-ry, cx, cy-ry); + nsvg__cubicBezTo(p, cx+rx*NSVG_KAPPA90, cy-ry, cx+rx, cy-ry*NSVG_KAPPA90, cx+rx, cy); + + nsvg__addPath(p, 1); + + nsvg__addShape(p); + } +} + +static void nsvg__parseLine(NSVGparser* p, const char** attr) +{ + float x1 = 0.0; + float y1 = 0.0; + float x2 = 0.0; + float y2 = 0.0; + int i; + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "x1") == 0) x1 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "y1") == 0) y1 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + if (strcmp(attr[i], "x2") == 0) x2 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); + if (strcmp(attr[i], "y2") == 0) y2 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); + } + } + + nsvg__resetPath(p); + + nsvg__moveTo(p, x1, y1); + nsvg__lineTo(p, x2, y2); + + nsvg__addPath(p, 0); + + nsvg__addShape(p); +} + +static void nsvg__parsePoly(NSVGparser* p, const char** attr, int closeFlag) +{ + int i; + const char* s; + float args[2]; + int nargs, npts = 0; + char item[64]; + + nsvg__resetPath(p); + + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "points") == 0) { + s = attr[i + 1]; + nargs = 0; + while (*s) { + s = nsvg__getNextPathItem(s, item); + args[nargs++] = (float)nsvg__atof(item); + if (nargs >= 2) { + if (npts == 0) + nsvg__moveTo(p, args[0], args[1]); + else + nsvg__lineTo(p, args[0], args[1]); + nargs = 0; + npts++; + } + } + } + } + } + + nsvg__addPath(p, (char)closeFlag); + + nsvg__addShape(p); +} + +static void nsvg__parseSVG(NSVGparser* p, const char** attr) +{ + int i; + for (i = 0; attr[i]; i += 2) { + if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "width") == 0) { + p->image->width = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 0.0f); + } else if (strcmp(attr[i], "height") == 0) { + p->image->height = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 0.0f); + } else if (strcmp(attr[i], "viewBox") == 0) { + const char *s = attr[i + 1]; + char buf[64]; + s = nsvg__parseNumber(s, buf, 64); + p->viewMinx = nsvg__atof(buf); + while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; + if (!*s) return; + s = nsvg__parseNumber(s, buf, 64); + p->viewMiny = nsvg__atof(buf); + while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; + if (!*s) return; + s = nsvg__parseNumber(s, buf, 64); + p->viewWidth = nsvg__atof(buf); + while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; + if (!*s) return; + s = nsvg__parseNumber(s, buf, 64); + p->viewHeight = nsvg__atof(buf); + } else if (strcmp(attr[i], "preserveAspectRatio") == 0) { + if (strstr(attr[i + 1], "none") != 0) { + // No uniform scaling + p->alignType = NSVG_ALIGN_NONE; + } else { + // Parse X align + if (strstr(attr[i + 1], "xMin") != 0) + p->alignX = NSVG_ALIGN_MIN; + else if (strstr(attr[i + 1], "xMid") != 0) + p->alignX = NSVG_ALIGN_MID; + else if (strstr(attr[i + 1], "xMax") != 0) + p->alignX = NSVG_ALIGN_MAX; + // Parse X align + if (strstr(attr[i + 1], "yMin") != 0) + p->alignY = NSVG_ALIGN_MIN; + else if (strstr(attr[i + 1], "yMid") != 0) + p->alignY = NSVG_ALIGN_MID; + else if (strstr(attr[i + 1], "yMax") != 0) + p->alignY = NSVG_ALIGN_MAX; + // Parse meet/slice + p->alignType = NSVG_ALIGN_MEET; + if (strstr(attr[i + 1], "slice") != 0) + p->alignType = NSVG_ALIGN_SLICE; + } + } + } + } +} + +static void nsvg__parseGradient(NSVGparser* p, const char** attr, char type) +{ + int i; + NSVGgradientData* grad = (NSVGgradientData*)malloc(sizeof(NSVGgradientData)); + if (grad == NULL) return; + memset(grad, 0, sizeof(NSVGgradientData)); + grad->units = NSVG_OBJECT_SPACE; + grad->type = type; + if (grad->type == NSVG_PAINT_LINEAR_GRADIENT) { + grad->linear.x1 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); + grad->linear.y1 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); + grad->linear.x2 = nsvg__coord(100.0f, NSVG_UNITS_PERCENT); + grad->linear.y2 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); + } else if (grad->type == NSVG_PAINT_RADIAL_GRADIENT) { + grad->radial.cx = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); + grad->radial.cy = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); + grad->radial.r = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); + } + + nsvg__xformIdentity(grad->xform); + + for (i = 0; attr[i]; i += 2) { + if (strcmp(attr[i], "id") == 0) { + strncpy(grad->id, attr[i+1], 63); + grad->id[63] = '\0'; + } else if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { + if (strcmp(attr[i], "gradientUnits") == 0) { + if (strcmp(attr[i+1], "objectBoundingBox") == 0) + grad->units = NSVG_OBJECT_SPACE; + else + grad->units = NSVG_USER_SPACE; + } else if (strcmp(attr[i], "gradientTransform") == 0) { + nsvg__parseTransform(grad->xform, attr[i + 1]); + } else if (strcmp(attr[i], "cx") == 0) { + grad->radial.cx = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "cy") == 0) { + grad->radial.cy = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "r") == 0) { + grad->radial.r = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "fx") == 0) { + grad->radial.fx = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "fy") == 0) { + grad->radial.fy = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "x1") == 0) { + grad->linear.x1 = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "y1") == 0) { + grad->linear.y1 = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "x2") == 0) { + grad->linear.x2 = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "y2") == 0) { + grad->linear.y2 = nsvg__parseCoordinateRaw(attr[i + 1]); + } else if (strcmp(attr[i], "spreadMethod") == 0) { + if (strcmp(attr[i+1], "pad") == 0) + grad->spread = NSVG_SPREAD_PAD; + else if (strcmp(attr[i+1], "reflect") == 0) + grad->spread = NSVG_SPREAD_REFLECT; + else if (strcmp(attr[i+1], "repeat") == 0) + grad->spread = NSVG_SPREAD_REPEAT; + } else if (strcmp(attr[i], "xlink:href") == 0) { + const char *href = attr[i+1]; + strncpy(grad->ref, href+1, 62); + grad->ref[62] = '\0'; + } + } + } + + grad->next = p->gradients; + p->gradients = grad; +} + +static void nsvg__parseGradientStop(NSVGparser* p, const char** attr) +{ + NSVGattrib* curAttr = nsvg__getAttr(p); + NSVGgradientData* grad; + NSVGgradientStop* stop; + int i, idx; + + curAttr->stopOffset = 0; + curAttr->stopColor = 0; + curAttr->stopOpacity = 1.0f; + + for (i = 0; attr[i]; i += 2) { + nsvg__parseAttr(p, attr[i], attr[i + 1]); + } + + // Add stop to the last gradient. + grad = p->gradients; + if (grad == NULL) return; + + grad->nstops++; + grad->stops = (NSVGgradientStop*)realloc(grad->stops, sizeof(NSVGgradientStop)*grad->nstops); + if (grad->stops == NULL) return; + + // Insert + idx = grad->nstops-1; + for (i = 0; i < grad->nstops-1; i++) { + if (curAttr->stopOffset < grad->stops[i].offset) { + idx = i; + break; + } + } + if (idx != grad->nstops-1) { + for (i = grad->nstops-1; i > idx; i--) + grad->stops[i] = grad->stops[i-1]; + } + + stop = &grad->stops[idx]; + stop->color = curAttr->stopColor; + stop->color |= (unsigned int)(curAttr->stopOpacity*255) << 24; + stop->offset = curAttr->stopOffset; +} + +static void nsvg__startElement(void* ud, const char* el, const char** attr) +{ + NSVGparser* p = (NSVGparser*)ud; + + if (p->defsFlag) { + // Skip everything but gradients in defs + if (strcmp(el, "linearGradient") == 0) { + nsvg__parseGradient(p, attr, NSVG_PAINT_LINEAR_GRADIENT); + } else if (strcmp(el, "radialGradient") == 0) { + nsvg__parseGradient(p, attr, NSVG_PAINT_RADIAL_GRADIENT); + } else if (strcmp(el, "stop") == 0) { + nsvg__parseGradientStop(p, attr); + } + return; + } + + if (strcmp(el, "g") == 0) { + nsvg__pushAttr(p); + nsvg__parseAttribs(p, attr); + } else if (strcmp(el, "path") == 0) { + if (p->pathFlag) // Do not allow nested paths. + return; + nsvg__pushAttr(p); + nsvg__parseAttribs(p, attr); + nsvg__parsePath(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "rect") == 0) { + nsvg__pushAttr(p); + nsvg__parseAttribs(p, attr); + nsvg__parseRect(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "circle") == 0) { + nsvg__pushAttr(p); + nsvg__parseAttribs(p, attr); + nsvg__parseCircle(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "ellipse") == 0) { + nsvg__pushAttr(p); + nsvg__parseAttribs(p, attr); + nsvg__parseEllipse(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "line") == 0) { + nsvg__pushAttr(p); + nsvg__parseAttribs(p, attr); + nsvg__parseLine(p, attr); + nsvg__popAttr(p); + } else if (strcmp(el, "polyline") == 0) { + nsvg__pushAttr(p); + nsvg__parseAttribs(p, attr); + nsvg__parsePoly(p, attr, 0); + nsvg__popAttr(p); + } else if (strcmp(el, "polygon") == 0) { + nsvg__pushAttr(p); + nsvg__parseAttribs(p, attr); + nsvg__parsePoly(p, attr, 1); + nsvg__popAttr(p); + } else if (strcmp(el, "linearGradient") == 0) { + nsvg__parseGradient(p, attr, NSVG_PAINT_LINEAR_GRADIENT); + } else if (strcmp(el, "radialGradient") == 0) { + nsvg__parseGradient(p, attr, NSVG_PAINT_RADIAL_GRADIENT); + } else if (strcmp(el, "stop") == 0) { + nsvg__parseGradientStop(p, attr); + } else if (strcmp(el, "defs") == 0) { + p->defsFlag = 1; + } else if (strcmp(el, "svg") == 0) { + nsvg__parseSVG(p, attr); + } +} + +static void nsvg__endElement(void* ud, const char* el) +{ + NSVGparser* p = (NSVGparser*)ud; + + if (strcmp(el, "g") == 0) { + nsvg__popAttr(p); + } else if (strcmp(el, "path") == 0) { + p->pathFlag = 0; + } else if (strcmp(el, "defs") == 0) { + p->defsFlag = 0; + } +} + +static void nsvg__content(void* ud, const char* s) +{ + NSVG_NOTUSED(ud); + NSVG_NOTUSED(s); + // empty +} + +static void nsvg__imageBounds(NSVGparser* p, float* bounds) +{ + NSVGshape* shape; + shape = p->image->shapes; + if (shape == NULL) { + bounds[0] = bounds[1] = bounds[2] = bounds[3] = 0.0; + return; + } + bounds[0] = shape->bounds[0]; + bounds[1] = shape->bounds[1]; + bounds[2] = shape->bounds[2]; + bounds[3] = shape->bounds[3]; + for (shape = shape->next; shape != NULL; shape = shape->next) { + bounds[0] = nsvg__minf(bounds[0], shape->bounds[0]); + bounds[1] = nsvg__minf(bounds[1], shape->bounds[1]); + bounds[2] = nsvg__maxf(bounds[2], shape->bounds[2]); + bounds[3] = nsvg__maxf(bounds[3], shape->bounds[3]); + } +} + +static float nsvg__viewAlign(float content, float container, int type) +{ + if (type == NSVG_ALIGN_MIN) + return 0; + else if (type == NSVG_ALIGN_MAX) + return container - content; + // mid + return (container - content) * 0.5f; +} + +static void nsvg__scaleGradient(NSVGgradient* grad, float tx, float ty, float sx, float sy) +{ + float t[6]; + nsvg__xformSetTranslation(t, tx, ty); + nsvg__xformMultiply (grad->xform, t); + + nsvg__xformSetScale(t, sx, sy); + nsvg__xformMultiply (grad->xform, t); +} + +static void nsvg__scaleToViewbox(NSVGparser* p, const char* units) +{ + NSVGshape* shape; + NSVGpath* path; + float tx, ty, sx, sy, us, bounds[4], t[6], avgs; + int i; + float* pt; + + // Guess image size if not set completely. + nsvg__imageBounds(p, bounds); + + if (p->viewWidth == 0) { + if (p->image->width > 0) { + p->viewWidth = p->image->width; + } else { + p->viewMinx = bounds[0]; + p->viewWidth = bounds[2] - bounds[0]; + } + } + if (p->viewHeight == 0) { + if (p->image->height > 0) { + p->viewHeight = p->image->height; + } else { + p->viewMiny = bounds[1]; + p->viewHeight = bounds[3] - bounds[1]; + } + } + if (p->image->width == 0) + p->image->width = p->viewWidth; + if (p->image->height == 0) + p->image->height = p->viewHeight; + + tx = -p->viewMinx; + ty = -p->viewMiny; + sx = p->viewWidth > 0 ? p->image->width / p->viewWidth : 0; + sy = p->viewHeight > 0 ? p->image->height / p->viewHeight : 0; + // Unit scaling + us = 1.0f / nsvg__convertToPixels(p, nsvg__coord(1.0f, nsvg__parseUnits(units)), 0.0f, 1.0f); + + // Fix aspect ratio + if (p->alignType == NSVG_ALIGN_MEET) { + // fit whole image into viewbox + sx = sy = nsvg__minf(sx, sy); + tx += nsvg__viewAlign(p->viewWidth*sx, p->image->width, p->alignX) / sx; + ty += nsvg__viewAlign(p->viewHeight*sy, p->image->height, p->alignY) / sy; + } else if (p->alignType == NSVG_ALIGN_SLICE) { + // fill whole viewbox with image + sx = sy = nsvg__maxf(sx, sy); + tx += nsvg__viewAlign(p->viewWidth*sx, p->image->width, p->alignX) / sx; + ty += nsvg__viewAlign(p->viewHeight*sy, p->image->height, p->alignY) / sy; + } + + // Transform + sx *= us; + sy *= us; + avgs = (sx+sy) / 2.0f; + for (shape = p->image->shapes; shape != NULL; shape = shape->next) { + shape->bounds[0] = (shape->bounds[0] + tx) * sx; + shape->bounds[1] = (shape->bounds[1] + ty) * sy; + shape->bounds[2] = (shape->bounds[2] + tx) * sx; + shape->bounds[3] = (shape->bounds[3] + ty) * sy; + for (path = shape->paths; path != NULL; path = path->next) { + path->bounds[0] = (path->bounds[0] + tx) * sx; + path->bounds[1] = (path->bounds[1] + ty) * sy; + path->bounds[2] = (path->bounds[2] + tx) * sx; + path->bounds[3] = (path->bounds[3] + ty) * sy; + for (i =0; i < path->npts; i++) { + pt = &path->pts[i*2]; + pt[0] = (pt[0] + tx) * sx; + pt[1] = (pt[1] + ty) * sy; + } + } + + if (shape->fill.type == NSVG_PAINT_LINEAR_GRADIENT || shape->fill.type == NSVG_PAINT_RADIAL_GRADIENT) { + nsvg__scaleGradient(shape->fill.gradient, tx,ty, sx,sy); + memcpy(t, shape->fill.gradient->xform, sizeof(float)*6); + nsvg__xformInverse(shape->fill.gradient->xform, t); + } + if (shape->stroke.type == NSVG_PAINT_LINEAR_GRADIENT || shape->stroke.type == NSVG_PAINT_RADIAL_GRADIENT) { + nsvg__scaleGradient(shape->stroke.gradient, tx,ty, sx,sy); + memcpy(t, shape->stroke.gradient->xform, sizeof(float)*6); + nsvg__xformInverse(shape->stroke.gradient->xform, t); + } + + shape->strokeWidth *= avgs; + shape->strokeDashOffset *= avgs; + for (i = 0; i < shape->strokeDashCount; i++) + shape->strokeDashArray[i] *= avgs; + } +} + +NSVGimage* nsvgParse(char* input, const char* units, float dpi) +{ + NSVGparser* p; + NSVGimage* ret = 0; + + p = nsvg__createParser(); + if (p == NULL) { + return NULL; + } + p->dpi = dpi; + + nsvg__parseXML(input, nsvg__startElement, nsvg__endElement, nsvg__content, p); + + // Scale to viewBox + nsvg__scaleToViewbox(p, units); + + ret = p->image; + p->image = NULL; + + nsvg__deleteParser(p); + + return ret; +} + +NSVGimage* nsvgParseFromFile(const char* filename, const char* units, float dpi) +{ + FILE* fp = NULL; + size_t size; + char* data = NULL; + NSVGimage* image = NULL; + + fp = fopen(filename, "rb"); + if (!fp) goto error; + fseek(fp, 0, SEEK_END); + size = ftell(fp); + fseek(fp, 0, SEEK_SET); + data = (char*)malloc(size+1); + if (data == NULL) goto error; + if (fread(data, 1, size, fp) != size) goto error; + data[size] = '\0'; // Must be null terminated. + fclose(fp); + image = nsvgParse(data, units, dpi); + free(data); + + return image; + +error: + if (fp) fclose(fp); + if (data) free(data); + if (image) nsvgDelete(image); + return NULL; +} + +NSVGpath* nsvgDuplicatePath(NSVGpath* p) +{ + NSVGpath* res = NULL; + + if (p == NULL) + return NULL; + + res = (NSVGpath*)malloc(sizeof(NSVGpath)); + if (res == NULL) goto error; + memset(res, 0, sizeof(NSVGpath)); + + res->pts = (float*)malloc(p->npts*2*sizeof(float)); + if (res->pts == NULL) goto error; + memcpy(res->pts, p->pts, p->npts * sizeof(float) * 2); + res->npts = p->npts; + + memcpy(res->bounds, p->bounds, sizeof(p->bounds)); + + res->closed = p->closed; + + return res; + +error: + if (res != NULL) { + free(res->pts); + free(res); + } + return NULL; +} + +void nsvgDelete(NSVGimage* image) +{ + NSVGshape *snext, *shape; + if (image == NULL) return; + shape = image->shapes; + while (shape != NULL) { + snext = shape->next; + nsvg__deletePaths(shape->paths); + nsvg__deletePaint(&shape->fill); + nsvg__deletePaint(&shape->stroke); + free(shape); + shape = snext; + } + free(image); +} + +#endif diff --git a/submodules/SyncCore/Sources/AutodownloadSettings.swift b/submodules/SyncCore/Sources/AutodownloadSettings.swift index 3724217966..a8f2af62e2 100644 --- a/submodules/SyncCore/Sources/AutodownloadSettings.swift +++ b/submodules/SyncCore/Sources/AutodownloadSettings.swift @@ -13,14 +13,16 @@ public struct AutodownloadPresetSettings: PostboxCoding, Equatable { public let fileSizeMax: Int32 public let preloadLargeVideo: Bool public let lessDataForPhoneCalls: Bool + public let videoUploadMaxbitrate: Int32 - public init(disabled: Bool, photoSizeMax: Int32, videoSizeMax: Int32, fileSizeMax: Int32, preloadLargeVideo: Bool, lessDataForPhoneCalls: Bool) { + public init(disabled: Bool, photoSizeMax: Int32, videoSizeMax: Int32, fileSizeMax: Int32, preloadLargeVideo: Bool, lessDataForPhoneCalls: Bool, videoUploadMaxbitrate: Int32) { self.disabled = disabled self.photoSizeMax = photoSizeMax self.videoSizeMax = videoSizeMax self.fileSizeMax = fileSizeMax self.preloadLargeVideo = preloadLargeVideo self.lessDataForPhoneCalls = lessDataForPhoneCalls + self.videoUploadMaxbitrate = videoUploadMaxbitrate } public init(decoder: PostboxDecoder) { @@ -30,6 +32,7 @@ public struct AutodownloadPresetSettings: PostboxCoding, Equatable { self.fileSizeMax = decoder.decodeInt32ForKey("fileSizeMax", orElse: 0) self.preloadLargeVideo = decoder.decodeInt32ForKey("preloadLargeVideo", orElse: 0) != 0 self.lessDataForPhoneCalls = decoder.decodeInt32ForKey("lessDataForPhoneCalls", orElse: 0) != 0 + self.videoUploadMaxbitrate = decoder.decodeInt32ForKey("videoUploadMaxbitrate", orElse: 0) } public func encode(_ encoder: PostboxEncoder) { @@ -39,6 +42,7 @@ public struct AutodownloadPresetSettings: PostboxCoding, Equatable { encoder.encodeInt32(self.fileSizeMax, forKey: "fileSizeMax") encoder.encodeInt32(self.preloadLargeVideo ? 1 : 0, forKey: "preloadLargeVideo") encoder.encodeInt32(self.lessDataForPhoneCalls ? 1 : 0, forKey: "lessDataForPhoneCalls") + encoder.encodeInt32(self.videoUploadMaxbitrate, forKey: "videoUploadMaxbitrate") } } @@ -48,9 +52,10 @@ public struct AutodownloadSettings: PreferencesEntry, Equatable { public let highPreset: AutodownloadPresetSettings public static var defaultSettings: AutodownloadSettings { - return AutodownloadSettings(lowPreset: AutodownloadPresetSettings(disabled: false, photoSizeMax: 1 * 1024 * 1024, videoSizeMax: 0, fileSizeMax: 0, preloadLargeVideo: false, lessDataForPhoneCalls: true), - mediumPreset: AutodownloadPresetSettings(disabled: false, photoSizeMax: 1 * 1024 * 1024, videoSizeMax: Int32(2.5 * 1024 * 1024), fileSizeMax: 1 * 1024 * 1024, preloadLargeVideo: false, lessDataForPhoneCalls: false), - highPreset: AutodownloadPresetSettings(disabled: false, photoSizeMax: 1 * 1024 * 1024, videoSizeMax: 10 * 1024 * 1024, fileSizeMax: 3 * 1024 * 1024, preloadLargeVideo: false, lessDataForPhoneCalls: false)) + return AutodownloadSettings( + lowPreset: AutodownloadPresetSettings(disabled: false, photoSizeMax: 1 * 1024 * 1024, videoSizeMax: 0, fileSizeMax: 0, preloadLargeVideo: false, lessDataForPhoneCalls: true, videoUploadMaxbitrate: 0), + mediumPreset: AutodownloadPresetSettings(disabled: false, photoSizeMax: 1 * 1024 * 1024, videoSizeMax: Int32(2.5 * 1024 * 1024), fileSizeMax: 1 * 1024 * 1024, preloadLargeVideo: false, lessDataForPhoneCalls: false, videoUploadMaxbitrate: 0), + highPreset: AutodownloadPresetSettings(disabled: false, photoSizeMax: 1 * 1024 * 1024, videoSizeMax: 10 * 1024 * 1024, fileSizeMax: 3 * 1024 * 1024, preloadLargeVideo: false, lessDataForPhoneCalls: false, videoUploadMaxbitrate: 0)) } public init(lowPreset: AutodownloadPresetSettings, mediumPreset: AutodownloadPresetSettings, highPreset: AutodownloadPresetSettings) { diff --git a/submodules/SyncCore/Sources/CachedChannelData.swift b/submodules/SyncCore/Sources/CachedChannelData.swift index c00a913477..49bcb7c71c 100644 --- a/submodules/SyncCore/Sources/CachedChannelData.swift +++ b/submodules/SyncCore/Sources/CachedChannelData.swift @@ -166,6 +166,7 @@ public final class CachedChannelData: CachedPeerData { public let slowModeTimeout: Int32? public let slowModeValidUntilTimestamp: Int32? public let hasScheduledMessages: Bool + public let statsDatacenterId: Int32 public let peerIds: Set public let messageIds: Set @@ -192,9 +193,10 @@ public final class CachedChannelData: CachedPeerData { self.slowModeTimeout = nil self.slowModeValidUntilTimestamp = nil self.hasScheduledMessages = false + self.statsDatacenterId = 0 } - public init(isNotAccessible: Bool, flags: CachedChannelFlags, about: String?, participantsSummary: CachedChannelParticipantsSummary, exportedInvitation: ExportedInvitation?, botInfos: [CachedPeerBotInfo], peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, stickerPack: StickerPackCollectionInfo?, minAvailableMessageId: MessageId?, migrationReference: ChannelMigrationReference?, linkedDiscussionPeerId: PeerId?, peerGeoLocation: PeerGeoLocation?, slowModeTimeout: Int32?, slowModeValidUntilTimestamp: Int32?, hasScheduledMessages: Bool) { + public init(isNotAccessible: Bool, flags: CachedChannelFlags, about: String?, participantsSummary: CachedChannelParticipantsSummary, exportedInvitation: ExportedInvitation?, botInfos: [CachedPeerBotInfo], peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, stickerPack: StickerPackCollectionInfo?, minAvailableMessageId: MessageId?, migrationReference: ChannelMigrationReference?, linkedDiscussionPeerId: PeerId?, peerGeoLocation: PeerGeoLocation?, slowModeTimeout: Int32?, slowModeValidUntilTimestamp: Int32?, hasScheduledMessages: Bool, statsDatacenterId: Int32) { self.isNotAccessible = isNotAccessible self.flags = flags self.about = about @@ -211,6 +213,7 @@ public final class CachedChannelData: CachedPeerData { self.slowModeTimeout = slowModeTimeout self.slowModeValidUntilTimestamp = slowModeValidUntilTimestamp self.hasScheduledMessages = hasScheduledMessages + self.statsDatacenterId = statsDatacenterId var peerIds = Set() for botInfo in botInfos { @@ -231,67 +234,71 @@ public final class CachedChannelData: CachedPeerData { } public func withUpdatedIsNotAccessible(_ isNotAccessible: Bool) -> CachedChannelData { - return CachedChannelData(isNotAccessible: isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages) + return CachedChannelData(isNotAccessible: isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId) } public func withUpdatedFlags(_ flags: CachedChannelFlags) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId) } public func withUpdatedAbout(_ about: String?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId) } public func withUpdatedParticipantsSummary(_ participantsSummary: CachedChannelParticipantsSummary) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId) } public func withUpdatedExportedInvitation(_ exportedInvitation: ExportedInvitation?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId) } public func withUpdatedBotInfos(_ botInfos: [CachedPeerBotInfo]) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId) } public func withUpdatedPeerStatusSettings(_ peerStatusSettings: PeerStatusSettings?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId) } public func withUpdatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId) } public func withUpdatedStickerPack(_ stickerPack: StickerPackCollectionInfo?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId) } public func withUpdatedMinAvailableMessageId(_ minAvailableMessageId: MessageId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId) } public func withUpdatedMigrationReference(_ migrationReference: ChannelMigrationReference?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId) } public func withUpdatedLinkedDiscussionPeerId(_ linkedDiscussionPeerId: PeerId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId) } public func withUpdatedPeerGeoLocation(_ peerGeoLocation: PeerGeoLocation?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId) } public func withUpdatedSlowModeTimeout(_ slowModeTimeout: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId) } public func withUpdatedSlowModeValidUntilTimestamp(_ slowModeValidUntilTimestamp: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId) } public func withUpdatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: hasScheduledMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: hasScheduledMessages, statsDatacenterId: self.statsDatacenterId) + } + + public func withUpdatedStatsDatacenterId(_ statsDatacenterId: Int32) -> CachedChannelData { + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: statsDatacenterId) } public init(decoder: PostboxDecoder) { @@ -346,6 +353,7 @@ public final class CachedChannelData: CachedPeerData { self.slowModeTimeout = decoder.decodeOptionalInt32ForKey("smt") self.slowModeValidUntilTimestamp = decoder.decodeOptionalInt32ForKey("smv") self.hasScheduledMessages = decoder.decodeBoolForKey("hsm", orElse: false) + self.statsDatacenterId = decoder.decodeInt32ForKey("sdi", orElse: 0) if let linkedDiscussionPeerId = self.linkedDiscussionPeerId { peerIds.insert(linkedDiscussionPeerId) @@ -430,6 +438,7 @@ public final class CachedChannelData: CachedPeerData { encoder.encodeNil(forKey: "smv") } encoder.encodeBool(self.hasScheduledMessages, forKey: "hsm") + encoder.encodeInt32(self.statsDatacenterId, forKey: "sdi") } public func isEqual(to: CachedPeerData) -> Bool { @@ -497,6 +506,14 @@ public final class CachedChannelData: CachedPeerData { return false } + if other.hasScheduledMessages != self.hasScheduledMessages { + return false + } + + if other.statsDatacenterId != self.statsDatacenterId { + return false + } + return true } } diff --git a/submodules/SyncCore/Sources/EmbeddedMediaStickersMessageAttribute.swift b/submodules/SyncCore/Sources/EmbeddedMediaStickersMessageAttribute.swift new file mode 100644 index 0000000000..e904017618 --- /dev/null +++ b/submodules/SyncCore/Sources/EmbeddedMediaStickersMessageAttribute.swift @@ -0,0 +1,18 @@ +import Foundation +import Postbox + +public class EmbeddedMediaStickersMessageAttribute: MessageAttribute { + public let files: [TelegramMediaFile] + + public init(files: [TelegramMediaFile]) { + self.files = files + } + + required public init(decoder: PostboxDecoder) { + self.files = decoder.decodeObjectArrayWithDecoderForKey("files") + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeObjectArray(self.files, forKey: "files") + } +} diff --git a/submodules/SyncCore/Sources/MediaReference.swift b/submodules/SyncCore/Sources/MediaReference.swift index bf20c50227..0a8876b606 100644 --- a/submodules/SyncCore/Sources/MediaReference.swift +++ b/submodules/SyncCore/Sources/MediaReference.swift @@ -142,6 +142,72 @@ public enum WebpageReferenceContent: PostboxCoding, Hashable, Equatable { } } +public enum ThemeReference: PostboxCoding, Hashable, Equatable { + case slug(String) + + public init(decoder: PostboxDecoder) { + switch decoder.decodeInt32ForKey("r", orElse: 0) { + case 0: + self = .slug(decoder.decodeStringForKey("s", orElse: "")) + default: + self = .slug("") + assertionFailure() + } + } + + public func encode(_ encoder: PostboxEncoder) { + switch self { + case let .slug(slug): + encoder.encodeInt32(0, forKey: "r") + encoder.encodeString(slug, forKey: "s") + } + } + + public static func ==(lhs: ThemeReference, rhs: ThemeReference) -> Bool { + switch lhs { + case let .slug(slug): + if case .slug(slug) = rhs { + return true + } else { + return false + } + } + } +} + +public enum WallpaperReference: PostboxCoding, Hashable, Equatable { + case slug(String) + + public init(decoder: PostboxDecoder) { + switch decoder.decodeInt32ForKey("r", orElse: 0) { + case 0: + self = .slug(decoder.decodeStringForKey("s", orElse: "")) + default: + self = .slug("") + assertionFailure() + } + } + + public func encode(_ encoder: PostboxEncoder) { + switch self { + case let .slug(slug): + encoder.encodeInt32(0, forKey: "r") + encoder.encodeString(slug, forKey: "s") + } + } + + public static func ==(lhs: WallpaperReference, rhs: WallpaperReference) -> Bool { + switch lhs { + case let .slug(slug): + if case .slug(slug) = rhs { + return true + } else { + return false + } + } + } +} + public enum AnyMediaReference: Equatable { case standalone(media: Media) case message(message: MessageReference, media: Media) @@ -428,8 +494,9 @@ public enum MediaResourceReference: Equatable { case standalone(resource: MediaResource) case avatar(peer: PeerReference, resource: MediaResource) case messageAuthorAvatar(message: MessageReference, resource: MediaResource) - case wallpaper(resource: MediaResource) + case wallpaper(wallpaper: WallpaperReference?, resource: MediaResource) case stickerPackThumbnail(stickerPack: StickerPackReference, resource: MediaResource) + case theme(theme: ThemeReference, resource: MediaResource) public var resource: MediaResource { switch self { @@ -441,10 +508,12 @@ public enum MediaResourceReference: Equatable { return resource case let .messageAuthorAvatar(_, resource): return resource - case let .wallpaper(resource): + case let .wallpaper(_, resource): return resource case let .stickerPackThumbnail(_, resource): return resource + case let .theme(_, resource): + return resource } } @@ -474,8 +543,8 @@ public enum MediaResourceReference: Equatable { } else { return false } - case let .wallpaper(lhsResource): - if case let .wallpaper(rhsResource) = rhs, lhsResource.isEqual(to: rhsResource) { + case let .wallpaper(lhsWallpaper, lhsResource): + if case let .wallpaper(rhsWallpaper, rhsResource) = rhs, lhsWallpaper == rhsWallpaper, lhsResource.isEqual(to: rhsResource) { return true } else { return false @@ -486,6 +555,12 @@ public enum MediaResourceReference: Equatable { } else { return false } + case let .theme(lhsTheme, lhsResource): + if case let .theme(rhsTheme, rhsResource) = rhs, lhsTheme == rhsTheme, lhsResource.isEqual(to: rhsResource) { + return true + } else { + return false + } } } } diff --git a/submodules/SyncCore/Sources/Namespaces.swift b/submodules/SyncCore/Sources/Namespaces.swift index f35cc6db1e..1af4170023 100644 --- a/submodules/SyncCore/Sources/Namespaces.swift +++ b/submodules/SyncCore/Sources/Namespaces.swift @@ -67,7 +67,8 @@ public struct Namespaces { public static let cachedStickerQueryResults: Int8 = 5 public static let cachedSecureIdConfiguration: Int8 = 6 public static let cachedWallpapersConfiguration: Int8 = 7 - public static let cachedThemesConfiguration: Int8 = 7 + public static let cachedThemesConfiguration: Int8 = 8 + public static let cachedPollResults: Int8 = 9 } public struct UnorderedItemList { @@ -138,10 +139,44 @@ public struct OperationLogTags { public static let SynchronizeEmojiKeywords = PeerOperationLogTag(value: 19) } +public struct LegacyPeerSummaryCounterTags: OptionSet, Sequence, Hashable { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let regularChatsAndPrivateGroups = LegacyPeerSummaryCounterTags(rawValue: 1 << 0) + public static let publicGroups = LegacyPeerSummaryCounterTags(rawValue: 1 << 1) + public static let channels = LegacyPeerSummaryCounterTags(rawValue: 1 << 2) + + public func makeIterator() -> AnyIterator { + var index = 0 + return AnyIterator { () -> LegacyPeerSummaryCounterTags? in + while index < 31 { + let currentTags = self.rawValue >> UInt32(index) + let tag = LegacyPeerSummaryCounterTags(rawValue: 1 << UInt32(index)) + index += 1 + if currentTags == 0 { + break + } + + if (currentTags & 1) != 0 { + return tag + } + } + return nil + } + } +} + public extension PeerSummaryCounterTags { - static let regularChatsAndPrivateGroups = PeerSummaryCounterTags(rawValue: 1 << 0) - static let publicGroups = PeerSummaryCounterTags(rawValue: 1 << 1) - static let channels = PeerSummaryCounterTags(rawValue: 1 << 2) + static let privateChat = PeerSummaryCounterTags(rawValue: 1 << 3) + static let secretChat = PeerSummaryCounterTags(rawValue: 1 << 4) + static let privateGroup = PeerSummaryCounterTags(rawValue: 1 << 5) + static let bot = PeerSummaryCounterTags(rawValue: 1 << 6) + static let channel = PeerSummaryCounterTags(rawValue: 1 << 7) + static let publicGroup = PeerSummaryCounterTags(rawValue: 1 << 8) } private enum PreferencesKeyValues: Int32 { @@ -160,6 +195,9 @@ private enum PreferencesKeyValues: Int32 { case contactsSettings = 16 case secretChatSettings = 17 case walletCollection = 18 + case contentSettings = 19 + case chatListFilters = 20 + case peersNearby = 21 } public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { @@ -264,6 +302,24 @@ public struct PreferencesKeys { key.setInt32(0, value: PreferencesKeyValues.walletCollection.rawValue) return key }() + + public static let contentSettings: ValueBoxKey = { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: PreferencesKeyValues.contentSettings.rawValue) + return key + }() + + public static let chatListFilters: ValueBoxKey = { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: PreferencesKeyValues.chatListFilters.rawValue) + return key + }() + + public static let peersNearby: ValueBoxKey = { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: PreferencesKeyValues.peersNearby.rawValue) + return key + }() } private enum SharedDataKeyValues: Int32 { diff --git a/submodules/SyncCore/Sources/ReplyMarkupMessageAttribute.swift b/submodules/SyncCore/Sources/ReplyMarkupMessageAttribute.swift index 6cebd304fc..c72fdced40 100644 --- a/submodules/SyncCore/Sources/ReplyMarkupMessageAttribute.swift +++ b/submodules/SyncCore/Sources/ReplyMarkupMessageAttribute.swift @@ -10,6 +10,7 @@ public enum ReplyMarkupButtonAction: PostboxCoding, Equatable { case openWebApp case payment case urlAuth(url: String, buttonId: Int32) + case setupPoll(isQuiz: Bool?) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("v", orElse: 0) { @@ -31,6 +32,8 @@ public enum ReplyMarkupButtonAction: PostboxCoding, Equatable { self = .payment case 8: self = .urlAuth(url: decoder.decodeStringForKey("u", orElse: ""), buttonId: decoder.decodeInt32ForKey("b", orElse: 0)) + case 9: + self = .setupPoll(isQuiz: decoder.decodeOptionalInt32ForKey("isq").flatMap { $0 != 0 }) default: self = .text } @@ -38,30 +41,37 @@ public enum ReplyMarkupButtonAction: PostboxCoding, Equatable { public func encode(_ encoder: PostboxEncoder) { switch self { - case .text: - encoder.encodeInt32(0, forKey: "v") - case let .url(url): - encoder.encodeInt32(1, forKey: "v") - encoder.encodeString(url, forKey: "u") - case let .callback(data): - encoder.encodeInt32(2, forKey: "v") - encoder.encodeBytes(data, forKey: "d") - case .requestPhone: - encoder.encodeInt32(3, forKey: "v") - case .requestMap: - encoder.encodeInt32(4, forKey: "v") - case let .switchInline(samePeer, query): - encoder.encodeInt32(5, forKey: "v") - encoder.encodeInt32(samePeer ? 1 : 0, forKey: "s") - encoder.encodeString(query, forKey: "q") - case .openWebApp: - encoder.encodeInt32(6, forKey: "v") - case .payment: - encoder.encodeInt32(7, forKey: "v") - case let .urlAuth(url, buttonId): - encoder.encodeInt32(8, forKey: "v") - encoder.encodeString(url, forKey: "u") - encoder.encodeInt32(buttonId, forKey: "b") + case .text: + encoder.encodeInt32(0, forKey: "v") + case let .url(url): + encoder.encodeInt32(1, forKey: "v") + encoder.encodeString(url, forKey: "u") + case let .callback(data): + encoder.encodeInt32(2, forKey: "v") + encoder.encodeBytes(data, forKey: "d") + case .requestPhone: + encoder.encodeInt32(3, forKey: "v") + case .requestMap: + encoder.encodeInt32(4, forKey: "v") + case let .switchInline(samePeer, query): + encoder.encodeInt32(5, forKey: "v") + encoder.encodeInt32(samePeer ? 1 : 0, forKey: "s") + encoder.encodeString(query, forKey: "q") + case .openWebApp: + encoder.encodeInt32(6, forKey: "v") + case .payment: + encoder.encodeInt32(7, forKey: "v") + case let .urlAuth(url, buttonId): + encoder.encodeInt32(8, forKey: "v") + encoder.encodeString(url, forKey: "u") + encoder.encodeInt32(buttonId, forKey: "b") + case let .setupPoll(isQuiz): + encoder.encodeInt32(9, forKey: "v") + if let isQuiz = isQuiz { + encoder.encodeInt32(isQuiz ? 1 : 0, forKey: "isq") + } else { + encoder.encodeNil(forKey: "isq") + } } } } diff --git a/submodules/SyncCore/Sources/StandaloneAccountTransaction.swift b/submodules/SyncCore/Sources/StandaloneAccountTransaction.swift index 93d625cb33..443881967e 100644 --- a/submodules/SyncCore/Sources/StandaloneAccountTransaction.swift +++ b/submodules/SyncCore/Sources/StandaloneAccountTransaction.swift @@ -19,19 +19,30 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = { } return SeedConfiguration(globalMessageIdsPeerIdNamespaces: globalMessageIdsPeerIdNamespaces, initializeChatListWithHole: (topLevel: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1)), groups: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1))), messageHoles: messageHoles, existingMessageTags: MessageTags.all, messageTagsWithSummary: MessageTags.unseenPersonalMessage, existingGlobalMessageTags: GlobalMessageTags.all, peerNamespacesRequiringMessageTextIndex: [Namespaces.Peer.SecretChat], peerSummaryCounterTags: { peer in - if let peer = peer as? TelegramChannel { - switch peer.info { - case .group: - if let addressName = peer.username, !addressName.isEmpty { - return [.publicGroups] - } else { - return [.regularChatsAndPrivateGroups] - } - case .broadcast: - return [.channels] + if let peer = peer as? TelegramUser { + if peer.botInfo != nil { + return .bot + } else { + return .privateChat + } + } else if let _ = peer as? TelegramGroup { + return .privateGroup + } else if let _ = peer as? TelegramSecretChat { + return .secretChat + } else if let channel = peer as? TelegramChannel { + switch channel.info { + case .broadcast: + return .channel + case .group: + if channel.username != nil { + return .publicGroup + } else { + return .privateGroup + } } } else { - return [.regularChatsAndPrivateGroups] + assertionFailure() + return .privateChat } }, additionalChatListIndexNamespace: Namespaces.Message.Cloud, messageNamespacesRequiringGroupStatsValidation: [Namespaces.Message.Cloud], defaultMessageNamespaceReadStates: [Namespaces.Message.Local: .idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: 0, markedUnread: false)], chatMessagesNamespaces: Set([Namespaces.Message.Cloud, Namespaces.Message.Local, Namespaces.Message.SecretIncoming])) }() diff --git a/submodules/SyncCore/Sources/TelegramMediaImage.swift b/submodules/SyncCore/Sources/TelegramMediaImage.swift index e474fcf24c..2717009705 100644 --- a/submodules/SyncCore/Sources/TelegramMediaImage.swift +++ b/submodules/SyncCore/Sources/TelegramMediaImage.swift @@ -39,6 +39,16 @@ public enum TelegramMediaImageReference: PostboxCoding, Equatable { } } +public struct TelegramMediaImageFlags: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let hasStickers = TelegramMediaImageFlags(rawValue: 1 << 0) +} + public final class TelegramMediaImage: Media, Equatable { public let imageId: MediaId public let representations: [TelegramMediaImageRepresentation] @@ -46,17 +56,19 @@ public final class TelegramMediaImage: Media, Equatable { public let reference: TelegramMediaImageReference? public let partialReference: PartialMediaReference? public let peerIds: [PeerId] = [] + public let flags: TelegramMediaImageFlags public var id: MediaId? { return self.imageId } - public init(imageId: MediaId, representations: [TelegramMediaImageRepresentation], immediateThumbnailData: Data?, reference: TelegramMediaImageReference?, partialReference: PartialMediaReference?) { + public init(imageId: MediaId, representations: [TelegramMediaImageRepresentation], immediateThumbnailData: Data?, reference: TelegramMediaImageReference?, partialReference: PartialMediaReference?, flags: TelegramMediaImageFlags) { self.imageId = imageId self.representations = representations self.immediateThumbnailData = immediateThumbnailData self.reference = reference self.partialReference = partialReference + self.flags = flags } public init(decoder: PostboxDecoder) { @@ -65,6 +77,7 @@ public final class TelegramMediaImage: Media, Equatable { self.immediateThumbnailData = decoder.decodeDataForKey("itd") self.reference = decoder.decodeObjectForKey("rf", decoder: { TelegramMediaImageReference(decoder: $0) }) as? TelegramMediaImageReference self.partialReference = decoder.decodeAnyObjectForKey("prf", decoder: { PartialMediaReference(decoder: $0) }) as? PartialMediaReference + self.flags = TelegramMediaImageFlags(rawValue: decoder.decodeInt32ForKey("fl", orElse: 0)) } public func encode(_ encoder: PostboxEncoder) { @@ -87,6 +100,7 @@ public final class TelegramMediaImage: Media, Equatable { } else { encoder.encodeNil(forKey: "prf") } + encoder.encodeInt32(self.flags.rawValue, forKey: "fl") } public func representationForDisplayAtSize(_ size: PixelDimensions) -> TelegramMediaImageRepresentation? { @@ -130,6 +144,9 @@ public final class TelegramMediaImage: Media, Equatable { if self.partialReference != other.partialReference { return false } + if self.flags != other.flags { + return false + } return true } return false @@ -152,6 +169,9 @@ public final class TelegramMediaImage: Media, Equatable { if self.partialReference != other.partialReference { return false } + if self.flags != other.flags { + return false + } return true } return false @@ -162,7 +182,7 @@ public final class TelegramMediaImage: Media, Equatable { } public func withUpdatedPartialReference(_ partialReference: PartialMediaReference?) -> TelegramMediaImage { - return TelegramMediaImage(imageId: self.imageId, representations: self.representations, immediateThumbnailData: self.immediateThumbnailData, reference: self.reference, partialReference: partialReference) + return TelegramMediaImage(imageId: self.imageId, representations: self.representations, immediateThumbnailData: self.immediateThumbnailData, reference: self.reference, partialReference: partialReference, flags: self.flags) } } diff --git a/submodules/SyncCore/Sources/TelegramMediaPoll.swift b/submodules/SyncCore/Sources/TelegramMediaPoll.swift index 91bcc68457..f01ba73c87 100644 --- a/submodules/SyncCore/Sources/TelegramMediaPoll.swift +++ b/submodules/SyncCore/Sources/TelegramMediaPoll.swift @@ -24,38 +24,45 @@ public struct TelegramMediaPollOptionVoters: Equatable, PostboxCoding { public let selected: Bool public let opaqueIdentifier: Data public let count: Int32 + public let isCorrect: Bool - public init(selected: Bool, opaqueIdentifier: Data, count: Int32) { + public init(selected: Bool, opaqueIdentifier: Data, count: Int32, isCorrect: Bool) { self.selected = selected self.opaqueIdentifier = opaqueIdentifier self.count = count + self.isCorrect = isCorrect } public init(decoder: PostboxDecoder) { self.selected = decoder.decodeInt32ForKey("s", orElse: 0) != 0 self.opaqueIdentifier = decoder.decodeDataForKey("i") ?? Data() self.count = decoder.decodeInt32ForKey("c", orElse: 0) + self.isCorrect = decoder.decodeInt32ForKey("cr", orElse: 0) != 0 } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.selected ? 1 : 0, forKey: "s") encoder.encodeData(self.opaqueIdentifier, forKey: "i") encoder.encodeInt32(self.count, forKey: "c") + encoder.encodeInt32(self.isCorrect ? 1 : 0, forKey: "cr") } } public struct TelegramMediaPollResults: Equatable, PostboxCoding { public let voters: [TelegramMediaPollOptionVoters]? public let totalVoters: Int32? + public let recentVoters: [PeerId] - public init(voters: [TelegramMediaPollOptionVoters]?, totalVoters: Int32?) { + public init(voters: [TelegramMediaPollOptionVoters]?, totalVoters: Int32?, recentVoters: [PeerId]) { self.voters = voters self.totalVoters = totalVoters + self.recentVoters = recentVoters } public init(decoder: PostboxDecoder) { self.voters = decoder.decodeOptionalObjectArrayWithDecoderForKey("v") self.totalVoters = decoder.decodeOptionalInt32ForKey("t") + self.recentVoters = decoder.decodeInt64ArrayForKey("rv").map(PeerId.init) } public func encode(_ encoder: PostboxEncoder) { @@ -69,6 +76,39 @@ public struct TelegramMediaPollResults: Equatable, PostboxCoding { } else { encoder.encodeNil(forKey: "t") } + encoder.encodeInt64Array(self.recentVoters.map { $0.toInt64() }, forKey: "rv") + } +} + +public enum TelegramMediaPollPublicity: Int32 { + case anonymous + case `public` +} + +public enum TelegramMediaPollKind: Equatable, PostboxCoding { + case poll(multipleAnswers: Bool) + case quiz + + public init(decoder: PostboxDecoder) { + switch decoder.decodeInt32ForKey("_v", orElse: 0) { + case 0: + self = .poll(multipleAnswers: decoder.decodeInt32ForKey("m", orElse: 0) != 0) + case 1: + self = .quiz + default: + assertionFailure() + self = .poll(multipleAnswers: false) + } + } + + public func encode(_ encoder: PostboxEncoder) { + switch self { + case let .poll(multipleAnswers): + encoder.encodeInt32(0, forKey: "_v") + encoder.encodeInt32(multipleAnswers ? 1 : 0, forKey: "m") + case .quiz: + encoder.encodeInt32(1, forKey: "_v") + } } } @@ -77,17 +117,26 @@ public final class TelegramMediaPoll: Media, Equatable { return self.pollId } public let pollId: MediaId - public let peerIds: [PeerId] = [] + public var peerIds: [PeerId] { + return results.recentVoters + } + + public let publicity: TelegramMediaPollPublicity + public let kind: TelegramMediaPollKind public let text: String public let options: [TelegramMediaPollOption] + public let correctAnswers: [Data]? public let results: TelegramMediaPollResults public let isClosed: Bool - public init(pollId: MediaId, text: String, options: [TelegramMediaPollOption], results: TelegramMediaPollResults, isClosed: Bool) { + public init(pollId: MediaId, publicity: TelegramMediaPollPublicity, kind: TelegramMediaPollKind, text: String, options: [TelegramMediaPollOption], correctAnswers: [Data]?, results: TelegramMediaPollResults, isClosed: Bool) { self.pollId = pollId + self.publicity = publicity + self.kind = kind self.text = text self.options = options + self.correctAnswers = correctAnswers self.results = results self.isClosed = isClosed } @@ -98,18 +147,28 @@ public final class TelegramMediaPoll: Media, Equatable { } else { self.pollId = MediaId(namespace: Namespaces.Media.LocalPoll, id: 0) } + self.publicity = TelegramMediaPollPublicity(rawValue: decoder.decodeInt32ForKey("pb", orElse: 0)) ?? TelegramMediaPollPublicity.anonymous + self.kind = decoder.decodeObjectForKey("kn", decoder: { TelegramMediaPollKind(decoder: $0) }) as? TelegramMediaPollKind ?? TelegramMediaPollKind.poll(multipleAnswers: false) self.text = decoder.decodeStringForKey("t", orElse: "") self.options = decoder.decodeObjectArrayWithDecoderForKey("os") - self.results = decoder.decodeObjectForKey("rs", decoder: { TelegramMediaPollResults(decoder: $0) }) as? TelegramMediaPollResults ?? TelegramMediaPollResults(voters: nil, totalVoters: nil) + self.correctAnswers = decoder.decodeOptionalDataArrayForKey("ca") + self.results = decoder.decodeObjectForKey("rs", decoder: { TelegramMediaPollResults(decoder: $0) }) as? TelegramMediaPollResults ?? TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: []) self.isClosed = decoder.decodeInt32ForKey("ic", orElse: 0) != 0 } public func encode(_ encoder: PostboxEncoder) { let buffer = WriteBuffer() self.pollId.encodeToBuffer(buffer) + encoder.encodeInt32(self.publicity.rawValue, forKey: "pb") + encoder.encodeObject(self.kind, forKey: "kn") encoder.encodeBytes(buffer, forKey: "i") encoder.encodeString(self.text, forKey: "t") encoder.encodeObjectArray(self.options, forKey: "os") + if let correctAnswers = self.correctAnswers { + encoder.encodeDataArray(correctAnswers, forKey: "ca") + } else { + encoder.encodeNil(forKey: "ca") + } encoder.encodeObject(results, forKey: "rs") encoder.encodeInt32(self.isClosed ? 1 : 0, forKey: "ic") } @@ -129,12 +188,21 @@ public final class TelegramMediaPoll: Media, Equatable { if lhs.pollId != rhs.pollId { return false } + if lhs.publicity != rhs.publicity { + return false + } + if lhs.kind != rhs.kind { + return false + } if lhs.text != rhs.text { return false } if lhs.options != rhs.options { return false } + if lhs.correctAnswers != rhs.correctAnswers { + return false + } if lhs.results != rhs.results { return false } @@ -149,20 +217,26 @@ public final class TelegramMediaPoll: Media, Equatable { if min { if let currentVoters = self.results.voters, let updatedVoters = results.voters { var selectedOpaqueIdentifiers = Set() + var correctOpaqueIdentifiers = Set() for voters in currentVoters { if voters.selected { selectedOpaqueIdentifiers.insert(voters.opaqueIdentifier) } + if voters.isCorrect { + correctOpaqueIdentifiers.insert(voters.opaqueIdentifier) + } } updatedResults = TelegramMediaPollResults(voters: updatedVoters.map({ voters in - return TelegramMediaPollOptionVoters(selected: selectedOpaqueIdentifiers.contains(voters.opaqueIdentifier), opaqueIdentifier: voters.opaqueIdentifier, count: voters.count) - }), totalVoters: results.totalVoters) + return TelegramMediaPollOptionVoters(selected: selectedOpaqueIdentifiers.contains(voters.opaqueIdentifier), opaqueIdentifier: voters.opaqueIdentifier, count: voters.count, isCorrect: correctOpaqueIdentifiers.contains(voters.opaqueIdentifier)) + }), totalVoters: results.totalVoters, recentVoters: results.recentVoters) + } else if let updatedVoters = results.voters { + updatedResults = TelegramMediaPollResults(voters: updatedVoters, totalVoters: results.totalVoters, recentVoters: results.recentVoters) } else { - updatedResults = TelegramMediaPollResults(voters: self.results.voters, totalVoters: results.totalVoters) + updatedResults = TelegramMediaPollResults(voters: self.results.voters, totalVoters: results.totalVoters, recentVoters: results.recentVoters) } } else { updatedResults = results } - return TelegramMediaPoll(pollId: self.pollId, text: self.text, options: self.options, results: updatedResults, isClosed: self.isClosed) + return TelegramMediaPoll(pollId: self.pollId, publicity: self.publicity, kind: self.kind, text: self.text, options: self.options, correctAnswers: self.correctAnswers, results: updatedResults, isClosed: self.isClosed) } } diff --git a/submodules/SyncCore/Sources/TelegramMediaWebpage.swift b/submodules/SyncCore/Sources/TelegramMediaWebpage.swift index 9a0ef4d6d9..2e6eba6cbe 100644 --- a/submodules/SyncCore/Sources/TelegramMediaWebpage.swift +++ b/submodules/SyncCore/Sources/TelegramMediaWebpage.swift @@ -1,5 +1,74 @@ import Postbox +private enum TelegramMediaWebpageAttributeTypes: Int32 { + case unsupported + case theme +} + +public enum TelegramMediaWebpageAttribute: PostboxCoding, Equatable { + case unsupported + case theme(TelegraMediaWebpageThemeAttribute) + + public init(decoder: PostboxDecoder) { + switch decoder.decodeInt32ForKey("r", orElse: 0) { + case TelegramMediaWebpageAttributeTypes.theme.rawValue: + self = .theme(decoder.decodeObjectForKey("a", decoder: { TelegraMediaWebpageThemeAttribute(decoder: $0) }) as! TelegraMediaWebpageThemeAttribute) + default: + self = .unsupported + } + } + + public func encode(_ encoder: PostboxEncoder) { + switch self { + case .unsupported: + encoder.encodeInt32(TelegramMediaWebpageAttributeTypes.unsupported.rawValue, forKey: "r") + case let .theme(attribute): + encoder.encodeInt32(TelegramMediaWebpageAttributeTypes.theme.rawValue, forKey: "r") + encoder.encodeObject(attribute, forKey: "a") + } + } +} + +public final class TelegraMediaWebpageThemeAttribute: PostboxCoding, Equatable { + public static func == (lhs: TelegraMediaWebpageThemeAttribute, rhs: TelegraMediaWebpageThemeAttribute) -> Bool { + if lhs.settings != rhs.settings { + return false + } + if lhs.files.count != rhs.files.count { + return false + } else { + for i in 0 ..< lhs.files.count { + if !lhs.files[i].isEqual(to: rhs.files[i]) { + return false + } + } + } + return true + } + + public let files: [TelegramMediaFile] + public let settings: TelegramThemeSettings? + + public init(files: [TelegramMediaFile], settings: TelegramThemeSettings?) { + self.files = files + self.settings = settings + } + + public init(decoder: PostboxDecoder) { + self.files = decoder.decodeObjectArrayForKey("files") + self.settings = decoder.decodeObjectForKey("settings", decoder: { TelegramThemeSettings(decoder: $0) }) as? TelegramThemeSettings + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeObjectArray(self.files, forKey: "files") + if let settings = self.settings { + encoder.encodeObject(settings, forKey: "settings") + } else { + encoder.encodeNil(forKey: "settings") + } + } +} + public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { public let url: String public let displayUrl: String @@ -16,10 +85,10 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { public let image: TelegramMediaImage? public let file: TelegramMediaFile? - public let files: [TelegramMediaFile]? + public let attributes: [TelegramMediaWebpageAttribute] public let instantPage: InstantPage? - public init(url: String, displayUrl: String, hash: Int32, type: String?, websiteName: String?, title: String?, text: String?, embedUrl: String?, embedType: String?, embedSize: PixelDimensions?, duration: Int?, author: String?, image: TelegramMediaImage?, file: TelegramMediaFile?, files: [TelegramMediaFile]?, instantPage: InstantPage?) { + public init(url: String, displayUrl: String, hash: Int32, type: String?, websiteName: String?, title: String?, text: String?, embedUrl: String?, embedType: String?, embedSize: PixelDimensions?, duration: Int?, author: String?, image: TelegramMediaImage?, file: TelegramMediaFile?, attributes: [TelegramMediaWebpageAttribute], instantPage: InstantPage?) { self.url = url self.displayUrl = displayUrl self.hash = hash @@ -34,7 +103,7 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { self.author = author self.image = image self.file = file - self.files = files + self.attributes = attributes self.instantPage = instantPage } @@ -72,7 +141,14 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { self.file = nil } - self.files = decoder.decodeOptionalObjectArrayWithDecoderForKey("fis") + var effectiveAttributes: [TelegramMediaWebpageAttribute] = [] + if let attributes = decoder.decodeObjectArrayWithDecoderForKey("attr") as [TelegramMediaWebpageAttribute]? { + effectiveAttributes.append(contentsOf: attributes) + } + if let legacyFiles = decoder.decodeOptionalObjectArrayWithDecoderForKey("fis") as [TelegramMediaFile]? { + effectiveAttributes.append(.theme(TelegraMediaWebpageThemeAttribute(files: legacyFiles, settings: nil))) + } + self.attributes = effectiveAttributes if let instantPage = decoder.decodeObjectForKey("ip", decoder: { InstantPage(decoder: $0) }) as? InstantPage { self.instantPage = instantPage @@ -142,11 +218,9 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "fi") } - if let files = self.files { - encoder.encodeObjectArray(files, forKey: "fis") - } else { - encoder.encodeNil(forKey: "fis") - } + + encoder.encodeObjectArray(self.attributes, forKey: "attr") + if let instantPage = self.instantPage { encoder.encodeObject(instantPage, forKey: "ip") } else { @@ -187,18 +261,14 @@ public func ==(lhs: TelegramMediaWebpageLoadedContent, rhs: TelegramMediaWebpage return false } - if let lhsFiles = lhs.files, let rhsFiles = rhs.files { - if lhsFiles.count != rhsFiles.count { - return false - } else { - for i in 0 ..< lhsFiles.count { - if !lhsFiles[i].isEqual(to: rhsFiles[i]) { - return false - } + if lhs.attributes.count != rhs.attributes.count { + return false + } else { + for i in 0 ..< lhs.attributes.count { + if lhs.attributes[i] != rhs.attributes[i] { + return false } } - } else if (lhs.files == nil) != (rhs.files == nil) { - return false } if lhs.instantPage != rhs.instantPage { diff --git a/submodules/SyncCore/Sources/TelegramTheme.swift b/submodules/SyncCore/Sources/TelegramTheme.swift index 640005bff7..8245179787 100644 --- a/submodules/SyncCore/Sources/TelegramTheme.swift +++ b/submodules/SyncCore/Sources/TelegramTheme.swift @@ -1,21 +1,94 @@ import Postbox +public enum TelegramBaseTheme: Int32 { + case classic + case day + case night + case tinted +} + +public extension UInt32 { + init(bitPattern: UInt32) { + self = bitPattern + } +} + +public final class TelegramThemeSettings: PostboxCoding, Equatable { + public static func == (lhs: TelegramThemeSettings, rhs: TelegramThemeSettings) -> Bool { + if lhs.baseTheme != rhs.baseTheme { + return false + } + if lhs.accentColor != rhs.accentColor { + return false + } + if lhs.messageColors?.0 != rhs.messageColors?.0 || lhs.messageColors?.1 != rhs.messageColors?.1 { + return false + } + if lhs.wallpaper != rhs.wallpaper { + return false + } + return true + } + + public let baseTheme: TelegramBaseTheme + public let accentColor: UInt32 + public let messageColors: (top: UInt32, bottom: UInt32)? + public let wallpaper: TelegramWallpaper? + + public init(baseTheme: TelegramBaseTheme, accentColor: UInt32, messageColors: (top: UInt32, bottom: UInt32)?, wallpaper: TelegramWallpaper?) { + self.baseTheme = baseTheme + self.accentColor = accentColor + self.messageColors = messageColors + self.wallpaper = wallpaper + } + + public init(decoder: PostboxDecoder) { + self.baseTheme = TelegramBaseTheme(rawValue: decoder.decodeInt32ForKey("baseTheme", orElse: 0)) ?? .classic + self.accentColor = UInt32(bitPattern: decoder.decodeInt32ForKey("accent", orElse: 0)) + if let topMessageColor = decoder.decodeOptionalInt32ForKey("topMessage"), let bottomMessageColor = decoder.decodeOptionalInt32ForKey("bottomMessage") { + self.messageColors = (UInt32(bitPattern: topMessageColor), UInt32(bitPattern: bottomMessageColor)) + } else { + self.messageColors = nil + } + self.wallpaper = decoder.decodeObjectForKey("wallpaper", decoder: { TelegramWallpaper(decoder: $0) }) as? TelegramWallpaper + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.baseTheme.rawValue, forKey: "baseTheme") + encoder.encodeInt32(Int32(bitPattern: self.accentColor), forKey: "accent") + if let (topMessageColor, bottomMessageColor) = self.messageColors { + encoder.encodeInt32(Int32(bitPattern: topMessageColor), forKey: "topMessage") + encoder.encodeInt32(Int32(bitPattern: bottomMessageColor), forKey: "bottomMessage") + } else { + encoder.encodeNil(forKey: "topMessage") + encoder.encodeNil(forKey: "bottomMessage") + } + if let wallpaper = self.wallpaper { + encoder.encodeObject(wallpaper, forKey: "wallpaper") + } else { + encoder.encodeNil(forKey: "wallpaper") + } + } +} + public final class TelegramTheme: OrderedItemListEntryContents, Equatable { public let id: Int64 public let accessHash: Int64 public let slug: String public let title: String public let file: TelegramMediaFile? + public let settings: TelegramThemeSettings? public let isCreator: Bool public let isDefault: Bool public let installCount: Int32 - public init(id: Int64, accessHash: Int64, slug: String, title: String, file: TelegramMediaFile?, isCreator: Bool, isDefault: Bool, installCount: Int32) { + public init(id: Int64, accessHash: Int64, slug: String, title: String, file: TelegramMediaFile?, settings: TelegramThemeSettings?, isCreator: Bool, isDefault: Bool, installCount: Int32) { self.id = id self.accessHash = accessHash self.slug = slug self.title = title self.file = file + self.settings = settings self.isCreator = isCreator self.isDefault = isDefault self.installCount = installCount @@ -27,6 +100,7 @@ public final class TelegramTheme: OrderedItemListEntryContents, Equatable { self.slug = decoder.decodeStringForKey("slug", orElse: "") self.title = decoder.decodeStringForKey("title", orElse: "") self.file = decoder.decodeObjectForKey("file", decoder: { TelegramMediaFile(decoder: $0) }) as? TelegramMediaFile + self.settings = decoder.decodeObjectForKey("settings", decoder: { TelegramThemeSettings(decoder: $0) }) as? TelegramThemeSettings self.isCreator = decoder.decodeInt32ForKey("isCreator", orElse: 0) != 0 self.isDefault = decoder.decodeInt32ForKey("isDefault", orElse: 0) != 0 self.installCount = decoder.decodeInt32ForKey("installCount", orElse: 0) @@ -42,6 +116,11 @@ public final class TelegramTheme: OrderedItemListEntryContents, Equatable { } else { encoder.encodeNil(forKey: "file") } + if let settings = self.settings { + encoder.encodeObject(settings, forKey: "settings") + } else { + encoder.encodeNil(forKey: "settings") + } encoder.encodeInt32(self.isCreator ? 1 : 0, forKey: "isCreator") encoder.encodeInt32(self.isDefault ? 1 : 0, forKey: "isDefault") encoder.encodeInt32(self.installCount, forKey: "installCount") @@ -63,6 +142,9 @@ public final class TelegramTheme: OrderedItemListEntryContents, Equatable { if lhs.file?.id != rhs.file?.id { return false } + if lhs.settings != rhs.settings { + return false + } if lhs.isCreator != rhs.isCreator { return false } diff --git a/submodules/SyncCore/Sources/TelegramWallpaper.swift b/submodules/SyncCore/Sources/TelegramWallpaper.swift index da472ffd7a..7b500b04de 100644 --- a/submodules/SyncCore/Sources/TelegramWallpaper.swift +++ b/submodules/SyncCore/Sources/TelegramWallpaper.swift @@ -3,42 +3,81 @@ import Postbox public struct WallpaperSettings: PostboxCoding, Equatable { public let blur: Bool public let motion: Bool - public let color: Int32? + public let color: UInt32? + public let bottomColor: UInt32? public let intensity: Int32? + public let rotation: Int32? - public init(blur: Bool = false, motion: Bool = false, color: Int32? = nil, intensity: Int32? = nil) { + public init(blur: Bool = false, motion: Bool = false, color: UInt32? = nil, bottomColor: UInt32? = nil, intensity: Int32? = nil, rotation: Int32? = nil) { self.blur = blur self.motion = motion self.color = color + self.bottomColor = bottomColor self.intensity = intensity + self.rotation = rotation } public init(decoder: PostboxDecoder) { self.blur = decoder.decodeInt32ForKey("b", orElse: 0) != 0 self.motion = decoder.decodeInt32ForKey("m", orElse: 0) != 0 - self.color = decoder.decodeOptionalInt32ForKey("c") + self.color = decoder.decodeOptionalInt32ForKey("c").flatMap { UInt32(bitPattern: $0) } + self.bottomColor = decoder.decodeOptionalInt32ForKey("bc").flatMap { UInt32(bitPattern: $0) } self.intensity = decoder.decodeOptionalInt32ForKey("i") + self.rotation = decoder.decodeOptionalInt32ForKey("r") } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.blur ? 1 : 0, forKey: "b") encoder.encodeInt32(self.motion ? 1 : 0, forKey: "m") if let color = self.color { - encoder.encodeInt32(color, forKey: "c") + encoder.encodeInt32(Int32(bitPattern: color), forKey: "c") } else { encoder.encodeNil(forKey: "c") } + if let bottomColor = self.bottomColor { + encoder.encodeInt32(Int32(bitPattern: bottomColor), forKey: "bc") + } else { + encoder.encodeNil(forKey: "bc") + } if let intensity = self.intensity { encoder.encodeInt32(intensity, forKey: "i") } else { encoder.encodeNil(forKey: "i") } + if let rotation = self.rotation { + encoder.encodeInt32(rotation, forKey: "r") + } else { + encoder.encodeNil(forKey: "r") + } + } + + public static func ==(lhs: WallpaperSettings, rhs: WallpaperSettings) -> Bool { + if lhs.blur != rhs.blur { + return false + } + if lhs.motion != rhs.motion { + return false + } + if lhs.color != rhs.color { + return false + } + if lhs.bottomColor != rhs.bottomColor { + return false + } + if lhs.intensity != rhs.intensity { + return false + } + if lhs.rotation != rhs.rotation { + return false + } + return true } } public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { case builtin(WallpaperSettings) - case color(Int32) + case color(UInt32) + case gradient(UInt32, UInt32, WallpaperSettings) case image([TelegramMediaImageRepresentation], WallpaperSettings) case file(id: Int64, accessHash: Int64, isCreator: Bool, isDefault: Bool, isPattern: Bool, isDark: Bool, slug: String, file: TelegramMediaFile, settings: WallpaperSettings) @@ -48,7 +87,7 @@ public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { let settings = decoder.decodeObjectForKey("settings", decoder: { WallpaperSettings(decoder: $0) }) as? WallpaperSettings ?? WallpaperSettings() self = .builtin(settings) case 1: - self = .color(decoder.decodeInt32ForKey("c", orElse: 0)) + self = .color(UInt32(bitPattern: decoder.decodeInt32ForKey("c", orElse: 0))) case 2: let settings = decoder.decodeObjectForKey("settings", decoder: { WallpaperSettings(decoder: $0) }) as? WallpaperSettings ?? WallpaperSettings() self = .image(decoder.decodeObjectArrayWithDecoderForKey("i"), settings) @@ -59,7 +98,9 @@ public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { } else { self = .color(0xffffff) } - + case 4: + let settings = decoder.decodeObjectForKey("settings", decoder: { WallpaperSettings(decoder: $0) }) as? WallpaperSettings ?? WallpaperSettings() + self = .gradient(UInt32(bitPattern: decoder.decodeInt32ForKey("c1", orElse: 0)), UInt32(bitPattern: decoder.decodeInt32ForKey("c2", orElse: 0)), settings) default: assertionFailure() self = .color(0xffffff) @@ -82,7 +123,12 @@ public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { encoder.encodeObject(settings, forKey: "settings") case let .color(color): encoder.encodeInt32(1, forKey: "v") - encoder.encodeInt32(color, forKey: "c") + encoder.encodeInt32(Int32(bitPattern: color), forKey: "c") + case let .gradient(topColor, bottomColor, settings): + encoder.encodeInt32(4, forKey: "v") + encoder.encodeInt32(Int32(bitPattern: topColor), forKey: "c1") + encoder.encodeInt32(Int32(bitPattern: bottomColor), forKey: "c2") + encoder.encodeObject(settings, forKey: "settings") case let .image(representations, settings): encoder.encodeInt32(2, forKey: "v") encoder.encodeObjectArray(representations, forKey: "i") @@ -115,6 +161,12 @@ public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { } else { return false } + case let .gradient(topColor, bottomColor, settings): + if case .gradient(topColor, bottomColor, settings) = rhs { + return true + } else { + return false + } case let .image(representations, settings): if case .image(representations, settings) = rhs { return true @@ -122,7 +174,42 @@ public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { return false } case let .file(lhsId, _, lhsIsCreator, lhsIsDefault, lhsIsPattern, lhsIsDark, lhsSlug, lhsFile, lhsSettings): - if case let .file(rhsId, _, rhsIsCreator, rhsIsDefault, rhsIsPattern, rhsIsDark, rhsSlug, rhsFile, rhsSettings) = rhs, lhsId == rhsId, lhsIsCreator == rhsIsCreator, lhsIsDefault == rhsIsDefault, lhsIsPattern == rhsIsPattern, lhsIsDark == rhsIsDark, lhsSlug == rhsSlug, lhsFile == rhsFile, lhsSettings == rhsSettings { + if case let .file(rhsId, _, rhsIsCreator, rhsIsDefault, rhsIsPattern, rhsIsDark, rhsSlug, rhsFile, rhsSettings) = rhs, lhsId == rhsId, lhsIsCreator == rhsIsCreator, lhsIsDefault == rhsIsDefault, lhsIsPattern == rhsIsPattern, lhsIsDark == rhsIsDark, lhsSlug == rhsSlug, lhsFile.id == rhsFile.id, lhsSettings == rhsSettings { + return true + } else { + return false + } + } + } + + public func isBasicallyEqual(to wallpaper: TelegramWallpaper) -> Bool { + switch self { + case .builtin: + if case .builtin = wallpaper { + return true + } else { + return false + } + case let .color(color): + if case .color(color) = wallpaper { + return true + } else { + return false + } + case let .gradient(topColor, bottomColor, _): + if case .gradient(topColor, bottomColor, _) = wallpaper { + return true + } else { + return false + } + case let .image(representations, _): + if case .image(representations, _) = wallpaper { + return true + } else { + return false + } + case let .file(_, _, _, _, _, _, lhsSlug, _, lhsSettings): + if case let .file(_, _, _, _, _, _, rhsSlug, _, rhsSettings) = wallpaper, lhsSlug == rhsSlug, lhsSettings.color == rhsSettings.color && lhsSettings.intensity == rhsSettings.intensity { return true } else { return false @@ -132,7 +219,7 @@ public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { public var settings: WallpaperSettings? { switch self { - case let .builtin(settings), let .image(_, settings), let .file(_, _, _, _, _, _, _, _, settings): + case let .builtin(settings), let .gradient(_, _, settings), let .image(_, settings), let .file(_, _, _, _, _, _, _, _, settings): return settings default: return nil @@ -145,6 +232,8 @@ public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { return .builtin(settings) case .color: return self + case let .gradient(topColor, bottomColor, _): + return .gradient(topColor, bottomColor, settings) case let .image(representations, _): return .image(representations, settings) case let .file(id, accessHash, isCreator, isDefault, isPattern, isDark, slug, file, _): diff --git a/submodules/SyncCore/Sources/TextEntitiesMessageAttribute.swift b/submodules/SyncCore/Sources/TextEntitiesMessageAttribute.swift index 79fe68a5ed..e0c3f565a9 100644 --- a/submodules/SyncCore/Sources/TextEntitiesMessageAttribute.swift +++ b/submodules/SyncCore/Sources/TextEntitiesMessageAttribute.swift @@ -19,6 +19,7 @@ public enum MessageTextEntityType: Equatable { case Strikethrough case BlockQuote case Underline + case BankCard case Custom(type: CustomEntityType) } @@ -65,6 +66,8 @@ public struct MessageTextEntity: PostboxCoding, Equatable { self.type = .BlockQuote case 15: self.type = .Underline + case 16: + self.type = .BankCard case Int32.max: self.type = .Custom(type: decoder.decodeInt32ForKey("type", orElse: 0)) default: @@ -110,6 +113,8 @@ public struct MessageTextEntity: PostboxCoding, Equatable { encoder.encodeInt32(14, forKey: "_rawValue") case .Underline: encoder.encodeInt32(15, forKey: "_rawValue") + case .BankCard: + encoder.encodeInt32(16, forKey: "_rawValue") case let .Custom(type): encoder.encodeInt32(Int32.max, forKey: "_rawValue") encoder.encodeInt32(type, forKey: "type") diff --git a/submodules/TelegramAnimatedStickerNode/Sources/AnimatedStickerUtils.swift b/submodules/TelegramAnimatedStickerNode/Sources/AnimatedStickerUtils.swift index 01b15180e1..206574e171 100644 --- a/submodules/TelegramAnimatedStickerNode/Sources/AnimatedStickerUtils.swift +++ b/submodules/TelegramAnimatedStickerNode/Sources/AnimatedStickerUtils.swift @@ -183,13 +183,12 @@ private let threadPool: ThreadPool = { }() @available(iOS 9.0, *) -public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: CGSize, fitzModifier: EmojiFitzModifier? = nil, cacheKey: String) -> Signal { +public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: CGSize, fitzModifier: EmojiFitzModifier? = nil, cacheKey: String) -> Signal { return Signal({ subscriber in let cancelled = Atomic(value: false) threadPool.addTask(ThreadPoolTask({ _ in if cancelled.with({ $0 }) { - //print("cancelled 1") return } @@ -206,12 +205,6 @@ public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: C let endFrame = Int(player.frameCount) if cancelled.with({ $0 }) { - //print("cancelled 2") - return - } - - let path = NSTemporaryDirectory() + "\(arc4random64()).lz4v" - guard let fileContext = ManagedFile(queue: nil, path: path, mode: .readwrite) else { return } @@ -219,16 +212,36 @@ public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: C var currentFrame: Int32 = 0 + //let writeBuffer = WriteBuffer() + let tempFile = TempBox.shared.tempFile(fileName: "result.asticker") + guard let file = ManagedFile(queue: nil, path: tempFile.path, mode: .readwrite) else { + return + } + + func writeData(_ data: UnsafeRawPointer, length: Int) { + file.write(data, count: length) + } + + func commitData() { + } + + func completeWithCurrentResult() { + subscriber.putNext(.tempFile(tempFile)) + subscriber.putCompletion() + } + + var numberOfFramesCommitted = 0 + var fps: Int32 = player.frameRate var frameCount: Int32 = player.frameCount - let _ = fileContext.write(&fps, count: 4) - let _ = fileContext.write(&frameCount, count: 4) + writeData(&fps, length: 4) + writeData(&frameCount, length: 4) var widthValue: Int32 = Int32(size.width) var heightValue: Int32 = Int32(size.height) var bytesPerRowValue: Int32 = Int32(bytesPerRow) - let _ = fileContext.write(&widthValue, count: 4) - let _ = fileContext.write(&heightValue, count: 4) - let _ = fileContext.write(&bytesPerRowValue, count: 4) + writeData(&widthValue, length: 4) + writeData(&heightValue, length: 4) + writeData(&bytesPerRowValue, length: 4) let frameLength = bytesPerRow * Int(size.height) assert(frameLength % 16 == 0) @@ -262,7 +275,6 @@ public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: C while currentFrame < endFrame { if cancelled.with({ $0 }) { - //print("cancelled 3") return } @@ -298,8 +310,8 @@ public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: C compressedFrameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in let length = compression_encode_buffer(bytes, compressedFrameDataLength, previousYuvaFrameData.assumingMemoryBound(to: UInt8.self), yuvaLength, scratchData, COMPRESSION_LZFSE) var frameLengthValue: Int32 = Int32(length) - let _ = fileContext.write(&frameLengthValue, count: 4) - let _ = fileContext.write(bytes, count: length) + writeData(&frameLengthValue, length: 4) + writeData(bytes, length: length) } let tmp = previousYuvaFrameData @@ -309,16 +321,27 @@ public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: C compressionTime += CACurrentMediaTime() - compressionStartTime currentFrame += 1 + + numberOfFramesCommitted += 1 + + if numberOfFramesCommitted >= 5 { + numberOfFramesCommitted = 0 + + commitData() + } + } - subscriber.putNext(path) + commitData() + + completeWithCurrentResult() subscriber.putCompletion() - print("animation render time \(CACurrentMediaTime() - startTime)") + /*print("animation render time \(CACurrentMediaTime() - startTime)") print("of which drawing time \(drawingTime)") print("of which appending time \(appendingTime)") print("of which delta time \(deltaTime)") - print("of which compression time \(compressionTime)") + print("of which compression time \(compressionTime)")*/ } } })) diff --git a/submodules/TelegramAnimatedStickerNode/Sources/TelegramAnimatedStickerNode.swift b/submodules/TelegramAnimatedStickerNode/Sources/TelegramAnimatedStickerNode.swift index 7ac6fcf1f4..6947908e03 100644 --- a/submodules/TelegramAnimatedStickerNode/Sources/TelegramAnimatedStickerNode.swift +++ b/submodules/TelegramAnimatedStickerNode/Sources/TelegramAnimatedStickerNode.swift @@ -18,13 +18,13 @@ public final class AnimatedStickerResourceSource: AnimatedStickerNodeSource { self.fitzModifier = fitzModifier } - public func cachedDataPath(width: Int, height: Int) -> Signal { + public func cachedDataPath(width: Int, height: Int) -> Signal<(String, Bool), NoError> { return chatMessageAnimationData(postbox: self.account.postbox, resource: self.resource, fitzModifier: self.fitzModifier, width: width, height: height, synchronousLoad: false) |> filter { data in - return data.complete + return data.size != 0 } - |> map { data -> String in - return data.path + |> map { data -> (String, Bool) in + return (data.path, data.complete) } } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 11218fef0c..da72996990 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -11,7 +11,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-784000893] = { return Api.payments.ValidatedRequestedInfo.parse_validatedRequestedInfo($0) } dict[461151667] = { return Api.ChatFull.parse_chatFull($0) } dict[763976820] = { return Api.ChatFull.parse_channelFull($0) } - dict[1465219162] = { return Api.PollResults.parse_pollResults($0) } + dict[-932174686] = { return Api.PollResults.parse_pollResults($0) } dict[-925415106] = { return Api.ChatParticipant.parse_chatParticipant($0) } dict[-636267638] = { return Api.ChatParticipant.parse_chatParticipantCreator($0) } dict[-489233354] = { return Api.ChatParticipant.parse_chatParticipantAdmin($0) } @@ -52,6 +52,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-614138572] = { return Api.account.TmpPassword.parse_tmpPassword($0) } dict[-2103600678] = { return Api.SecureRequiredType.parse_secureRequiredType($0) } dict[41187252] = { return Api.SecureRequiredType.parse_secureRequiredTypeOneOf($0) } + dict[1654593920] = { return Api.auth.LoginToken.parse_loginToken($0) } + dict[110008598] = { return Api.auth.LoginToken.parse_loginTokenMigrateTo($0) } + dict[957176926] = { return Api.auth.LoginToken.parse_loginTokenSuccess($0) } dict[1064139624] = { return Api.JSONValue.parse_jsonNull($0) } dict[-952869270] = { return Api.JSONValue.parse_jsonBool($0) } dict[736157604] = { return Api.JSONValue.parse_jsonNumber($0) } @@ -68,7 +71,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1202287072] = { return Api.StatsURL.parse_statsURL($0) } dict[1516793212] = { return Api.ChatInvite.parse_chatInviteAlready($0) } dict[-540871282] = { return Api.ChatInvite.parse_chatInvite($0) } - dict[-767099577] = { return Api.AutoDownloadSettings.parse_autoDownloadSettings($0) } + dict[-532532493] = { return Api.AutoDownloadSettings.parse_autoDownloadSettings($0) } dict[1678812626] = { return Api.StickerSetCovered.parse_stickerSetCovered($0) } dict[872932635] = { return Api.StickerSetCovered.parse_stickerSetMultiCovered($0) } dict[1189204285] = { return Api.RecentMeUrl.parse_recentMeUrlUnknown($0) } @@ -105,7 +108,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1000708810] = { return Api.help.AppUpdate.parse_noAppUpdate($0) } dict[497489295] = { return Api.help.AppUpdate.parse_appUpdate($0) } dict[-209337866] = { return Api.LangPackDifference.parse_langPackDifference($0) } - dict[-1590738760] = { return Api.WallPaperSettings.parse_wallPaperSettings($0) } + dict[84438264] = { return Api.WallPaperSettings.parse_wallPaperSettings($0) } dict[1152191385] = { return Api.EmojiURL.parse_EmojiURL($0) } dict[-791039645] = { return Api.channels.ChannelParticipant.parse_channelParticipant($0) } dict[-1736378792] = { return Api.InputCheckPasswordSRP.parse_inputCheckPasswordEmpty($0) } @@ -125,6 +128,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1690108678] = { return Api.InputEncryptedFile.parse_inputEncryptedFileUploaded($0) } dict[1511503333] = { return Api.InputEncryptedFile.parse_inputEncryptedFile($0) } dict[767652808] = { return Api.InputEncryptedFile.parse_inputEncryptedFileBigUploaded($0) } + dict[-1456996667] = { return Api.messages.InactiveChats.parse_inactiveChats($0) } dict[1443858741] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedMessage($0) } dict[-1802240206] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedFile($0) } dict[1571494644] = { return Api.ExportedMessageLink.parse_exportedMessageLink($0) } @@ -240,6 +244,10 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[967122427] = { return Api.Update.parse_updateNewScheduledMessage($0) } dict[-1870238482] = { return Api.Update.parse_updateDeleteScheduledMessages($0) } dict[-2112423005] = { return Api.Update.parse_updateTheme($0) } + dict[-2027964103] = { return Api.Update.parse_updateGeoLiveViewed($0) } + dict[1448076945] = { return Api.Update.parse_updateLoginToken($0) } + dict[1123585836] = { return Api.Update.parse_updateMessagePollVote($0) } + dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) } dict[1558266229] = { return Api.PopularContact.parse_popularContact($0) } dict[-373643672] = { return Api.FolderPeer.parse_folderPeer($0) } dict[367766557] = { return Api.ChannelParticipant.parse_channelParticipant($0) } @@ -247,6 +255,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[470789295] = { return Api.ChannelParticipant.parse_channelParticipantBanned($0) } dict[-859915345] = { return Api.ChannelParticipant.parse_channelParticipantAdmin($0) } dict[-2138237532] = { return Api.ChannelParticipant.parse_channelParticipantCreator($0) } + dict[-1567730343] = { return Api.MessageUserVote.parse_messageUserVote($0) } + dict[909603888] = { return Api.MessageUserVote.parse_messageUserVoteInputOption($0) } + dict[244310238] = { return Api.MessageUserVote.parse_messageUserVoteMultiple($0) } dict[471043349] = { return Api.contacts.Blocked.parse_blocked($0) } dict[-1878523231] = { return Api.contacts.Blocked.parse_blockedSlice($0) } dict[-55902537] = { return Api.InputDialogPeer.parse_inputDialogPeer($0) } @@ -262,6 +273,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1344716869] = { return Api.KeyboardButton.parse_keyboardButtonBuy($0) } dict[280464681] = { return Api.KeyboardButton.parse_keyboardButtonUrlAuth($0) } dict[-802258988] = { return Api.KeyboardButton.parse_inputKeyboardButtonUrlAuth($0) } + dict[-1144565411] = { return Api.KeyboardButton.parse_keyboardButtonRequestPoll($0) } dict[-748155807] = { return Api.ContactStatus.parse_contactStatus($0) } dict[1679398724] = { return Api.SecureFile.parse_secureFileEmpty($0) } dict[-534283678] = { return Api.SecureFile.parse_secureFile($0) } @@ -299,7 +311,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-350980120] = { return Api.WebPage.parse_webPageEmpty($0) } dict[-981018084] = { return Api.WebPage.parse_webPagePending($0) } dict[-2054908813] = { return Api.WebPage.parse_webPageNotModified($0) } - dict[-94051982] = { return Api.WebPage.parse_webPage($0) } + dict[-392411726] = { return Api.WebPage.parse_webPage($0) } dict[1036876423] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageText($0) } dict[-190472735] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaGeo($0) } dict[1262639204] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageGame($0) } @@ -343,8 +355,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-440664550] = { return Api.InputMedia.parse_inputMediaPhotoExternal($0) } dict[-78455655] = { return Api.InputMedia.parse_inputMediaDocumentExternal($0) } dict[-122978821] = { return Api.InputMedia.parse_inputMediaContact($0) } - dict[112424539] = { return Api.InputMedia.parse_inputMediaPoll($0) } dict[-833715459] = { return Api.InputMedia.parse_inputMediaGeoLive($0) } + dict[-1410741723] = { return Api.InputMedia.parse_inputMediaPoll($0) } dict[2134579434] = { return Api.InputPeer.parse_inputPeerEmpty($0) } dict[2107670217] = { return Api.InputPeer.parse_inputPeerSelf($0) } dict[396093539] = { return Api.InputPeer.parse_inputPeerChat($0) } @@ -483,6 +495,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1141711456] = { return Api.SecurePasswordKdfAlgo.parse_securePasswordKdfAlgoPBKDF2HMACSHA512iter100000($0) } dict[-2042159726] = { return Api.SecurePasswordKdfAlgo.parse_securePasswordKdfAlgoSHA512($0) } dict[-1032140601] = { return Api.BotCommand.parse_botCommand($0) } + dict[1474462241] = { return Api.account.ContentSettings.parse_contentSettings($0) } dict[-2066640507] = { return Api.messages.AffectedMessages.parse_affectedMessages($0) } dict[-402498398] = { return Api.messages.SavedGifs.parse_savedGifsNotModified($0) } dict[772213157] = { return Api.messages.SavedGifs.parse_savedGifs($0) } @@ -495,6 +508,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1564789301] = { return Api.PhoneCallProtocol.parse_phoneCallProtocol($0) } dict[-1567175714] = { return Api.MessageFwdAuthor.parse_messageFwdAuthor($0) } dict[-1539849235] = { return Api.WallPaper.parse_wallPaper($0) } + dict[-1963717851] = { return Api.WallPaper.parse_wallPaperNoFile($0) } dict[-1938715001] = { return Api.messages.Messages.parse_messages($0) } dict[1951620897] = { return Api.messages.Messages.parse_messagesNotModified($0) } dict[-1725551049] = { return Api.messages.Messages.parse_channelMessages($0) } @@ -537,6 +551,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1739392570] = { return Api.DocumentAttribute.parse_documentAttributeAudio($0) } dict[358154344] = { return Api.DocumentAttribute.parse_documentAttributeFilename($0) } dict[-1744710921] = { return Api.DocumentAttribute.parse_documentAttributeHasStickers($0) } + dict[-177732982] = { return Api.BankCardOpenUrl.parse_bankCardOpenUrl($0) } dict[307276766] = { return Api.account.Authorizations.parse_authorizations($0) } dict[935395612] = { return Api.ChatPhoto.parse_chatPhotoEmpty($0) } dict[1197267925] = { return Api.ChatPhoto.parse_chatPhoto($0) } @@ -587,8 +602,11 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[2104790276] = { return Api.DataJSON.parse_dataJSON($0) } dict[-433014407] = { return Api.InputWallPaper.parse_inputWallPaper($0) } dict[1913199744] = { return Api.InputWallPaper.parse_inputWallPaperSlug($0) } + dict[-2077770836] = { return Api.InputWallPaper.parse_inputWallPaperNoFile($0) } + dict[-1118798639] = { return Api.InputThemeSettings.parse_inputThemeSettings($0) } dict[1251549527] = { return Api.InputStickeredMedia.parse_inputStickeredMediaPhoto($0) } dict[70813275] = { return Api.InputStickeredMedia.parse_inputStickeredMediaDocument($0) } + dict[1421174295] = { return Api.WebPageAttribute.parse_webPageAttributeTheme($0) } dict[82699215] = { return Api.messages.FeaturedStickers.parse_featuredStickersNotModified($0) } dict[-123893531] = { return Api.messages.FeaturedStickers.parse_featuredStickers($0) } dict[-2048646399] = { return Api.PhoneCallDiscardReason.parse_phoneCallDiscardReasonMissed($0) } @@ -612,6 +630,11 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1430205163] = { return Api.InputWebFileLocation.parse_inputWebFileGeoMessageLocation($0) } dict[-1625153079] = { return Api.InputWebFileLocation.parse_inputWebFileGeoPointLocation($0) } dict[-332168592] = { return Api.MessageFwdHeader.parse_messageFwdHeader($0) } + dict[-1012849566] = { return Api.BaseTheme.parse_baseThemeClassic($0) } + dict[-69724536] = { return Api.BaseTheme.parse_baseThemeDay($0) } + dict[-1212997976] = { return Api.BaseTheme.parse_baseThemeNight($0) } + dict[1834973166] = { return Api.BaseTheme.parse_baseThemeTinted($0) } + dict[1527845466] = { return Api.BaseTheme.parse_baseThemeArctic($0) } dict[398898678] = { return Api.help.Support.parse_support($0) } dict[1474492012] = { return Api.MessagesFilter.parse_inputMessagesFilterEmpty($0) } dict[-1777752804] = { return Api.MessagesFilter.parse_inputMessagesFilterPhotos($0) } @@ -656,6 +679,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-395967805] = { return Api.messages.AllStickers.parse_allStickersNotModified($0) } dict[-302170017] = { return Api.messages.AllStickers.parse_allStickers($0) } dict[-1655957568] = { return Api.PhoneConnection.parse_phoneConnection($0) } + dict[-206688531] = { return Api.help.UserInfo.parse_userInfoEmpty($0) } + dict[32192344] = { return Api.help.UserInfo.parse_userInfo($0) } dict[-1194283041] = { return Api.AccountDaysTTL.parse_accountDaysTTL($0) } dict[-1658158621] = { return Api.SecureValueType.parse_secureValueTypePersonalDetails($0) } dict[1034709504] = { return Api.SecureValueType.parse_secureValueTypePassport($0) } @@ -722,10 +747,10 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1363483106] = { return Api.DialogPeer.parse_dialogPeerFolder($0) } dict[-104284986] = { return Api.WebDocument.parse_webDocumentNoProxy($0) } dict[475467473] = { return Api.WebDocument.parse_webDocument($0) } - dict[1211967244] = { return Api.Theme.parse_themeDocumentNotModified($0) } - dict[-136770336] = { return Api.Theme.parse_theme($0) } + dict[42930452] = { return Api.Theme.parse_theme($0) } dict[-1290580579] = { return Api.contacts.Found.parse_found($0) } dict[-368018716] = { return Api.ChannelAdminLogEventsFilter.parse_channelAdminLogEventsFilter($0) } + dict[-1676371894] = { return Api.ThemeSettings.parse_themeSettings($0) } dict[1889961234] = { return Api.PeerNotifySettings.parse_peerNotifySettingsEmpty($0) } dict[-1353671392] = { return Api.PeerNotifySettings.parse_peerNotifySettings($0) } dict[-1995686519] = { return Api.InputBotInlineMessageID.parse_inputBotInlineMessageID($0) } @@ -758,6 +783,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1672577397] = { return Api.MessageEntity.parse_messageEntityUnderline($0) } dict[-1090087980] = { return Api.MessageEntity.parse_messageEntityStrike($0) } dict[34469328] = { return Api.MessageEntity.parse_messageEntityBlockquote($0) } + dict[1981704948] = { return Api.MessageEntity.parse_messageEntityBankCard($0) } dict[483901197] = { return Api.InputPhoto.parse_inputPhotoEmpty($0) } dict[1001634122] = { return Api.InputPhoto.parse_inputPhoto($0) } dict[-567906571] = { return Api.contacts.TopPeers.parse_topPeersNotModified($0) } @@ -775,11 +801,13 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-94974410] = { return Api.EncryptedChat.parse_encryptedChat($0) } dict[332848423] = { return Api.EncryptedChat.parse_encryptedChatDiscarded($0) } dict[-901375139] = { return Api.PeerLocated.parse_peerLocated($0) } + dict[-118740917] = { return Api.PeerLocated.parse_peerSelfLocated($0) } dict[922273905] = { return Api.Document.parse_documentEmpty($0) } dict[-1683841855] = { return Api.Document.parse_document($0) } dict[-1707344487] = { return Api.messages.HighScores.parse_highScores($0) } dict[-892779534] = { return Api.WebAuthorization.parse_webAuthorization($0) } dict[-805141448] = { return Api.ImportedContact.parse_importedContact($0) } + dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) } return dict }() @@ -861,6 +889,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.SecureRequiredType: _1.serialize(buffer, boxed) + case let _1 as Api.auth.LoginToken: + _1.serialize(buffer, boxed) case let _1 as Api.JSONValue: _1.serialize(buffer, boxed) case let _1 as Api.Photo: @@ -913,6 +943,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.InputEncryptedFile: _1.serialize(buffer, boxed) + case let _1 as Api.messages.InactiveChats: + _1.serialize(buffer, boxed) case let _1 as Api.messages.SentEncryptedMessage: _1.serialize(buffer, boxed) case let _1 as Api.ExportedMessageLink: @@ -937,12 +969,16 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.Update: _1.serialize(buffer, boxed) + case let _1 as Api.messages.VotesList: + _1.serialize(buffer, boxed) case let _1 as Api.PopularContact: _1.serialize(buffer, boxed) case let _1 as Api.FolderPeer: _1.serialize(buffer, boxed) case let _1 as Api.ChannelParticipant: _1.serialize(buffer, boxed) + case let _1 as Api.MessageUserVote: + _1.serialize(buffer, boxed) case let _1 as Api.contacts.Blocked: _1.serialize(buffer, boxed) case let _1 as Api.InputDialogPeer: @@ -1139,6 +1175,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.BotCommand: _1.serialize(buffer, boxed) + case let _1 as Api.account.ContentSettings: + _1.serialize(buffer, boxed) case let _1 as Api.messages.AffectedMessages: _1.serialize(buffer, boxed) case let _1 as Api.messages.SavedGifs: @@ -1179,6 +1217,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.DocumentAttribute: _1.serialize(buffer, boxed) + case let _1 as Api.BankCardOpenUrl: + _1.serialize(buffer, boxed) case let _1 as Api.account.Authorizations: _1.serialize(buffer, boxed) case let _1 as Api.ChatPhoto: @@ -1231,8 +1271,12 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.InputWallPaper: _1.serialize(buffer, boxed) + case let _1 as Api.InputThemeSettings: + _1.serialize(buffer, boxed) case let _1 as Api.InputStickeredMedia: _1.serialize(buffer, boxed) + case let _1 as Api.WebPageAttribute: + _1.serialize(buffer, boxed) case let _1 as Api.messages.FeaturedStickers: _1.serialize(buffer, boxed) case let _1 as Api.PhoneCallDiscardReason: @@ -1259,6 +1303,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.MessageFwdHeader: _1.serialize(buffer, boxed) + case let _1 as Api.BaseTheme: + _1.serialize(buffer, boxed) case let _1 as Api.help.Support: _1.serialize(buffer, boxed) case let _1 as Api.MessagesFilter: @@ -1291,6 +1337,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.PhoneConnection: _1.serialize(buffer, boxed) + case let _1 as Api.help.UserInfo: + _1.serialize(buffer, boxed) case let _1 as Api.AccountDaysTTL: _1.serialize(buffer, boxed) case let _1 as Api.SecureValueType: @@ -1323,6 +1371,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.ChannelAdminLogEventsFilter: _1.serialize(buffer, boxed) + case let _1 as Api.ThemeSettings: + _1.serialize(buffer, boxed) case let _1 as Api.PeerNotifySettings: _1.serialize(buffer, boxed) case let _1 as Api.InputBotInlineMessageID: @@ -1365,1641 +1415,11 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.ImportedContact: _1.serialize(buffer, boxed) + case let _1 as Api.payments.BankCardData: + _1.serialize(buffer, boxed) default: break } } } -public extension Api { -public struct messages { - public enum StickerSet: TypeConstructorDescription { - case stickerSet(set: Api.StickerSet, packs: [Api.StickerPack], documents: [Api.Document]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .stickerSet(let set, let packs, let documents): - if boxed { - buffer.appendInt32(-1240849242) - } - set.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(packs.count)) - for item in packs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(documents.count)) - for item in documents { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .stickerSet(let set, let packs, let documents): - return ("stickerSet", [("set", set), ("packs", packs), ("documents", documents)]) - } - } - - public static func parse_stickerSet(_ reader: BufferReader) -> StickerSet? { - var _1: Api.StickerSet? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StickerSet - } - var _2: [Api.StickerPack]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) - } - var _3: [Api.Document]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.StickerSet.stickerSet(set: _1!, packs: _2!, documents: _3!) - } - else { - return nil - } - } - - } - public enum ArchivedStickers: TypeConstructorDescription { - case archivedStickers(count: Int32, sets: [Api.StickerSetCovered]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .archivedStickers(let count, let sets): - if boxed { - buffer.appendInt32(1338747336) - } - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(sets.count)) - for item in sets { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .archivedStickers(let count, let sets): - return ("archivedStickers", [("count", count), ("sets", sets)]) - } - } - - public static func parse_archivedStickers(_ reader: BufferReader) -> ArchivedStickers? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.StickerSetCovered]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.ArchivedStickers.archivedStickers(count: _1!, sets: _2!) - } - else { - return nil - } - } - - } - public enum SentEncryptedMessage: TypeConstructorDescription { - case sentEncryptedMessage(date: Int32) - case sentEncryptedFile(date: Int32, file: Api.EncryptedFile) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .sentEncryptedMessage(let date): - if boxed { - buffer.appendInt32(1443858741) - } - serializeInt32(date, buffer: buffer, boxed: false) - break - case .sentEncryptedFile(let date, let file): - if boxed { - buffer.appendInt32(-1802240206) - } - serializeInt32(date, buffer: buffer, boxed: false) - file.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .sentEncryptedMessage(let date): - return ("sentEncryptedMessage", [("date", date)]) - case .sentEncryptedFile(let date, let file): - return ("sentEncryptedFile", [("date", date), ("file", file)]) - } - } - - public static func parse_sentEncryptedMessage(_ reader: BufferReader) -> SentEncryptedMessage? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.messages.SentEncryptedMessage.sentEncryptedMessage(date: _1!) - } - else { - return nil - } - } - public static func parse_sentEncryptedFile(_ reader: BufferReader) -> SentEncryptedMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.EncryptedFile? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.EncryptedFile - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.SentEncryptedMessage.sentEncryptedFile(date: _1!, file: _2!) - } - else { - return nil - } - } - - } - public enum Stickers: TypeConstructorDescription { - case stickersNotModified - case stickers(hash: Int32, stickers: [Api.Document]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .stickersNotModified: - if boxed { - buffer.appendInt32(-244016606) - } - - break - case .stickers(let hash, let stickers): - if boxed { - buffer.appendInt32(-463889475) - } - serializeInt32(hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(stickers.count)) - for item in stickers { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .stickersNotModified: - return ("stickersNotModified", []) - case .stickers(let hash, let stickers): - return ("stickers", [("hash", hash), ("stickers", stickers)]) - } - } - - public static func parse_stickersNotModified(_ reader: BufferReader) -> Stickers? { - return Api.messages.Stickers.stickersNotModified - } - public static func parse_stickers(_ reader: BufferReader) -> Stickers? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.Document]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.Stickers.stickers(hash: _1!, stickers: _2!) - } - else { - return nil - } - } - - } - public enum FoundStickerSets: TypeConstructorDescription { - case foundStickerSetsNotModified - case foundStickerSets(hash: Int32, sets: [Api.StickerSetCovered]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .foundStickerSetsNotModified: - if boxed { - buffer.appendInt32(223655517) - } - - break - case .foundStickerSets(let hash, let sets): - if boxed { - buffer.appendInt32(1359533640) - } - serializeInt32(hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(sets.count)) - for item in sets { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .foundStickerSetsNotModified: - return ("foundStickerSetsNotModified", []) - case .foundStickerSets(let hash, let sets): - return ("foundStickerSets", [("hash", hash), ("sets", sets)]) - } - } - - public static func parse_foundStickerSetsNotModified(_ reader: BufferReader) -> FoundStickerSets? { - return Api.messages.FoundStickerSets.foundStickerSetsNotModified - } - public static func parse_foundStickerSets(_ reader: BufferReader) -> FoundStickerSets? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.StickerSetCovered]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.FoundStickerSets.foundStickerSets(hash: _1!, sets: _2!) - } - else { - return nil - } - } - - } - public enum FoundGifs: TypeConstructorDescription { - case foundGifs(nextOffset: Int32, results: [Api.FoundGif]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .foundGifs(let nextOffset, let results): - if boxed { - buffer.appendInt32(1158290442) - } - serializeInt32(nextOffset, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(results.count)) - for item in results { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .foundGifs(let nextOffset, let results): - return ("foundGifs", [("nextOffset", nextOffset), ("results", results)]) - } - } - - public static func parse_foundGifs(_ reader: BufferReader) -> FoundGifs? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.FoundGif]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.FoundGif.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.FoundGifs.foundGifs(nextOffset: _1!, results: _2!) - } - else { - return nil - } - } - - } - public enum BotResults: TypeConstructorDescription { - case botResults(flags: Int32, queryId: Int64, nextOffset: String?, switchPm: Api.InlineBotSwitchPM?, results: [Api.BotInlineResult], cacheTime: Int32, users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .botResults(let flags, let queryId, let nextOffset, let switchPm, let results, let cacheTime, let users): - if boxed { - buffer.appendInt32(-1803769784) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt64(queryId, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 2) != 0 {switchPm!.serialize(buffer, true)} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(results.count)) - for item in results { - item.serialize(buffer, true) - } - serializeInt32(cacheTime, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .botResults(let flags, let queryId, let nextOffset, let switchPm, let results, let cacheTime, let users): - return ("botResults", [("flags", flags), ("queryId", queryId), ("nextOffset", nextOffset), ("switchPm", switchPm), ("results", results), ("cacheTime", cacheTime), ("users", users)]) - } - } - - public static func parse_botResults(_ reader: BufferReader) -> BotResults? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: String? - if Int(_1!) & Int(1 << 1) != 0 {_3 = parseString(reader) } - var _4: Api.InlineBotSwitchPM? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.InlineBotSwitchPM - } } - var _5: [Api.BotInlineResult]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotInlineResult.self) - } - var _6: Int32? - _6 = reader.readInt32() - var _7: [Api.User]? - if let _ = reader.readInt32() { - _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.messages.BotResults.botResults(flags: _1!, queryId: _2!, nextOffset: _3, switchPm: _4, results: _5!, cacheTime: _6!, users: _7!) - } - else { - return nil - } - } - - } - public enum BotCallbackAnswer: TypeConstructorDescription { - case botCallbackAnswer(flags: Int32, message: String?, url: String?, cacheTime: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .botCallbackAnswer(let flags, let message, let url, let cacheTime): - if boxed { - buffer.appendInt32(911761060) - } - serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeString(message!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 2) != 0 {serializeString(url!, buffer: buffer, boxed: false)} - serializeInt32(cacheTime, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .botCallbackAnswer(let flags, let message, let url, let cacheTime): - return ("botCallbackAnswer", [("flags", flags), ("message", message), ("url", url), ("cacheTime", cacheTime)]) - } - } - - public static func parse_botCallbackAnswer(_ reader: BufferReader) -> BotCallbackAnswer? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) } - var _3: String? - if Int(_1!) & Int(1 << 2) != 0 {_3 = parseString(reader) } - var _4: Int32? - _4 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.messages.BotCallbackAnswer.botCallbackAnswer(flags: _1!, message: _2, url: _3, cacheTime: _4!) - } - else { - return nil - } - } - - } - public enum Chats: TypeConstructorDescription { - case chats(chats: [Api.Chat]) - case chatsSlice(count: Int32, chats: [Api.Chat]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .chats(let chats): - if boxed { - buffer.appendInt32(1694474197) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - break - case .chatsSlice(let count, let chats): - if boxed { - buffer.appendInt32(-1663561404) - } - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .chats(let chats): - return ("chats", [("chats", chats)]) - case .chatsSlice(let count, let chats): - return ("chatsSlice", [("count", count), ("chats", chats)]) - } - } - - public static func parse_chats(_ reader: BufferReader) -> Chats? { - var _1: [Api.Chat]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - let _c1 = _1 != nil - if _c1 { - return Api.messages.Chats.chats(chats: _1!) - } - else { - return nil - } - } - public static func parse_chatsSlice(_ reader: BufferReader) -> Chats? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.Chats.chatsSlice(count: _1!, chats: _2!) - } - else { - return nil - } - } - - } - public enum DhConfig: TypeConstructorDescription { - case dhConfigNotModified(random: Buffer) - case dhConfig(g: Int32, p: Buffer, version: Int32, random: Buffer) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .dhConfigNotModified(let random): - if boxed { - buffer.appendInt32(-1058912715) - } - serializeBytes(random, buffer: buffer, boxed: false) - break - case .dhConfig(let g, let p, let version, let random): - if boxed { - buffer.appendInt32(740433629) - } - serializeInt32(g, buffer: buffer, boxed: false) - serializeBytes(p, buffer: buffer, boxed: false) - serializeInt32(version, buffer: buffer, boxed: false) - serializeBytes(random, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .dhConfigNotModified(let random): - return ("dhConfigNotModified", [("random", random)]) - case .dhConfig(let g, let p, let version, let random): - return ("dhConfig", [("g", g), ("p", p), ("version", version), ("random", random)]) - } - } - - public static func parse_dhConfigNotModified(_ reader: BufferReader) -> DhConfig? { - var _1: Buffer? - _1 = parseBytes(reader) - let _c1 = _1 != nil - if _c1 { - return Api.messages.DhConfig.dhConfigNotModified(random: _1!) - } - else { - return nil - } - } - public static func parse_dhConfig(_ reader: BufferReader) -> DhConfig? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Buffer? - _2 = parseBytes(reader) - var _3: Int32? - _3 = reader.readInt32() - var _4: Buffer? - _4 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.messages.DhConfig.dhConfig(g: _1!, p: _2!, version: _3!, random: _4!) - } - else { - return nil - } - } - - } - public enum AffectedHistory: TypeConstructorDescription { - case affectedHistory(pts: Int32, ptsCount: Int32, offset: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .affectedHistory(let pts, let ptsCount, let offset): - if boxed { - buffer.appendInt32(-1269012015) - } - serializeInt32(pts, buffer: buffer, boxed: false) - serializeInt32(ptsCount, buffer: buffer, boxed: false) - serializeInt32(offset, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .affectedHistory(let pts, let ptsCount, let offset): - return ("affectedHistory", [("pts", pts), ("ptsCount", ptsCount), ("offset", offset)]) - } - } - - public static func parse_affectedHistory(_ reader: BufferReader) -> AffectedHistory? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.AffectedHistory.affectedHistory(pts: _1!, ptsCount: _2!, offset: _3!) - } - else { - return nil - } - } - - } - public enum MessageEditData: TypeConstructorDescription { - case messageEditData(flags: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .messageEditData(let flags): - if boxed { - buffer.appendInt32(649453030) - } - serializeInt32(flags, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .messageEditData(let flags): - return ("messageEditData", [("flags", flags)]) - } - } - - public static func parse_messageEditData(_ reader: BufferReader) -> MessageEditData? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.messages.MessageEditData.messageEditData(flags: _1!) - } - else { - return nil - } - } - - } - public enum ChatFull: TypeConstructorDescription { - case chatFull(fullChat: Api.ChatFull, chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .chatFull(let fullChat, let chats, let users): - if boxed { - buffer.appendInt32(-438840932) - } - fullChat.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .chatFull(let fullChat, let chats, let users): - return ("chatFull", [("fullChat", fullChat), ("chats", chats), ("users", users)]) - } - } - - public static func parse_chatFull(_ reader: BufferReader) -> ChatFull? { - var _1: Api.ChatFull? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.ChatFull - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.ChatFull.chatFull(fullChat: _1!, chats: _2!, users: _3!) - } - else { - return nil - } - } - - } - public enum SearchCounter: TypeConstructorDescription { - case searchCounter(flags: Int32, filter: Api.MessagesFilter, count: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .searchCounter(let flags, let filter, let count): - if boxed { - buffer.appendInt32(-398136321) - } - serializeInt32(flags, buffer: buffer, boxed: false) - filter.serialize(buffer, true) - serializeInt32(count, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .searchCounter(let flags, let filter, let count): - return ("searchCounter", [("flags", flags), ("filter", filter), ("count", count)]) - } - } - - public static func parse_searchCounter(_ reader: BufferReader) -> SearchCounter? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.MessagesFilter? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.MessagesFilter - } - var _3: Int32? - _3 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.SearchCounter.searchCounter(flags: _1!, filter: _2!, count: _3!) - } - else { - return nil - } - } - - } - public enum StickerSetInstallResult: TypeConstructorDescription { - case stickerSetInstallResultSuccess - case stickerSetInstallResultArchive(sets: [Api.StickerSetCovered]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .stickerSetInstallResultSuccess: - if boxed { - buffer.appendInt32(946083368) - } - - break - case .stickerSetInstallResultArchive(let sets): - if boxed { - buffer.appendInt32(904138920) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(sets.count)) - for item in sets { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .stickerSetInstallResultSuccess: - return ("stickerSetInstallResultSuccess", []) - case .stickerSetInstallResultArchive(let sets): - return ("stickerSetInstallResultArchive", [("sets", sets)]) - } - } - - public static func parse_stickerSetInstallResultSuccess(_ reader: BufferReader) -> StickerSetInstallResult? { - return Api.messages.StickerSetInstallResult.stickerSetInstallResultSuccess - } - public static func parse_stickerSetInstallResultArchive(_ reader: BufferReader) -> StickerSetInstallResult? { - var _1: [Api.StickerSetCovered]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) - } - let _c1 = _1 != nil - if _c1 { - return Api.messages.StickerSetInstallResult.stickerSetInstallResultArchive(sets: _1!) - } - else { - return nil - } - } - - } - public enum AffectedMessages: TypeConstructorDescription { - case affectedMessages(pts: Int32, ptsCount: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .affectedMessages(let pts, let ptsCount): - if boxed { - buffer.appendInt32(-2066640507) - } - serializeInt32(pts, buffer: buffer, boxed: false) - serializeInt32(ptsCount, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .affectedMessages(let pts, let ptsCount): - return ("affectedMessages", [("pts", pts), ("ptsCount", ptsCount)]) - } - } - - public static func parse_affectedMessages(_ reader: BufferReader) -> AffectedMessages? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.AffectedMessages.affectedMessages(pts: _1!, ptsCount: _2!) - } - else { - return nil - } - } - - } - public enum SavedGifs: TypeConstructorDescription { - case savedGifsNotModified - case savedGifs(hash: Int32, gifs: [Api.Document]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .savedGifsNotModified: - if boxed { - buffer.appendInt32(-402498398) - } - - break - case .savedGifs(let hash, let gifs): - if boxed { - buffer.appendInt32(772213157) - } - serializeInt32(hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(gifs.count)) - for item in gifs { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .savedGifsNotModified: - return ("savedGifsNotModified", []) - case .savedGifs(let hash, let gifs): - return ("savedGifs", [("hash", hash), ("gifs", gifs)]) - } - } - - public static func parse_savedGifsNotModified(_ reader: BufferReader) -> SavedGifs? { - return Api.messages.SavedGifs.savedGifsNotModified - } - public static func parse_savedGifs(_ reader: BufferReader) -> SavedGifs? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.Document]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.SavedGifs.savedGifs(hash: _1!, gifs: _2!) - } - else { - return nil - } - } - - } - public enum Messages: TypeConstructorDescription { - case messages(messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) - case messagesNotModified(count: Int32) - case channelMessages(flags: Int32, pts: Int32, count: Int32, messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) - case messagesSlice(flags: Int32, count: Int32, nextRate: Int32?, messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .messages(let messages, let chats, let users): - if boxed { - buffer.appendInt32(-1938715001) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messages.count)) - for item in messages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - case .messagesNotModified(let count): - if boxed { - buffer.appendInt32(1951620897) - } - serializeInt32(count, buffer: buffer, boxed: false) - break - case .channelMessages(let flags, let pts, let count, let messages, let chats, let users): - if boxed { - buffer.appendInt32(-1725551049) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(pts, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messages.count)) - for item in messages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - case .messagesSlice(let flags, let count, let nextRate, let messages, let chats, let users): - if boxed { - buffer.appendInt32(-923939298) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeInt32(nextRate!, buffer: buffer, boxed: false)} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messages.count)) - for item in messages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .messages(let messages, let chats, let users): - return ("messages", [("messages", messages), ("chats", chats), ("users", users)]) - case .messagesNotModified(let count): - return ("messagesNotModified", [("count", count)]) - case .channelMessages(let flags, let pts, let count, let messages, let chats, let users): - return ("channelMessages", [("flags", flags), ("pts", pts), ("count", count), ("messages", messages), ("chats", chats), ("users", users)]) - case .messagesSlice(let flags, let count, let nextRate, let messages, let chats, let users): - return ("messagesSlice", [("flags", flags), ("count", count), ("nextRate", nextRate), ("messages", messages), ("chats", chats), ("users", users)]) - } - } - - public static func parse_messages(_ reader: BufferReader) -> Messages? { - var _1: [Api.Message]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.Messages.messages(messages: _1!, chats: _2!, users: _3!) - } - else { - return nil - } - } - public static func parse_messagesNotModified(_ reader: BufferReader) -> Messages? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.messages.Messages.messagesNotModified(count: _1!) - } - else { - return nil - } - } - public static func parse_channelMessages(_ reader: BufferReader) -> Messages? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: [Api.Message]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _5: [Api.Chat]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _6: [Api.User]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.messages.Messages.channelMessages(flags: _1!, pts: _2!, count: _3!, messages: _4!, chats: _5!, users: _6!) - } - else { - return nil - } - } - public static func parse_messagesSlice(_ reader: BufferReader) -> Messages? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt32() } - var _4: [Api.Message]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _5: [Api.Chat]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _6: [Api.User]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.messages.Messages.messagesSlice(flags: _1!, count: _2!, nextRate: _3, messages: _4!, chats: _5!, users: _6!) - } - else { - return nil - } - } - - } - public enum PeerDialogs: TypeConstructorDescription { - case peerDialogs(dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User], state: Api.updates.State) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .peerDialogs(let dialogs, let messages, let chats, let users, let state): - if boxed { - buffer.appendInt32(863093588) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(dialogs.count)) - for item in dialogs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messages.count)) - for item in messages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - state.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .peerDialogs(let dialogs, let messages, let chats, let users, let state): - return ("peerDialogs", [("dialogs", dialogs), ("messages", messages), ("chats", chats), ("users", users), ("state", state)]) - } - } - - public static func parse_peerDialogs(_ reader: BufferReader) -> PeerDialogs? { - var _1: [Api.Dialog]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Dialog.self) - } - var _2: [Api.Message]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _3: [Api.Chat]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _4: [Api.User]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - var _5: Api.updates.State? - if let signature = reader.readInt32() { - _5 = Api.parse(reader, signature: signature) as? Api.updates.State - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.messages.PeerDialogs.peerDialogs(dialogs: _1!, messages: _2!, chats: _3!, users: _4!, state: _5!) - } - else { - return nil - } - } - - } - public enum RecentStickers: TypeConstructorDescription { - case recentStickersNotModified - case recentStickers(hash: Int32, packs: [Api.StickerPack], stickers: [Api.Document], dates: [Int32]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .recentStickersNotModified: - if boxed { - buffer.appendInt32(186120336) - } - - break - case .recentStickers(let hash, let packs, let stickers, let dates): - if boxed { - buffer.appendInt32(586395571) - } - serializeInt32(hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(packs.count)) - for item in packs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(stickers.count)) - for item in stickers { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(dates.count)) - for item in dates { - serializeInt32(item, buffer: buffer, boxed: false) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .recentStickersNotModified: - return ("recentStickersNotModified", []) - case .recentStickers(let hash, let packs, let stickers, let dates): - return ("recentStickers", [("hash", hash), ("packs", packs), ("stickers", stickers), ("dates", dates)]) - } - } - - public static func parse_recentStickersNotModified(_ reader: BufferReader) -> RecentStickers? { - return Api.messages.RecentStickers.recentStickersNotModified - } - public static func parse_recentStickers(_ reader: BufferReader) -> RecentStickers? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.StickerPack]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) - } - var _3: [Api.Document]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - var _4: [Int32]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.messages.RecentStickers.recentStickers(hash: _1!, packs: _2!, stickers: _3!, dates: _4!) - } - else { - return nil - } - } - - } - public enum FeaturedStickers: TypeConstructorDescription { - case featuredStickersNotModified - case featuredStickers(hash: Int32, sets: [Api.StickerSetCovered], unread: [Int64]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .featuredStickersNotModified: - if boxed { - buffer.appendInt32(82699215) - } - - break - case .featuredStickers(let hash, let sets, let unread): - if boxed { - buffer.appendInt32(-123893531) - } - serializeInt32(hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(sets.count)) - for item in sets { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(unread.count)) - for item in unread { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .featuredStickersNotModified: - return ("featuredStickersNotModified", []) - case .featuredStickers(let hash, let sets, let unread): - return ("featuredStickers", [("hash", hash), ("sets", sets), ("unread", unread)]) - } - } - - public static func parse_featuredStickersNotModified(_ reader: BufferReader) -> FeaturedStickers? { - return Api.messages.FeaturedStickers.featuredStickersNotModified - } - public static func parse_featuredStickers(_ reader: BufferReader) -> FeaturedStickers? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.StickerSetCovered]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) - } - var _3: [Int64]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.FeaturedStickers.featuredStickers(hash: _1!, sets: _2!, unread: _3!) - } - else { - return nil - } - } - - } - public enum Dialogs: TypeConstructorDescription { - case dialogs(dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) - case dialogsSlice(count: Int32, dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) - case dialogsNotModified(count: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .dialogs(let dialogs, let messages, let chats, let users): - if boxed { - buffer.appendInt32(364538944) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(dialogs.count)) - for item in dialogs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messages.count)) - for item in messages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - case .dialogsSlice(let count, let dialogs, let messages, let chats, let users): - if boxed { - buffer.appendInt32(1910543603) - } - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(dialogs.count)) - for item in dialogs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messages.count)) - for item in messages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - case .dialogsNotModified(let count): - if boxed { - buffer.appendInt32(-253500010) - } - serializeInt32(count, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .dialogs(let dialogs, let messages, let chats, let users): - return ("dialogs", [("dialogs", dialogs), ("messages", messages), ("chats", chats), ("users", users)]) - case .dialogsSlice(let count, let dialogs, let messages, let chats, let users): - return ("dialogsSlice", [("count", count), ("dialogs", dialogs), ("messages", messages), ("chats", chats), ("users", users)]) - case .dialogsNotModified(let count): - return ("dialogsNotModified", [("count", count)]) - } - } - - public static func parse_dialogs(_ reader: BufferReader) -> Dialogs? { - var _1: [Api.Dialog]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Dialog.self) - } - var _2: [Api.Message]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _3: [Api.Chat]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _4: [Api.User]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.messages.Dialogs.dialogs(dialogs: _1!, messages: _2!, chats: _3!, users: _4!) - } - else { - return nil - } - } - public static func parse_dialogsSlice(_ reader: BufferReader) -> Dialogs? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.Dialog]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Dialog.self) - } - var _3: [Api.Message]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _4: [Api.Chat]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.messages.Dialogs.dialogsSlice(count: _1!, dialogs: _2!, messages: _3!, chats: _4!, users: _5!) - } - else { - return nil - } - } - public static func parse_dialogsNotModified(_ reader: BufferReader) -> Dialogs? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.messages.Dialogs.dialogsNotModified(count: _1!) - } - else { - return nil - } - } - - } - public enum FavedStickers: TypeConstructorDescription { - case favedStickersNotModified - case favedStickers(hash: Int32, packs: [Api.StickerPack], stickers: [Api.Document]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .favedStickersNotModified: - if boxed { - buffer.appendInt32(-1634752813) - } - - break - case .favedStickers(let hash, let packs, let stickers): - if boxed { - buffer.appendInt32(-209768682) - } - serializeInt32(hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(packs.count)) - for item in packs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(stickers.count)) - for item in stickers { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .favedStickersNotModified: - return ("favedStickersNotModified", []) - case .favedStickers(let hash, let packs, let stickers): - return ("favedStickers", [("hash", hash), ("packs", packs), ("stickers", stickers)]) - } - } - - public static func parse_favedStickersNotModified(_ reader: BufferReader) -> FavedStickers? { - return Api.messages.FavedStickers.favedStickersNotModified - } - public static func parse_favedStickers(_ reader: BufferReader) -> FavedStickers? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.StickerPack]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) - } - var _3: [Api.Document]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.FavedStickers.favedStickers(hash: _1!, packs: _2!, stickers: _3!) - } - else { - return nil - } - } - - } - public enum AllStickers: TypeConstructorDescription { - case allStickersNotModified - case allStickers(hash: Int32, sets: [Api.StickerSet]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .allStickersNotModified: - if boxed { - buffer.appendInt32(-395967805) - } - - break - case .allStickers(let hash, let sets): - if boxed { - buffer.appendInt32(-302170017) - } - serializeInt32(hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(sets.count)) - for item in sets { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .allStickersNotModified: - return ("allStickersNotModified", []) - case .allStickers(let hash, let sets): - return ("allStickers", [("hash", hash), ("sets", sets)]) - } - } - - public static func parse_allStickersNotModified(_ reader: BufferReader) -> AllStickers? { - return Api.messages.AllStickers.allStickersNotModified - } - public static func parse_allStickers(_ reader: BufferReader) -> AllStickers? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.StickerSet]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSet.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.AllStickers.allStickers(hash: _1!, sets: _2!) - } - else { - return nil - } - } - - } - public enum HighScores: TypeConstructorDescription { - case highScores(scores: [Api.HighScore], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .highScores(let scores, let users): - if boxed { - buffer.appendInt32(-1707344487) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(scores.count)) - for item in scores { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .highScores(let scores, let users): - return ("highScores", [("scores", scores), ("users", users)]) - } - } - - public static func parse_highScores(_ reader: BufferReader) -> HighScores? { - var _1: [Api.HighScore]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.HighScore.self) - } - var _2: [Api.User]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.HighScores.highScores(scores: _1!, users: _2!) - } - else { - return nil - } - } - - } -} -} diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index c564b24423..be520b8f94 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -1,3 +1,1757 @@ +public extension Api { +public struct messages { + public enum StickerSet: TypeConstructorDescription { + case stickerSet(set: Api.StickerSet, packs: [Api.StickerPack], documents: [Api.Document]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .stickerSet(let set, let packs, let documents): + if boxed { + buffer.appendInt32(-1240849242) + } + set.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(packs.count)) + for item in packs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(documents.count)) + for item in documents { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .stickerSet(let set, let packs, let documents): + return ("stickerSet", [("set", set), ("packs", packs), ("documents", documents)]) + } + } + + public static func parse_stickerSet(_ reader: BufferReader) -> StickerSet? { + var _1: Api.StickerSet? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StickerSet + } + var _2: [Api.StickerPack]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) + } + var _3: [Api.Document]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.StickerSet.stickerSet(set: _1!, packs: _2!, documents: _3!) + } + else { + return nil + } + } + + } + public enum ArchivedStickers: TypeConstructorDescription { + case archivedStickers(count: Int32, sets: [Api.StickerSetCovered]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .archivedStickers(let count, let sets): + if boxed { + buffer.appendInt32(1338747336) + } + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sets.count)) + for item in sets { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .archivedStickers(let count, let sets): + return ("archivedStickers", [("count", count), ("sets", sets)]) + } + } + + public static func parse_archivedStickers(_ reader: BufferReader) -> ArchivedStickers? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.StickerSetCovered]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.ArchivedStickers.archivedStickers(count: _1!, sets: _2!) + } + else { + return nil + } + } + + } + public enum InactiveChats: TypeConstructorDescription { + case inactiveChats(dates: [Int32], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inactiveChats(let dates, let chats, let users): + if boxed { + buffer.appendInt32(-1456996667) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(dates.count)) + for item in dates { + serializeInt32(item, buffer: buffer, boxed: false) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inactiveChats(let dates, let chats, let users): + return ("inactiveChats", [("dates", dates), ("chats", chats), ("users", users)]) + } + } + + public static func parse_inactiveChats(_ reader: BufferReader) -> InactiveChats? { + var _1: [Int32]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.InactiveChats.inactiveChats(dates: _1!, chats: _2!, users: _3!) + } + else { + return nil + } + } + + } + public enum SentEncryptedMessage: TypeConstructorDescription { + case sentEncryptedMessage(date: Int32) + case sentEncryptedFile(date: Int32, file: Api.EncryptedFile) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .sentEncryptedMessage(let date): + if boxed { + buffer.appendInt32(1443858741) + } + serializeInt32(date, buffer: buffer, boxed: false) + break + case .sentEncryptedFile(let date, let file): + if boxed { + buffer.appendInt32(-1802240206) + } + serializeInt32(date, buffer: buffer, boxed: false) + file.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .sentEncryptedMessage(let date): + return ("sentEncryptedMessage", [("date", date)]) + case .sentEncryptedFile(let date, let file): + return ("sentEncryptedFile", [("date", date), ("file", file)]) + } + } + + public static func parse_sentEncryptedMessage(_ reader: BufferReader) -> SentEncryptedMessage? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.messages.SentEncryptedMessage.sentEncryptedMessage(date: _1!) + } + else { + return nil + } + } + public static func parse_sentEncryptedFile(_ reader: BufferReader) -> SentEncryptedMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.EncryptedFile? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.EncryptedFile + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.SentEncryptedMessage.sentEncryptedFile(date: _1!, file: _2!) + } + else { + return nil + } + } + + } + public enum VotesList: TypeConstructorDescription { + case votesList(flags: Int32, count: Int32, votes: [Api.MessageUserVote], users: [Api.User], nextOffset: String?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .votesList(let flags, let count, let votes, let users, let nextOffset): + if boxed { + buffer.appendInt32(136574537) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(votes.count)) + for item in votes { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .votesList(let flags, let count, let votes, let users, let nextOffset): + return ("votesList", [("flags", flags), ("count", count), ("votes", votes), ("users", users), ("nextOffset", nextOffset)]) + } + } + + public static func parse_votesList(_ reader: BufferReader) -> VotesList? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.MessageUserVote]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageUserVote.self) + } + var _4: [Api.User]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _5: String? + if Int(_1!) & Int(1 << 0) != 0 {_5 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.messages.VotesList.votesList(flags: _1!, count: _2!, votes: _3!, users: _4!, nextOffset: _5) + } + else { + return nil + } + } + + } + public enum Stickers: TypeConstructorDescription { + case stickersNotModified + case stickers(hash: Int32, stickers: [Api.Document]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .stickersNotModified: + if boxed { + buffer.appendInt32(-244016606) + } + + break + case .stickers(let hash, let stickers): + if boxed { + buffer.appendInt32(-463889475) + } + serializeInt32(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(stickers.count)) + for item in stickers { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .stickersNotModified: + return ("stickersNotModified", []) + case .stickers(let hash, let stickers): + return ("stickers", [("hash", hash), ("stickers", stickers)]) + } + } + + public static func parse_stickersNotModified(_ reader: BufferReader) -> Stickers? { + return Api.messages.Stickers.stickersNotModified + } + public static func parse_stickers(_ reader: BufferReader) -> Stickers? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.Document]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.Stickers.stickers(hash: _1!, stickers: _2!) + } + else { + return nil + } + } + + } + public enum FoundStickerSets: TypeConstructorDescription { + case foundStickerSetsNotModified + case foundStickerSets(hash: Int32, sets: [Api.StickerSetCovered]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .foundStickerSetsNotModified: + if boxed { + buffer.appendInt32(223655517) + } + + break + case .foundStickerSets(let hash, let sets): + if boxed { + buffer.appendInt32(1359533640) + } + serializeInt32(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sets.count)) + for item in sets { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .foundStickerSetsNotModified: + return ("foundStickerSetsNotModified", []) + case .foundStickerSets(let hash, let sets): + return ("foundStickerSets", [("hash", hash), ("sets", sets)]) + } + } + + public static func parse_foundStickerSetsNotModified(_ reader: BufferReader) -> FoundStickerSets? { + return Api.messages.FoundStickerSets.foundStickerSetsNotModified + } + public static func parse_foundStickerSets(_ reader: BufferReader) -> FoundStickerSets? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.StickerSetCovered]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.FoundStickerSets.foundStickerSets(hash: _1!, sets: _2!) + } + else { + return nil + } + } + + } + public enum FoundGifs: TypeConstructorDescription { + case foundGifs(nextOffset: Int32, results: [Api.FoundGif]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .foundGifs(let nextOffset, let results): + if boxed { + buffer.appendInt32(1158290442) + } + serializeInt32(nextOffset, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(results.count)) + for item in results { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .foundGifs(let nextOffset, let results): + return ("foundGifs", [("nextOffset", nextOffset), ("results", results)]) + } + } + + public static func parse_foundGifs(_ reader: BufferReader) -> FoundGifs? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.FoundGif]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.FoundGif.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.FoundGifs.foundGifs(nextOffset: _1!, results: _2!) + } + else { + return nil + } + } + + } + public enum BotResults: TypeConstructorDescription { + case botResults(flags: Int32, queryId: Int64, nextOffset: String?, switchPm: Api.InlineBotSwitchPM?, results: [Api.BotInlineResult], cacheTime: Int32, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .botResults(let flags, let queryId, let nextOffset, let switchPm, let results, let cacheTime, let users): + if boxed { + buffer.appendInt32(-1803769784) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(queryId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {switchPm!.serialize(buffer, true)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(results.count)) + for item in results { + item.serialize(buffer, true) + } + serializeInt32(cacheTime, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .botResults(let flags, let queryId, let nextOffset, let switchPm, let results, let cacheTime, let users): + return ("botResults", [("flags", flags), ("queryId", queryId), ("nextOffset", nextOffset), ("switchPm", switchPm), ("results", results), ("cacheTime", cacheTime), ("users", users)]) + } + } + + public static func parse_botResults(_ reader: BufferReader) -> BotResults? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: String? + if Int(_1!) & Int(1 << 1) != 0 {_3 = parseString(reader) } + var _4: Api.InlineBotSwitchPM? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.InlineBotSwitchPM + } } + var _5: [Api.BotInlineResult]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotInlineResult.self) + } + var _6: Int32? + _6 = reader.readInt32() + var _7: [Api.User]? + if let _ = reader.readInt32() { + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.messages.BotResults.botResults(flags: _1!, queryId: _2!, nextOffset: _3, switchPm: _4, results: _5!, cacheTime: _6!, users: _7!) + } + else { + return nil + } + } + + } + public enum BotCallbackAnswer: TypeConstructorDescription { + case botCallbackAnswer(flags: Int32, message: String?, url: String?, cacheTime: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .botCallbackAnswer(let flags, let message, let url, let cacheTime): + if boxed { + buffer.appendInt32(911761060) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(message!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeString(url!, buffer: buffer, boxed: false)} + serializeInt32(cacheTime, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .botCallbackAnswer(let flags, let message, let url, let cacheTime): + return ("botCallbackAnswer", [("flags", flags), ("message", message), ("url", url), ("cacheTime", cacheTime)]) + } + } + + public static func parse_botCallbackAnswer(_ reader: BufferReader) -> BotCallbackAnswer? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) } + var _3: String? + if Int(_1!) & Int(1 << 2) != 0 {_3 = parseString(reader) } + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.messages.BotCallbackAnswer.botCallbackAnswer(flags: _1!, message: _2, url: _3, cacheTime: _4!) + } + else { + return nil + } + } + + } + public enum Chats: TypeConstructorDescription { + case chats(chats: [Api.Chat]) + case chatsSlice(count: Int32, chats: [Api.Chat]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .chats(let chats): + if boxed { + buffer.appendInt32(1694474197) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + break + case .chatsSlice(let count, let chats): + if boxed { + buffer.appendInt32(-1663561404) + } + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .chats(let chats): + return ("chats", [("chats", chats)]) + case .chatsSlice(let count, let chats): + return ("chatsSlice", [("count", count), ("chats", chats)]) + } + } + + public static func parse_chats(_ reader: BufferReader) -> Chats? { + var _1: [Api.Chat]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.messages.Chats.chats(chats: _1!) + } + else { + return nil + } + } + public static func parse_chatsSlice(_ reader: BufferReader) -> Chats? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.Chats.chatsSlice(count: _1!, chats: _2!) + } + else { + return nil + } + } + + } + public enum DhConfig: TypeConstructorDescription { + case dhConfigNotModified(random: Buffer) + case dhConfig(g: Int32, p: Buffer, version: Int32, random: Buffer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .dhConfigNotModified(let random): + if boxed { + buffer.appendInt32(-1058912715) + } + serializeBytes(random, buffer: buffer, boxed: false) + break + case .dhConfig(let g, let p, let version, let random): + if boxed { + buffer.appendInt32(740433629) + } + serializeInt32(g, buffer: buffer, boxed: false) + serializeBytes(p, buffer: buffer, boxed: false) + serializeInt32(version, buffer: buffer, boxed: false) + serializeBytes(random, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .dhConfigNotModified(let random): + return ("dhConfigNotModified", [("random", random)]) + case .dhConfig(let g, let p, let version, let random): + return ("dhConfig", [("g", g), ("p", p), ("version", version), ("random", random)]) + } + } + + public static func parse_dhConfigNotModified(_ reader: BufferReader) -> DhConfig? { + var _1: Buffer? + _1 = parseBytes(reader) + let _c1 = _1 != nil + if _c1 { + return Api.messages.DhConfig.dhConfigNotModified(random: _1!) + } + else { + return nil + } + } + public static func parse_dhConfig(_ reader: BufferReader) -> DhConfig? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Buffer? + _2 = parseBytes(reader) + var _3: Int32? + _3 = reader.readInt32() + var _4: Buffer? + _4 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.messages.DhConfig.dhConfig(g: _1!, p: _2!, version: _3!, random: _4!) + } + else { + return nil + } + } + + } + public enum AffectedHistory: TypeConstructorDescription { + case affectedHistory(pts: Int32, ptsCount: Int32, offset: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .affectedHistory(let pts, let ptsCount, let offset): + if boxed { + buffer.appendInt32(-1269012015) + } + serializeInt32(pts, buffer: buffer, boxed: false) + serializeInt32(ptsCount, buffer: buffer, boxed: false) + serializeInt32(offset, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .affectedHistory(let pts, let ptsCount, let offset): + return ("affectedHistory", [("pts", pts), ("ptsCount", ptsCount), ("offset", offset)]) + } + } + + public static func parse_affectedHistory(_ reader: BufferReader) -> AffectedHistory? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.AffectedHistory.affectedHistory(pts: _1!, ptsCount: _2!, offset: _3!) + } + else { + return nil + } + } + + } + public enum MessageEditData: TypeConstructorDescription { + case messageEditData(flags: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageEditData(let flags): + if boxed { + buffer.appendInt32(649453030) + } + serializeInt32(flags, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .messageEditData(let flags): + return ("messageEditData", [("flags", flags)]) + } + } + + public static func parse_messageEditData(_ reader: BufferReader) -> MessageEditData? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.messages.MessageEditData.messageEditData(flags: _1!) + } + else { + return nil + } + } + + } + public enum ChatFull: TypeConstructorDescription { + case chatFull(fullChat: Api.ChatFull, chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .chatFull(let fullChat, let chats, let users): + if boxed { + buffer.appendInt32(-438840932) + } + fullChat.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .chatFull(let fullChat, let chats, let users): + return ("chatFull", [("fullChat", fullChat), ("chats", chats), ("users", users)]) + } + } + + public static func parse_chatFull(_ reader: BufferReader) -> ChatFull? { + var _1: Api.ChatFull? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.ChatFull + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.ChatFull.chatFull(fullChat: _1!, chats: _2!, users: _3!) + } + else { + return nil + } + } + + } + public enum SearchCounter: TypeConstructorDescription { + case searchCounter(flags: Int32, filter: Api.MessagesFilter, count: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .searchCounter(let flags, let filter, let count): + if boxed { + buffer.appendInt32(-398136321) + } + serializeInt32(flags, buffer: buffer, boxed: false) + filter.serialize(buffer, true) + serializeInt32(count, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .searchCounter(let flags, let filter, let count): + return ("searchCounter", [("flags", flags), ("filter", filter), ("count", count)]) + } + } + + public static func parse_searchCounter(_ reader: BufferReader) -> SearchCounter? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.MessagesFilter? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.MessagesFilter + } + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.SearchCounter.searchCounter(flags: _1!, filter: _2!, count: _3!) + } + else { + return nil + } + } + + } + public enum StickerSetInstallResult: TypeConstructorDescription { + case stickerSetInstallResultSuccess + case stickerSetInstallResultArchive(sets: [Api.StickerSetCovered]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .stickerSetInstallResultSuccess: + if boxed { + buffer.appendInt32(946083368) + } + + break + case .stickerSetInstallResultArchive(let sets): + if boxed { + buffer.appendInt32(904138920) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sets.count)) + for item in sets { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .stickerSetInstallResultSuccess: + return ("stickerSetInstallResultSuccess", []) + case .stickerSetInstallResultArchive(let sets): + return ("stickerSetInstallResultArchive", [("sets", sets)]) + } + } + + public static func parse_stickerSetInstallResultSuccess(_ reader: BufferReader) -> StickerSetInstallResult? { + return Api.messages.StickerSetInstallResult.stickerSetInstallResultSuccess + } + public static func parse_stickerSetInstallResultArchive(_ reader: BufferReader) -> StickerSetInstallResult? { + var _1: [Api.StickerSetCovered]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.messages.StickerSetInstallResult.stickerSetInstallResultArchive(sets: _1!) + } + else { + return nil + } + } + + } + public enum AffectedMessages: TypeConstructorDescription { + case affectedMessages(pts: Int32, ptsCount: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .affectedMessages(let pts, let ptsCount): + if boxed { + buffer.appendInt32(-2066640507) + } + serializeInt32(pts, buffer: buffer, boxed: false) + serializeInt32(ptsCount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .affectedMessages(let pts, let ptsCount): + return ("affectedMessages", [("pts", pts), ("ptsCount", ptsCount)]) + } + } + + public static func parse_affectedMessages(_ reader: BufferReader) -> AffectedMessages? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.AffectedMessages.affectedMessages(pts: _1!, ptsCount: _2!) + } + else { + return nil + } + } + + } + public enum SavedGifs: TypeConstructorDescription { + case savedGifsNotModified + case savedGifs(hash: Int32, gifs: [Api.Document]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .savedGifsNotModified: + if boxed { + buffer.appendInt32(-402498398) + } + + break + case .savedGifs(let hash, let gifs): + if boxed { + buffer.appendInt32(772213157) + } + serializeInt32(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(gifs.count)) + for item in gifs { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .savedGifsNotModified: + return ("savedGifsNotModified", []) + case .savedGifs(let hash, let gifs): + return ("savedGifs", [("hash", hash), ("gifs", gifs)]) + } + } + + public static func parse_savedGifsNotModified(_ reader: BufferReader) -> SavedGifs? { + return Api.messages.SavedGifs.savedGifsNotModified + } + public static func parse_savedGifs(_ reader: BufferReader) -> SavedGifs? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.Document]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.SavedGifs.savedGifs(hash: _1!, gifs: _2!) + } + else { + return nil + } + } + + } + public enum Messages: TypeConstructorDescription { + case messages(messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + case messagesNotModified(count: Int32) + case channelMessages(flags: Int32, pts: Int32, count: Int32, messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + case messagesSlice(flags: Int32, count: Int32, nextRate: Int32?, messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messages(let messages, let chats, let users): + if boxed { + buffer.appendInt32(-1938715001) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + case .messagesNotModified(let count): + if boxed { + buffer.appendInt32(1951620897) + } + serializeInt32(count, buffer: buffer, boxed: false) + break + case .channelMessages(let flags, let pts, let count, let messages, let chats, let users): + if boxed { + buffer.appendInt32(-1725551049) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(pts, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + case .messagesSlice(let flags, let count, let nextRate, let messages, let chats, let users): + if boxed { + buffer.appendInt32(-923939298) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(nextRate!, buffer: buffer, boxed: false)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .messages(let messages, let chats, let users): + return ("messages", [("messages", messages), ("chats", chats), ("users", users)]) + case .messagesNotModified(let count): + return ("messagesNotModified", [("count", count)]) + case .channelMessages(let flags, let pts, let count, let messages, let chats, let users): + return ("channelMessages", [("flags", flags), ("pts", pts), ("count", count), ("messages", messages), ("chats", chats), ("users", users)]) + case .messagesSlice(let flags, let count, let nextRate, let messages, let chats, let users): + return ("messagesSlice", [("flags", flags), ("count", count), ("nextRate", nextRate), ("messages", messages), ("chats", chats), ("users", users)]) + } + } + + public static func parse_messages(_ reader: BufferReader) -> Messages? { + var _1: [Api.Message]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.Messages.messages(messages: _1!, chats: _2!, users: _3!) + } + else { + return nil + } + } + public static func parse_messagesNotModified(_ reader: BufferReader) -> Messages? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.messages.Messages.messagesNotModified(count: _1!) + } + else { + return nil + } + } + public static func parse_channelMessages(_ reader: BufferReader) -> Messages? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: [Api.Message]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _5: [Api.Chat]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _6: [Api.User]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.messages.Messages.channelMessages(flags: _1!, pts: _2!, count: _3!, messages: _4!, chats: _5!, users: _6!) + } + else { + return nil + } + } + public static func parse_messagesSlice(_ reader: BufferReader) -> Messages? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt32() } + var _4: [Api.Message]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _5: [Api.Chat]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _6: [Api.User]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.messages.Messages.messagesSlice(flags: _1!, count: _2!, nextRate: _3, messages: _4!, chats: _5!, users: _6!) + } + else { + return nil + } + } + + } + public enum PeerDialogs: TypeConstructorDescription { + case peerDialogs(dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User], state: Api.updates.State) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .peerDialogs(let dialogs, let messages, let chats, let users, let state): + if boxed { + buffer.appendInt32(863093588) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(dialogs.count)) + for item in dialogs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + state.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .peerDialogs(let dialogs, let messages, let chats, let users, let state): + return ("peerDialogs", [("dialogs", dialogs), ("messages", messages), ("chats", chats), ("users", users), ("state", state)]) + } + } + + public static func parse_peerDialogs(_ reader: BufferReader) -> PeerDialogs? { + var _1: [Api.Dialog]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Dialog.self) + } + var _2: [Api.Message]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _3: [Api.Chat]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _4: [Api.User]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _5: Api.updates.State? + if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.updates.State + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.messages.PeerDialogs.peerDialogs(dialogs: _1!, messages: _2!, chats: _3!, users: _4!, state: _5!) + } + else { + return nil + } + } + + } + public enum RecentStickers: TypeConstructorDescription { + case recentStickersNotModified + case recentStickers(hash: Int32, packs: [Api.StickerPack], stickers: [Api.Document], dates: [Int32]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .recentStickersNotModified: + if boxed { + buffer.appendInt32(186120336) + } + + break + case .recentStickers(let hash, let packs, let stickers, let dates): + if boxed { + buffer.appendInt32(586395571) + } + serializeInt32(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(packs.count)) + for item in packs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(stickers.count)) + for item in stickers { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(dates.count)) + for item in dates { + serializeInt32(item, buffer: buffer, boxed: false) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .recentStickersNotModified: + return ("recentStickersNotModified", []) + case .recentStickers(let hash, let packs, let stickers, let dates): + return ("recentStickers", [("hash", hash), ("packs", packs), ("stickers", stickers), ("dates", dates)]) + } + } + + public static func parse_recentStickersNotModified(_ reader: BufferReader) -> RecentStickers? { + return Api.messages.RecentStickers.recentStickersNotModified + } + public static func parse_recentStickers(_ reader: BufferReader) -> RecentStickers? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.StickerPack]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) + } + var _3: [Api.Document]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + var _4: [Int32]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.messages.RecentStickers.recentStickers(hash: _1!, packs: _2!, stickers: _3!, dates: _4!) + } + else { + return nil + } + } + + } + public enum FeaturedStickers: TypeConstructorDescription { + case featuredStickersNotModified + case featuredStickers(hash: Int32, sets: [Api.StickerSetCovered], unread: [Int64]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .featuredStickersNotModified: + if boxed { + buffer.appendInt32(82699215) + } + + break + case .featuredStickers(let hash, let sets, let unread): + if boxed { + buffer.appendInt32(-123893531) + } + serializeInt32(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sets.count)) + for item in sets { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(unread.count)) + for item in unread { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .featuredStickersNotModified: + return ("featuredStickersNotModified", []) + case .featuredStickers(let hash, let sets, let unread): + return ("featuredStickers", [("hash", hash), ("sets", sets), ("unread", unread)]) + } + } + + public static func parse_featuredStickersNotModified(_ reader: BufferReader) -> FeaturedStickers? { + return Api.messages.FeaturedStickers.featuredStickersNotModified + } + public static func parse_featuredStickers(_ reader: BufferReader) -> FeaturedStickers? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.StickerSetCovered]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) + } + var _3: [Int64]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.FeaturedStickers.featuredStickers(hash: _1!, sets: _2!, unread: _3!) + } + else { + return nil + } + } + + } + public enum Dialogs: TypeConstructorDescription { + case dialogs(dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + case dialogsSlice(count: Int32, dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + case dialogsNotModified(count: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .dialogs(let dialogs, let messages, let chats, let users): + if boxed { + buffer.appendInt32(364538944) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(dialogs.count)) + for item in dialogs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + case .dialogsSlice(let count, let dialogs, let messages, let chats, let users): + if boxed { + buffer.appendInt32(1910543603) + } + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(dialogs.count)) + for item in dialogs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + case .dialogsNotModified(let count): + if boxed { + buffer.appendInt32(-253500010) + } + serializeInt32(count, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .dialogs(let dialogs, let messages, let chats, let users): + return ("dialogs", [("dialogs", dialogs), ("messages", messages), ("chats", chats), ("users", users)]) + case .dialogsSlice(let count, let dialogs, let messages, let chats, let users): + return ("dialogsSlice", [("count", count), ("dialogs", dialogs), ("messages", messages), ("chats", chats), ("users", users)]) + case .dialogsNotModified(let count): + return ("dialogsNotModified", [("count", count)]) + } + } + + public static func parse_dialogs(_ reader: BufferReader) -> Dialogs? { + var _1: [Api.Dialog]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Dialog.self) + } + var _2: [Api.Message]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _3: [Api.Chat]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _4: [Api.User]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.messages.Dialogs.dialogs(dialogs: _1!, messages: _2!, chats: _3!, users: _4!) + } + else { + return nil + } + } + public static func parse_dialogsSlice(_ reader: BufferReader) -> Dialogs? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.Dialog]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Dialog.self) + } + var _3: [Api.Message]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.messages.Dialogs.dialogsSlice(count: _1!, dialogs: _2!, messages: _3!, chats: _4!, users: _5!) + } + else { + return nil + } + } + public static func parse_dialogsNotModified(_ reader: BufferReader) -> Dialogs? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.messages.Dialogs.dialogsNotModified(count: _1!) + } + else { + return nil + } + } + + } + public enum FavedStickers: TypeConstructorDescription { + case favedStickersNotModified + case favedStickers(hash: Int32, packs: [Api.StickerPack], stickers: [Api.Document]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .favedStickersNotModified: + if boxed { + buffer.appendInt32(-1634752813) + } + + break + case .favedStickers(let hash, let packs, let stickers): + if boxed { + buffer.appendInt32(-209768682) + } + serializeInt32(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(packs.count)) + for item in packs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(stickers.count)) + for item in stickers { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .favedStickersNotModified: + return ("favedStickersNotModified", []) + case .favedStickers(let hash, let packs, let stickers): + return ("favedStickers", [("hash", hash), ("packs", packs), ("stickers", stickers)]) + } + } + + public static func parse_favedStickersNotModified(_ reader: BufferReader) -> FavedStickers? { + return Api.messages.FavedStickers.favedStickersNotModified + } + public static func parse_favedStickers(_ reader: BufferReader) -> FavedStickers? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.StickerPack]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) + } + var _3: [Api.Document]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.FavedStickers.favedStickers(hash: _1!, packs: _2!, stickers: _3!) + } + else { + return nil + } + } + + } + public enum AllStickers: TypeConstructorDescription { + case allStickersNotModified + case allStickers(hash: Int32, sets: [Api.StickerSet]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .allStickersNotModified: + if boxed { + buffer.appendInt32(-395967805) + } + + break + case .allStickers(let hash, let sets): + if boxed { + buffer.appendInt32(-302170017) + } + serializeInt32(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sets.count)) + for item in sets { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .allStickersNotModified: + return ("allStickersNotModified", []) + case .allStickers(let hash, let sets): + return ("allStickers", [("hash", hash), ("sets", sets)]) + } + } + + public static func parse_allStickersNotModified(_ reader: BufferReader) -> AllStickers? { + return Api.messages.AllStickers.allStickersNotModified + } + public static func parse_allStickers(_ reader: BufferReader) -> AllStickers? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.StickerSet]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSet.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.AllStickers.allStickers(hash: _1!, sets: _2!) + } + else { + return nil + } + } + + } + public enum HighScores: TypeConstructorDescription { + case highScores(scores: [Api.HighScore], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .highScores(let scores, let users): + if boxed { + buffer.appendInt32(-1707344487) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(scores.count)) + for item in scores { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .highScores(let scores, let users): + return ("highScores", [("scores", scores), ("users", users)]) + } + } + + public static func parse_highScores(_ reader: BufferReader) -> HighScores? { + var _1: [Api.HighScore]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.HighScore.self) + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.HighScores.highScores(scores: _1!, users: _2!) + } + else { + return nil + } + } + + } +} +} public extension Api { public enum InputGeoPoint: TypeConstructorDescription { case inputGeoPointEmpty @@ -270,13 +2024,13 @@ public extension Api { } public enum PollResults: TypeConstructorDescription { - case pollResults(flags: Int32, results: [Api.PollAnswerVoters]?, totalVoters: Int32?) + case pollResults(flags: Int32, results: [Api.PollAnswerVoters]?, totalVoters: Int32?, recentVoters: [Int32]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .pollResults(let flags, let results, let totalVoters): + case .pollResults(let flags, let results, let totalVoters, let recentVoters): if boxed { - buffer.appendInt32(1465219162) + buffer.appendInt32(-932174686) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) @@ -285,14 +2039,19 @@ public extension Api { item.serialize(buffer, true) }} if Int(flags) & Int(1 << 2) != 0 {serializeInt32(totalVoters!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(recentVoters!.count)) + for item in recentVoters! { + serializeInt32(item, buffer: buffer, boxed: false) + }} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .pollResults(let flags, let results, let totalVoters): - return ("pollResults", [("flags", flags), ("results", results), ("totalVoters", totalVoters)]) + case .pollResults(let flags, let results, let totalVoters, let recentVoters): + return ("pollResults", [("flags", flags), ("results", results), ("totalVoters", totalVoters), ("recentVoters", recentVoters)]) } } @@ -305,11 +2064,16 @@ public extension Api { } } var _3: Int32? if Int(_1!) & Int(1 << 2) != 0 {_3 = reader.readInt32() } + var _4: [Int32]? + if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil - if _c1 && _c2 && _c3 { - return Api.PollResults.pollResults(flags: _1!, results: _2, totalVoters: _3) + let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.PollResults.pollResults(flags: _1!, results: _2, totalVoters: _3, recentVoters: _4) } else { return nil @@ -1908,26 +3672,27 @@ public extension Api { } public enum AutoDownloadSettings: TypeConstructorDescription { - case autoDownloadSettings(flags: Int32, photoSizeMax: Int32, videoSizeMax: Int32, fileSizeMax: Int32) + case autoDownloadSettings(flags: Int32, photoSizeMax: Int32, videoSizeMax: Int32, fileSizeMax: Int32, videoUploadMaxbitrate: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .autoDownloadSettings(let flags, let photoSizeMax, let videoSizeMax, let fileSizeMax): + case .autoDownloadSettings(let flags, let photoSizeMax, let videoSizeMax, let fileSizeMax, let videoUploadMaxbitrate): if boxed { - buffer.appendInt32(-767099577) + buffer.appendInt32(-532532493) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(photoSizeMax, buffer: buffer, boxed: false) serializeInt32(videoSizeMax, buffer: buffer, boxed: false) serializeInt32(fileSizeMax, buffer: buffer, boxed: false) + serializeInt32(videoUploadMaxbitrate, buffer: buffer, boxed: false) break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .autoDownloadSettings(let flags, let photoSizeMax, let videoSizeMax, let fileSizeMax): - return ("autoDownloadSettings", [("flags", flags), ("photoSizeMax", photoSizeMax), ("videoSizeMax", videoSizeMax), ("fileSizeMax", fileSizeMax)]) + case .autoDownloadSettings(let flags, let photoSizeMax, let videoSizeMax, let fileSizeMax, let videoUploadMaxbitrate): + return ("autoDownloadSettings", [("flags", flags), ("photoSizeMax", photoSizeMax), ("videoSizeMax", videoSizeMax), ("fileSizeMax", fileSizeMax), ("videoUploadMaxbitrate", videoUploadMaxbitrate)]) } } @@ -1940,12 +3705,15 @@ public extension Api { _3 = reader.readInt32() var _4: Int32? _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.AutoDownloadSettings.autoDownloadSettings(flags: _1!, photoSizeMax: _2!, videoSizeMax: _3!, fileSizeMax: _4!) + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.AutoDownloadSettings.autoDownloadSettings(flags: _1!, photoSizeMax: _2!, videoSizeMax: _3!, fileSizeMax: _4!, videoUploadMaxbitrate: _5!) } else { return nil @@ -2928,25 +4696,27 @@ public extension Api { } public enum WallPaperSettings: TypeConstructorDescription { - case wallPaperSettings(flags: Int32, backgroundColor: Int32?, intensity: Int32?) + case wallPaperSettings(flags: Int32, backgroundColor: Int32?, secondBackgroundColor: Int32?, intensity: Int32?, rotation: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .wallPaperSettings(let flags, let backgroundColor, let intensity): + case .wallPaperSettings(let flags, let backgroundColor, let secondBackgroundColor, let intensity, let rotation): if boxed { - buffer.appendInt32(-1590738760) + buffer.appendInt32(84438264) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt32(backgroundColor!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {serializeInt32(secondBackgroundColor!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 3) != 0 {serializeInt32(intensity!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {serializeInt32(rotation!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .wallPaperSettings(let flags, let backgroundColor, let intensity): - return ("wallPaperSettings", [("flags", flags), ("backgroundColor", backgroundColor), ("intensity", intensity)]) + case .wallPaperSettings(let flags, let backgroundColor, let secondBackgroundColor, let intensity, let rotation): + return ("wallPaperSettings", [("flags", flags), ("backgroundColor", backgroundColor), ("secondBackgroundColor", secondBackgroundColor), ("intensity", intensity), ("rotation", rotation)]) } } @@ -2956,12 +4726,18 @@ public extension Api { var _2: Int32? if Int(_1!) & Int(1 << 0) != 0 {_2 = reader.readInt32() } var _3: Int32? - if Int(_1!) & Int(1 << 3) != 0 {_3 = reader.readInt32() } + if Int(_1!) & Int(1 << 4) != 0 {_3 = reader.readInt32() } + var _4: Int32? + if Int(_1!) & Int(1 << 3) != 0 {_4 = reader.readInt32() } + var _5: Int32? + if Int(_1!) & Int(1 << 4) != 0 {_5 = reader.readInt32() } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil - if _c1 && _c2 && _c3 { - return Api.WallPaperSettings.wallPaperSettings(flags: _1!, backgroundColor: _2, intensity: _3) + let _c3 = (Int(_1!) & Int(1 << 4) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.WallPaperSettings.wallPaperSettings(flags: _1!, backgroundColor: _2, secondBackgroundColor: _3, intensity: _4, rotation: _5) } else { return nil @@ -4082,6 +5858,9 @@ public extension Api { case updateNewScheduledMessage(message: Api.Message) case updateDeleteScheduledMessages(peer: Api.Peer, messages: [Int32]) case updateTheme(theme: Api.Theme) + case updateGeoLiveViewed(peer: Api.Peer, msgId: Int32) + case updateLoginToken + case updateMessagePollVote(pollId: Int64, userId: Int32, options: [Buffer]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -4706,6 +6485,31 @@ public extension Api { } theme.serialize(buffer, true) break + case .updateGeoLiveViewed(let peer, let msgId): + if boxed { + buffer.appendInt32(-2027964103) + } + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + break + case .updateLoginToken: + if boxed { + buffer.appendInt32(1448076945) + } + + break + case .updateMessagePollVote(let pollId, let userId, let options): + if boxed { + buffer.appendInt32(1123585836) + } + serializeInt64(pollId, buffer: buffer, boxed: false) + serializeInt32(userId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(options.count)) + for item in options { + serializeBytes(item, buffer: buffer, boxed: false) + } + break } } @@ -4859,6 +6663,12 @@ public extension Api { return ("updateDeleteScheduledMessages", [("peer", peer), ("messages", messages)]) case .updateTheme(let theme): return ("updateTheme", [("theme", theme)]) + case .updateGeoLiveViewed(let peer, let msgId): + return ("updateGeoLiveViewed", [("peer", peer), ("msgId", msgId)]) + case .updateLoginToken: + return ("updateLoginToken", []) + case .updateMessagePollVote(let pollId, let userId, let options): + return ("updateMessagePollVote", [("pollId", pollId), ("userId", userId), ("options", options)]) } } @@ -6117,6 +7927,44 @@ public extension Api { return nil } } + public static func parse_updateGeoLiveViewed(_ reader: BufferReader) -> Update? { + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.Update.updateGeoLiveViewed(peer: _1!, msgId: _2!) + } + else { + return nil + } + } + public static func parse_updateLoginToken(_ reader: BufferReader) -> Update? { + return Api.Update.updateLoginToken + } + public static func parse_updateMessagePollVote(_ reader: BufferReader) -> Update? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Buffer]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: Buffer.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.Update.updateMessagePollVote(pollId: _1!, userId: _2!, options: _3!) + } + else { + return nil + } + } } public enum PopularContact: TypeConstructorDescription { @@ -6374,6 +8222,106 @@ public extension Api { } } + } + public enum MessageUserVote: TypeConstructorDescription { + case messageUserVote(userId: Int32, option: Buffer, date: Int32) + case messageUserVoteInputOption(userId: Int32, date: Int32) + case messageUserVoteMultiple(userId: Int32, options: [Buffer], date: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageUserVote(let userId, let option, let date): + if boxed { + buffer.appendInt32(-1567730343) + } + serializeInt32(userId, buffer: buffer, boxed: false) + serializeBytes(option, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + break + case .messageUserVoteInputOption(let userId, let date): + if boxed { + buffer.appendInt32(909603888) + } + serializeInt32(userId, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + break + case .messageUserVoteMultiple(let userId, let options, let date): + if boxed { + buffer.appendInt32(244310238) + } + serializeInt32(userId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(options.count)) + for item in options { + serializeBytes(item, buffer: buffer, boxed: false) + } + serializeInt32(date, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .messageUserVote(let userId, let option, let date): + return ("messageUserVote", [("userId", userId), ("option", option), ("date", date)]) + case .messageUserVoteInputOption(let userId, let date): + return ("messageUserVoteInputOption", [("userId", userId), ("date", date)]) + case .messageUserVoteMultiple(let userId, let options, let date): + return ("messageUserVoteMultiple", [("userId", userId), ("options", options), ("date", date)]) + } + } + + public static func parse_messageUserVote(_ reader: BufferReader) -> MessageUserVote? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Buffer? + _2 = parseBytes(reader) + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.MessageUserVote.messageUserVote(userId: _1!, option: _2!, date: _3!) + } + else { + return nil + } + } + public static func parse_messageUserVoteInputOption(_ reader: BufferReader) -> MessageUserVote? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.MessageUserVote.messageUserVoteInputOption(userId: _1!, date: _2!) + } + else { + return nil + } + } + public static func parse_messageUserVoteMultiple(_ reader: BufferReader) -> MessageUserVote? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Buffer]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: -1255641564, elementType: Buffer.self) + } + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.MessageUserVote.messageUserVoteMultiple(userId: _1!, options: _2!, date: _3!) + } + else { + return nil + } + } + } public enum InputDialogPeer: TypeConstructorDescription { case inputDialogPeer(peer: Api.InputPeer) @@ -6480,6 +8428,7 @@ public extension Api { case keyboardButtonBuy(text: String) case keyboardButtonUrlAuth(flags: Int32, text: String, fwdText: String?, url: String, buttonId: Int32) case inputKeyboardButtonUrlAuth(flags: Int32, text: String, fwdText: String?, url: String, bot: Api.InputUser) + case keyboardButtonRequestPoll(flags: Int32, quiz: Api.Bool?, text: String) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -6555,6 +8504,14 @@ public extension Api { serializeString(url, buffer: buffer, boxed: false) bot.serialize(buffer, true) break + case .keyboardButtonRequestPoll(let flags, let quiz, let text): + if boxed { + buffer.appendInt32(-1144565411) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {quiz!.serialize(buffer, true)} + serializeString(text, buffer: buffer, boxed: false) + break } } @@ -6580,6 +8537,8 @@ public extension Api { return ("keyboardButtonUrlAuth", [("flags", flags), ("text", text), ("fwdText", fwdText), ("url", url), ("buttonId", buttonId)]) case .inputKeyboardButtonUrlAuth(let flags, let text, let fwdText, let url, let bot): return ("inputKeyboardButtonUrlAuth", [("flags", flags), ("text", text), ("fwdText", fwdText), ("url", url), ("bot", bot)]) + case .keyboardButtonRequestPoll(let flags, let quiz, let text): + return ("keyboardButtonRequestPoll", [("flags", flags), ("quiz", quiz), ("text", text)]) } } @@ -6731,6 +8690,25 @@ public extension Api { return nil } } + public static func parse_keyboardButtonRequestPoll(_ reader: BufferReader) -> KeyboardButton? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Bool? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Bool + } } + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.KeyboardButton.keyboardButtonRequestPoll(flags: _1!, quiz: _2, text: _3!) + } + else { + return nil + } + } } public enum ContactStatus: TypeConstructorDescription { @@ -7453,7 +9431,7 @@ public extension Api { case webPageEmpty(id: Int64) case webPagePending(id: Int64, date: Int32) case webPageNotModified - case webPage(flags: Int32, id: Int64, url: String, displayUrl: String, hash: Int32, type: String?, siteName: String?, title: String?, description: String?, photo: Api.Photo?, embedUrl: String?, embedType: String?, embedWidth: Int32?, embedHeight: Int32?, duration: Int32?, author: String?, document: Api.Document?, documents: [Api.Document]?, cachedPage: Api.Page?) + case webPage(flags: Int32, id: Int64, url: String, displayUrl: String, hash: Int32, type: String?, siteName: String?, title: String?, description: String?, photo: Api.Photo?, embedUrl: String?, embedType: String?, embedWidth: Int32?, embedHeight: Int32?, duration: Int32?, author: String?, document: Api.Document?, cachedPage: Api.Page?, attributes: [Api.WebPageAttribute]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -7476,9 +9454,9 @@ public extension Api { } break - case .webPage(let flags, let id, let url, let displayUrl, let hash, let type, let siteName, let title, let description, let photo, let embedUrl, let embedType, let embedWidth, let embedHeight, let duration, let author, let document, let documents, let cachedPage): + case .webPage(let flags, let id, let url, let displayUrl, let hash, let type, let siteName, let title, let description, let photo, let embedUrl, let embedType, let embedWidth, let embedHeight, let duration, let author, let document, let cachedPage, let attributes): if boxed { - buffer.appendInt32(-94051982) + buffer.appendInt32(-392411726) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) @@ -7497,12 +9475,12 @@ public extension Api { if Int(flags) & Int(1 << 7) != 0 {serializeInt32(duration!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 8) != 0 {serializeString(author!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 9) != 0 {document!.serialize(buffer, true)} - if Int(flags) & Int(1 << 11) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(documents!.count)) - for item in documents! { + if Int(flags) & Int(1 << 10) != 0 {cachedPage!.serialize(buffer, true)} + if Int(flags) & Int(1 << 12) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes!.count)) + for item in attributes! { item.serialize(buffer, true) }} - if Int(flags) & Int(1 << 10) != 0 {cachedPage!.serialize(buffer, true)} break } } @@ -7515,8 +9493,8 @@ public extension Api { return ("webPagePending", [("id", id), ("date", date)]) case .webPageNotModified: return ("webPageNotModified", []) - case .webPage(let flags, let id, let url, let displayUrl, let hash, let type, let siteName, let title, let description, let photo, let embedUrl, let embedType, let embedWidth, let embedHeight, let duration, let author, let document, let documents, let cachedPage): - return ("webPage", [("flags", flags), ("id", id), ("url", url), ("displayUrl", displayUrl), ("hash", hash), ("type", type), ("siteName", siteName), ("title", title), ("description", description), ("photo", photo), ("embedUrl", embedUrl), ("embedType", embedType), ("embedWidth", embedWidth), ("embedHeight", embedHeight), ("duration", duration), ("author", author), ("document", document), ("documents", documents), ("cachedPage", cachedPage)]) + case .webPage(let flags, let id, let url, let displayUrl, let hash, let type, let siteName, let title, let description, let photo, let embedUrl, let embedType, let embedWidth, let embedHeight, let duration, let author, let document, let cachedPage, let attributes): + return ("webPage", [("flags", flags), ("id", id), ("url", url), ("displayUrl", displayUrl), ("hash", hash), ("type", type), ("siteName", siteName), ("title", title), ("description", description), ("photo", photo), ("embedUrl", embedUrl), ("embedType", embedType), ("embedWidth", embedWidth), ("embedHeight", embedHeight), ("duration", duration), ("author", author), ("document", document), ("cachedPage", cachedPage), ("attributes", attributes)]) } } @@ -7587,13 +9565,13 @@ public extension Api { if Int(_1!) & Int(1 << 9) != 0 {if let signature = reader.readInt32() { _17 = Api.parse(reader, signature: signature) as? Api.Document } } - var _18: [Api.Document]? - if Int(_1!) & Int(1 << 11) != 0 {if let _ = reader.readInt32() { - _18 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } } - var _19: Api.Page? + var _18: Api.Page? if Int(_1!) & Int(1 << 10) != 0 {if let signature = reader.readInt32() { - _19 = Api.parse(reader, signature: signature) as? Api.Page + _18 = Api.parse(reader, signature: signature) as? Api.Page + } } + var _19: [Api.WebPageAttribute]? + if Int(_1!) & Int(1 << 12) != 0 {if let _ = reader.readInt32() { + _19 = Api.parseVector(reader, elementSignature: 0, elementType: Api.WebPageAttribute.self) } } let _c1 = _1 != nil let _c2 = _2 != nil @@ -7612,10 +9590,10 @@ public extension Api { let _c15 = (Int(_1!) & Int(1 << 7) == 0) || _15 != nil let _c16 = (Int(_1!) & Int(1 << 8) == 0) || _16 != nil let _c17 = (Int(_1!) & Int(1 << 9) == 0) || _17 != nil - let _c18 = (Int(_1!) & Int(1 << 11) == 0) || _18 != nil - let _c19 = (Int(_1!) & Int(1 << 10) == 0) || _19 != nil + let _c18 = (Int(_1!) & Int(1 << 10) == 0) || _18 != nil + let _c19 = (Int(_1!) & Int(1 << 12) == 0) || _19 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 { - return Api.WebPage.webPage(flags: _1!, id: _2!, url: _3!, displayUrl: _4!, hash: _5!, type: _6, siteName: _7, title: _8, description: _9, photo: _10, embedUrl: _11, embedType: _12, embedWidth: _13, embedHeight: _14, duration: _15, author: _16, document: _17, documents: _18, cachedPage: _19) + return Api.WebPage.webPage(flags: _1!, id: _2!, url: _3!, displayUrl: _4!, hash: _5!, type: _6, siteName: _7, title: _8, description: _9, photo: _10, embedUrl: _11, embedType: _12, embedWidth: _13, embedHeight: _14, duration: _15, author: _16, document: _17, cachedPage: _18, attributes: _19) } else { return nil @@ -8385,8 +10363,8 @@ public extension Api { case inputMediaPhotoExternal(flags: Int32, url: String, ttlSeconds: Int32?) case inputMediaDocumentExternal(flags: Int32, url: String, ttlSeconds: Int32?) case inputMediaContact(phoneNumber: String, firstName: String, lastName: String, vcard: String) - case inputMediaPoll(poll: Api.Poll) case inputMediaGeoLive(flags: Int32, geoPoint: Api.InputGeoPoint, period: Int32?) + case inputMediaPoll(flags: Int32, poll: Api.Poll, correctAnswers: [Buffer]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -8514,12 +10492,6 @@ public extension Api { serializeString(lastName, buffer: buffer, boxed: false) serializeString(vcard, buffer: buffer, boxed: false) break - case .inputMediaPoll(let poll): - if boxed { - buffer.appendInt32(112424539) - } - poll.serialize(buffer, true) - break case .inputMediaGeoLive(let flags, let geoPoint, let period): if boxed { buffer.appendInt32(-833715459) @@ -8528,6 +10500,18 @@ public extension Api { geoPoint.serialize(buffer, true) if Int(flags) & Int(1 << 1) != 0 {serializeInt32(period!, buffer: buffer, boxed: false)} break + case .inputMediaPoll(let flags, let poll, let correctAnswers): + if boxed { + buffer.appendInt32(-1410741723) + } + serializeInt32(flags, buffer: buffer, boxed: false) + poll.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(correctAnswers!.count)) + for item in correctAnswers! { + serializeBytes(item, buffer: buffer, boxed: false) + }} + break } } @@ -8559,10 +10543,10 @@ public extension Api { return ("inputMediaDocumentExternal", [("flags", flags), ("url", url), ("ttlSeconds", ttlSeconds)]) case .inputMediaContact(let phoneNumber, let firstName, let lastName, let vcard): return ("inputMediaContact", [("phoneNumber", phoneNumber), ("firstName", firstName), ("lastName", lastName), ("vcard", vcard)]) - case .inputMediaPoll(let poll): - return ("inputMediaPoll", [("poll", poll)]) case .inputMediaGeoLive(let flags, let geoPoint, let period): return ("inputMediaGeoLive", [("flags", flags), ("geoPoint", geoPoint), ("period", period)]) + case .inputMediaPoll(let flags, let poll, let correctAnswers): + return ("inputMediaPoll", [("flags", flags), ("poll", poll), ("correctAnswers", correctAnswers)]) } } @@ -8831,19 +10815,6 @@ public extension Api { return nil } } - public static func parse_inputMediaPoll(_ reader: BufferReader) -> InputMedia? { - var _1: Api.Poll? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.Poll - } - let _c1 = _1 != nil - if _c1 { - return Api.InputMedia.inputMediaPoll(poll: _1!) - } - else { - return nil - } - } public static func parse_inputMediaGeoLive(_ reader: BufferReader) -> InputMedia? { var _1: Int32? _1 = reader.readInt32() @@ -8863,6 +10834,27 @@ public extension Api { return nil } } + public static func parse_inputMediaPoll(_ reader: BufferReader) -> InputMedia? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Poll? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Poll + } + var _3: [Buffer]? + if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: Buffer.self) + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.InputMedia.inputMediaPoll(flags: _1!, poll: _2!, correctAnswers: _3) + } + else { + return nil + } + } } public enum InputPeer: TypeConstructorDescription { @@ -12479,6 +14471,7 @@ public extension Api { } public enum WallPaper: TypeConstructorDescription { case wallPaper(id: Int64, flags: Int32, accessHash: Int64, slug: String, document: Api.Document, settings: Api.WallPaperSettings?) + case wallPaperNoFile(flags: Int32, settings: Api.WallPaperSettings?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -12493,6 +14486,13 @@ public extension Api { document.serialize(buffer, true) if Int(flags) & Int(1 << 2) != 0 {settings!.serialize(buffer, true)} break + case .wallPaperNoFile(let flags, let settings): + if boxed { + buffer.appendInt32(-1963717851) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {settings!.serialize(buffer, true)} + break } } @@ -12500,6 +14500,8 @@ public extension Api { switch self { case .wallPaper(let id, let flags, let accessHash, let slug, let document, let settings): return ("wallPaper", [("id", id), ("flags", flags), ("accessHash", accessHash), ("slug", slug), ("document", document), ("settings", settings)]) + case .wallPaperNoFile(let flags, let settings): + return ("wallPaperNoFile", [("flags", flags), ("settings", settings)]) } } @@ -12533,6 +14535,22 @@ public extension Api { return nil } } + public static func parse_wallPaperNoFile(_ reader: BufferReader) -> WallPaper? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.WallPaperSettings? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.WallPaperSettings + } } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 2) == 0) || _2 != nil + if _c1 && _c2 { + return Api.WallPaper.wallPaperNoFile(flags: _1!, settings: _2) + } + else { + return nil + } + } } public enum Invoice: TypeConstructorDescription { @@ -13676,6 +15694,44 @@ public extension Api { return Api.DocumentAttribute.documentAttributeHasStickers } + } + public enum BankCardOpenUrl: TypeConstructorDescription { + case bankCardOpenUrl(url: String, name: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .bankCardOpenUrl(let url, let name): + if boxed { + buffer.appendInt32(-177732982) + } + serializeString(url, buffer: buffer, boxed: false) + serializeString(name, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .bankCardOpenUrl(let url, let name): + return ("bankCardOpenUrl", [("url", url), ("name", name)]) + } + } + + public static func parse_bankCardOpenUrl(_ reader: BufferReader) -> BankCardOpenUrl? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.BankCardOpenUrl.bankCardOpenUrl(url: _1!, name: _2!) + } + else { + return nil + } + } + } public enum ChatPhoto: TypeConstructorDescription { case chatPhotoEmpty @@ -15162,6 +17218,7 @@ public extension Api { public enum InputWallPaper: TypeConstructorDescription { case inputWallPaper(id: Int64, accessHash: Int64) case inputWallPaperSlug(slug: String) + case inputWallPaperNoFile public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -15177,6 +17234,12 @@ public extension Api { buffer.appendInt32(1913199744) } serializeString(slug, buffer: buffer, boxed: false) + break + case .inputWallPaperNoFile: + if boxed { + buffer.appendInt32(-2077770836) + } + break } } @@ -15187,6 +17250,8 @@ public extension Api { return ("inputWallPaper", [("id", id), ("accessHash", accessHash)]) case .inputWallPaperSlug(let slug): return ("inputWallPaperSlug", [("slug", slug)]) + case .inputWallPaperNoFile: + return ("inputWallPaperNoFile", []) } } @@ -15215,6 +17280,73 @@ public extension Api { return nil } } + public static func parse_inputWallPaperNoFile(_ reader: BufferReader) -> InputWallPaper? { + return Api.InputWallPaper.inputWallPaperNoFile + } + + } + public enum InputThemeSettings: TypeConstructorDescription { + case inputThemeSettings(flags: Int32, baseTheme: Api.BaseTheme, accentColor: Int32, messageTopColor: Int32?, messageBottomColor: Int32?, wallpaper: Api.InputWallPaper?, wallpaperSettings: Api.WallPaperSettings?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputThemeSettings(let flags, let baseTheme, let accentColor, let messageTopColor, let messageBottomColor, let wallpaper, let wallpaperSettings): + if boxed { + buffer.appendInt32(-1118798639) + } + serializeInt32(flags, buffer: buffer, boxed: false) + baseTheme.serialize(buffer, true) + serializeInt32(accentColor, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(messageTopColor!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(messageBottomColor!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {wallpaper!.serialize(buffer, true)} + if Int(flags) & Int(1 << 1) != 0 {wallpaperSettings!.serialize(buffer, true)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputThemeSettings(let flags, let baseTheme, let accentColor, let messageTopColor, let messageBottomColor, let wallpaper, let wallpaperSettings): + return ("inputThemeSettings", [("flags", flags), ("baseTheme", baseTheme), ("accentColor", accentColor), ("messageTopColor", messageTopColor), ("messageBottomColor", messageBottomColor), ("wallpaper", wallpaper), ("wallpaperSettings", wallpaperSettings)]) + } + } + + public static func parse_inputThemeSettings(_ reader: BufferReader) -> InputThemeSettings? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.BaseTheme? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.BaseTheme + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_4 = reader.readInt32() } + var _5: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_5 = reader.readInt32() } + var _6: Api.InputWallPaper? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.InputWallPaper + } } + var _7: Api.WallPaperSettings? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.WallPaperSettings + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.InputThemeSettings.inputThemeSettings(flags: _1!, baseTheme: _2!, accentColor: _3!, messageTopColor: _4, messageBottomColor: _5, wallpaper: _6, wallpaperSettings: _7) + } + else { + return nil + } + } } public enum InputStickeredMedia: TypeConstructorDescription { @@ -15274,6 +17406,56 @@ public extension Api { } } + } + public enum WebPageAttribute: TypeConstructorDescription { + case webPageAttributeTheme(flags: Int32, documents: [Api.Document]?, settings: Api.ThemeSettings?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .webPageAttributeTheme(let flags, let documents, let settings): + if boxed { + buffer.appendInt32(1421174295) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(documents!.count)) + for item in documents! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 1) != 0 {settings!.serialize(buffer, true)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .webPageAttributeTheme(let flags, let documents, let settings): + return ("webPageAttributeTheme", [("flags", flags), ("documents", documents), ("settings", settings)]) + } + } + + public static func parse_webPageAttributeTheme(_ reader: BufferReader) -> WebPageAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.Document]? + if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } } + var _3: Api.ThemeSettings? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.ThemeSettings + } } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.WebPageAttribute.webPageAttributeTheme(flags: _1!, documents: _2, settings: _3) + } + else { + return nil + } + } + } public enum PhoneCallDiscardReason: TypeConstructorDescription { case phoneCallDiscardReasonMissed @@ -15852,6 +18034,80 @@ public extension Api { } } + } + public enum BaseTheme: TypeConstructorDescription { + case baseThemeClassic + case baseThemeDay + case baseThemeNight + case baseThemeTinted + case baseThemeArctic + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .baseThemeClassic: + if boxed { + buffer.appendInt32(-1012849566) + } + + break + case .baseThemeDay: + if boxed { + buffer.appendInt32(-69724536) + } + + break + case .baseThemeNight: + if boxed { + buffer.appendInt32(-1212997976) + } + + break + case .baseThemeTinted: + if boxed { + buffer.appendInt32(1834973166) + } + + break + case .baseThemeArctic: + if boxed { + buffer.appendInt32(1527845466) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .baseThemeClassic: + return ("baseThemeClassic", []) + case .baseThemeDay: + return ("baseThemeDay", []) + case .baseThemeNight: + return ("baseThemeNight", []) + case .baseThemeTinted: + return ("baseThemeTinted", []) + case .baseThemeArctic: + return ("baseThemeArctic", []) + } + } + + public static func parse_baseThemeClassic(_ reader: BufferReader) -> BaseTheme? { + return Api.BaseTheme.baseThemeClassic + } + public static func parse_baseThemeDay(_ reader: BufferReader) -> BaseTheme? { + return Api.BaseTheme.baseThemeDay + } + public static func parse_baseThemeNight(_ reader: BufferReader) -> BaseTheme? { + return Api.BaseTheme.baseThemeNight + } + public static func parse_baseThemeTinted(_ reader: BufferReader) -> BaseTheme? { + return Api.BaseTheme.baseThemeTinted + } + public static func parse_baseThemeArctic(_ reader: BufferReader) -> BaseTheme? { + return Api.BaseTheme.baseThemeArctic + } + } public enum MessagesFilter: TypeConstructorDescription { case inputMessagesFilterEmpty @@ -18230,20 +20486,13 @@ public extension Api { } public enum Theme: TypeConstructorDescription { - case themeDocumentNotModified - case theme(flags: Int32, id: Int64, accessHash: Int64, slug: String, title: String, document: Api.Document?, installsCount: Int32) + case theme(flags: Int32, id: Int64, accessHash: Int64, slug: String, title: String, document: Api.Document?, settings: Api.ThemeSettings?, installsCount: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .themeDocumentNotModified: + case .theme(let flags, let id, let accessHash, let slug, let title, let document, let settings, let installsCount): if boxed { - buffer.appendInt32(1211967244) - } - - break - case .theme(let flags, let id, let accessHash, let slug, let title, let document, let installsCount): - if boxed { - buffer.appendInt32(-136770336) + buffer.appendInt32(42930452) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) @@ -18251,6 +20500,7 @@ public extension Api { serializeString(slug, buffer: buffer, boxed: false) serializeString(title, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 2) != 0 {document!.serialize(buffer, true)} + if Int(flags) & Int(1 << 3) != 0 {settings!.serialize(buffer, true)} serializeInt32(installsCount, buffer: buffer, boxed: false) break } @@ -18258,16 +20508,11 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .themeDocumentNotModified: - return ("themeDocumentNotModified", []) - case .theme(let flags, let id, let accessHash, let slug, let title, let document, let installsCount): - return ("theme", [("flags", flags), ("id", id), ("accessHash", accessHash), ("slug", slug), ("title", title), ("document", document), ("installsCount", installsCount)]) + case .theme(let flags, let id, let accessHash, let slug, let title, let document, let settings, let installsCount): + return ("theme", [("flags", flags), ("id", id), ("accessHash", accessHash), ("slug", slug), ("title", title), ("document", document), ("settings", settings), ("installsCount", installsCount)]) } } - public static func parse_themeDocumentNotModified(_ reader: BufferReader) -> Theme? { - return Api.Theme.themeDocumentNotModified - } public static func parse_theme(_ reader: BufferReader) -> Theme? { var _1: Int32? _1 = reader.readInt32() @@ -18283,17 +20528,22 @@ public extension Api { if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.Document } } - var _7: Int32? - _7 = reader.readInt32() + var _7: Api.ThemeSettings? + if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.ThemeSettings + } } + var _8: Int32? + _8 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil - let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.Theme.theme(flags: _1!, id: _2!, accessHash: _3!, slug: _4!, title: _5!, document: _6, installsCount: _7!) + let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.Theme.theme(flags: _1!, id: _2!, accessHash: _3!, slug: _4!, title: _5!, document: _6, settings: _7, installsCount: _8!) } else { return nil @@ -18334,6 +20584,64 @@ public extension Api { } } + } + public enum ThemeSettings: TypeConstructorDescription { + case themeSettings(flags: Int32, baseTheme: Api.BaseTheme, accentColor: Int32, messageTopColor: Int32?, messageBottomColor: Int32?, wallpaper: Api.WallPaper?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .themeSettings(let flags, let baseTheme, let accentColor, let messageTopColor, let messageBottomColor, let wallpaper): + if boxed { + buffer.appendInt32(-1676371894) + } + serializeInt32(flags, buffer: buffer, boxed: false) + baseTheme.serialize(buffer, true) + serializeInt32(accentColor, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(messageTopColor!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(messageBottomColor!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {wallpaper!.serialize(buffer, true)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .themeSettings(let flags, let baseTheme, let accentColor, let messageTopColor, let messageBottomColor, let wallpaper): + return ("themeSettings", [("flags", flags), ("baseTheme", baseTheme), ("accentColor", accentColor), ("messageTopColor", messageTopColor), ("messageBottomColor", messageBottomColor), ("wallpaper", wallpaper)]) + } + } + + public static func parse_themeSettings(_ reader: BufferReader) -> ThemeSettings? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.BaseTheme? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.BaseTheme + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_4 = reader.readInt32() } + var _5: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_5 = reader.readInt32() } + var _6: Api.WallPaper? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.WallPaper + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.ThemeSettings.themeSettings(flags: _1!, baseTheme: _2!, accentColor: _3!, messageTopColor: _4, messageBottomColor: _5, wallpaper: _6) + } + else { + return nil + } + } + } public enum PeerNotifySettings: TypeConstructorDescription { case peerNotifySettingsEmpty @@ -18712,6 +21020,7 @@ public extension Api { case messageEntityUnderline(offset: Int32, length: Int32) case messageEntityStrike(offset: Int32, length: Int32) case messageEntityBlockquote(offset: Int32, length: Int32) + case messageEntityBankCard(offset: Int32, length: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -18845,6 +21154,13 @@ public extension Api { serializeInt32(offset, buffer: buffer, boxed: false) serializeInt32(length, buffer: buffer, boxed: false) break + case .messageEntityBankCard(let offset, let length): + if boxed { + buffer.appendInt32(1981704948) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break } } @@ -18886,6 +21202,8 @@ public extension Api { return ("messageEntityStrike", [("offset", offset), ("length", length)]) case .messageEntityBlockquote(let offset, let length): return ("messageEntityBlockquote", [("offset", offset), ("length", length)]) + case .messageEntityBankCard(let offset, let length): + return ("messageEntityBankCard", [("offset", offset), ("length", length)]) } } @@ -19155,6 +21473,20 @@ public extension Api { return nil } } + public static func parse_messageEntityBankCard(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.MessageEntity.messageEntityBankCard(offset: _1!, length: _2!) + } + else { + return nil + } + } } public enum InputPhoto: TypeConstructorDescription { @@ -19457,6 +21789,7 @@ public extension Api { } public enum PeerLocated: TypeConstructorDescription { case peerLocated(peer: Api.Peer, expires: Int32, distance: Int32) + case peerSelfLocated(expires: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -19468,6 +21801,12 @@ public extension Api { serializeInt32(expires, buffer: buffer, boxed: false) serializeInt32(distance, buffer: buffer, boxed: false) break + case .peerSelfLocated(let expires): + if boxed { + buffer.appendInt32(-118740917) + } + serializeInt32(expires, buffer: buffer, boxed: false) + break } } @@ -19475,6 +21814,8 @@ public extension Api { switch self { case .peerLocated(let peer, let expires, let distance): return ("peerLocated", [("peer", peer), ("expires", expires), ("distance", distance)]) + case .peerSelfLocated(let expires): + return ("peerSelfLocated", [("expires", expires)]) } } @@ -19497,6 +21838,17 @@ public extension Api { return nil } } + public static func parse_peerSelfLocated(_ reader: BufferReader) -> PeerLocated? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.PeerLocated.peerSelfLocated(expires: _1!) + } + else { + return nil + } + } } public enum Document: TypeConstructorDescription { diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 02f63a3f26..b83eff3dc4 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -490,10 +490,138 @@ public struct payments { } } + public enum BankCardData: TypeConstructorDescription { + case bankCardData(title: String, openUrls: [Api.BankCardOpenUrl]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .bankCardData(let title, let openUrls): + if boxed { + buffer.appendInt32(1042605427) + } + serializeString(title, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(openUrls.count)) + for item in openUrls { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .bankCardData(let title, let openUrls): + return ("bankCardData", [("title", title), ("openUrls", openUrls)]) + } + } + + public static func parse_bankCardData(_ reader: BufferReader) -> BankCardData? { + var _1: String? + _1 = parseString(reader) + var _2: [Api.BankCardOpenUrl]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BankCardOpenUrl.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.payments.BankCardData.bankCardData(title: _1!, openUrls: _2!) + } + else { + return nil + } + } + + } } } public extension Api { public struct auth { + public enum LoginToken: TypeConstructorDescription { + case loginToken(expires: Int32, token: Buffer) + case loginTokenMigrateTo(dcId: Int32, token: Buffer) + case loginTokenSuccess(authorization: Api.auth.Authorization) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .loginToken(let expires, let token): + if boxed { + buffer.appendInt32(1654593920) + } + serializeInt32(expires, buffer: buffer, boxed: false) + serializeBytes(token, buffer: buffer, boxed: false) + break + case .loginTokenMigrateTo(let dcId, let token): + if boxed { + buffer.appendInt32(110008598) + } + serializeInt32(dcId, buffer: buffer, boxed: false) + serializeBytes(token, buffer: buffer, boxed: false) + break + case .loginTokenSuccess(let authorization): + if boxed { + buffer.appendInt32(957176926) + } + authorization.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .loginToken(let expires, let token): + return ("loginToken", [("expires", expires), ("token", token)]) + case .loginTokenMigrateTo(let dcId, let token): + return ("loginTokenMigrateTo", [("dcId", dcId), ("token", token)]) + case .loginTokenSuccess(let authorization): + return ("loginTokenSuccess", [("authorization", authorization)]) + } + } + + public static func parse_loginToken(_ reader: BufferReader) -> LoginToken? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Buffer? + _2 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.auth.LoginToken.loginToken(expires: _1!, token: _2!) + } + else { + return nil + } + } + public static func parse_loginTokenMigrateTo(_ reader: BufferReader) -> LoginToken? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Buffer? + _2 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.auth.LoginToken.loginTokenMigrateTo(dcId: _1!, token: _2!) + } + else { + return nil + } + } + public static func parse_loginTokenSuccess(_ reader: BufferReader) -> LoginToken? { + var _1: Api.auth.Authorization? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.auth.Authorization + } + let _c1 = _1 != nil + if _c1 { + return Api.auth.LoginToken.loginTokenSuccess(authorization: _1!) + } + else { + return nil + } + } + + } public enum Authorization: TypeConstructorDescription { case authorization(flags: Int32, tmpSessions: Int32?, user: Api.User) case authorizationSignUpRequired(flags: Int32, termsOfService: Api.help.TermsOfService?) @@ -1775,6 +1903,70 @@ public struct help { } } + } + public enum UserInfo: TypeConstructorDescription { + case userInfoEmpty + case userInfo(message: String, entities: [Api.MessageEntity], author: String, date: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .userInfoEmpty: + if boxed { + buffer.appendInt32(-206688531) + } + + break + case .userInfo(let message, let entities, let author, let date): + if boxed { + buffer.appendInt32(32192344) + } + serializeString(message, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities.count)) + for item in entities { + item.serialize(buffer, true) + } + serializeString(author, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .userInfoEmpty: + return ("userInfoEmpty", []) + case .userInfo(let message, let entities, let author, let date): + return ("userInfo", [("message", message), ("entities", entities), ("author", author), ("date", date)]) + } + } + + public static func parse_userInfoEmpty(_ reader: BufferReader) -> UserInfo? { + return Api.help.UserInfo.userInfoEmpty + } + public static func parse_userInfo(_ reader: BufferReader) -> UserInfo? { + var _1: String? + _1 = parseString(reader) + var _2: [Api.MessageEntity]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } + var _3: String? + _3 = parseString(reader) + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.help.UserInfo.userInfo(message: _1!, entities: _2!, author: _3!, date: _4!) + } + else { + return nil + } + } + } public enum TermsOfServiceUpdate: TypeConstructorDescription { case termsOfServiceUpdateEmpty(expires: Int32) diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index 15affdff86..9d5351bb9e 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -673,6 +673,40 @@ public struct account { } } + } + public enum ContentSettings: TypeConstructorDescription { + case contentSettings(flags: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .contentSettings(let flags): + if boxed { + buffer.appendInt32(1474462241) + } + serializeInt32(flags, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .contentSettings(let flags): + return ("contentSettings", [("flags", flags)]) + } + } + + public static func parse_contentSettings(_ reader: BufferReader) -> ContentSettings? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.account.ContentSettings.contentSettings(flags: _1!) + } + else { + return nil + } + } + } public enum Authorizations: TypeConstructorDescription { case authorizations(authorizations: [Api.Authorization]) @@ -3161,6 +3195,25 @@ public extension Api { return result }) } + + public static func getPollVotes(flags: Int32, peer: Api.InputPeer, id: Int32, option: Buffer?, offset: String?, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1200736242) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeInt32(id, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeBytes(option!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeString(offset!, buffer: buffer, boxed: false)} + serializeInt32(limit, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getPollVotes", parameters: [("flags", flags), ("peer", peer), ("id", id), ("option", option), ("offset", offset), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.VotesList? in + let reader = BufferReader(buffer) + var result: Api.messages.VotesList? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.VotesList + } + return result + }) + } } public struct channels { public static func readHistory(channel: Api.InputChannel, maxId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { @@ -3727,6 +3780,20 @@ public extension Api { return result }) } + + public static func getInactiveChannels() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(300429806) + + return (FunctionDescription(name: "channels.getInactiveChannels", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.InactiveChats? in + let reader = BufferReader(buffer) + var result: Api.messages.InactiveChats? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.InactiveChats + } + return result + }) + } } public struct payments { public static func getPaymentForm(msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { @@ -3818,6 +3885,20 @@ public extension Api { return result }) } + + public static func getBankCardData(number: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(779736953) + serializeString(number, buffer: buffer, boxed: false) + return (FunctionDescription(name: "payments.getBankCardData", parameters: [("number", number)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.BankCardData? in + let reader = BufferReader(buffer) + var result: Api.payments.BankCardData? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.payments.BankCardData + } + return result + }) + } } public struct auth { public static func checkPhone(phoneNumber: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { @@ -4084,6 +4165,54 @@ public extension Api { return result }) } + + public static func exportLoginToken(apiId: Int32, apiHash: String, exceptIds: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1313598185) + serializeInt32(apiId, buffer: buffer, boxed: false) + serializeString(apiHash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(exceptIds.count)) + for item in exceptIds { + serializeInt32(item, buffer: buffer, boxed: false) + } + return (FunctionDescription(name: "auth.exportLoginToken", parameters: [("apiId", apiId), ("apiHash", apiHash), ("exceptIds", exceptIds)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.auth.LoginToken? in + let reader = BufferReader(buffer) + var result: Api.auth.LoginToken? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.auth.LoginToken + } + return result + }) + } + + public static func importLoginToken(token: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1783866140) + serializeBytes(token, buffer: buffer, boxed: false) + return (FunctionDescription(name: "auth.importLoginToken", parameters: [("token", token)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.auth.LoginToken? in + let reader = BufferReader(buffer) + var result: Api.auth.LoginToken? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.auth.LoginToken + } + return result + }) + } + + public static func acceptLoginToken(token: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-392909491) + serializeBytes(token, buffer: buffer, boxed: false) + return (FunctionDescription(name: "auth.acceptLoginToken", parameters: [("token", token)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Authorization? in + let reader = BufferReader(buffer) + var result: Api.Authorization? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Authorization + } + return result + }) + } } public struct bots { public static func sendCustomRequest(customMethod: String, params: Api.DataJSON) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { @@ -4443,11 +4572,13 @@ public extension Api { }) } - public static func getLocated(geoPoint: Api.InputGeoPoint) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + public static func getLocated(flags: Int32, geoPoint: Api.InputGeoPoint, selfExpires: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(171270230) + buffer.appendInt32(-750207932) + serializeInt32(flags, buffer: buffer, boxed: false) geoPoint.serialize(buffer, true) - return (FunctionDescription(name: "contacts.getLocated", parameters: [("geoPoint", geoPoint)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(selfExpires!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "contacts.getLocated", parameters: [("flags", flags), ("geoPoint", geoPoint), ("selfExpires", selfExpires)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -4700,6 +4831,40 @@ public extension Api { return result }) } + + public static func editUserInfo(userId: Api.InputUser, message: String, entities: [Api.MessageEntity]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1723407216) + userId.serialize(buffer, true) + serializeString(message, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities.count)) + for item in entities { + item.serialize(buffer, true) + } + return (FunctionDescription(name: "help.editUserInfo", parameters: [("userId", userId), ("message", message), ("entities", entities)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.help.UserInfo? in + let reader = BufferReader(buffer) + var result: Api.help.UserInfo? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.help.UserInfo + } + return result + }) + } + + public static func getUserInfo(userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(59377875) + userId.serialize(buffer, true) + return (FunctionDescription(name: "help.getUserInfo", parameters: [("userId", userId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.help.UserInfo? in + let reader = BufferReader(buffer) + var result: Api.help.UserInfo? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.help.UserInfo + } + return result + }) + } } public struct updates { public static func getState() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { @@ -5739,41 +5904,6 @@ public extension Api { }) } - public static func createTheme(slug: String, title: String, document: Api.InputDocument) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(729808255) - serializeString(slug, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) - document.serialize(buffer, true) - return (FunctionDescription(name: "account.createTheme", parameters: [("slug", slug), ("title", title), ("document", document)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Theme? in - let reader = BufferReader(buffer) - var result: Api.Theme? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Theme - } - return result - }) - } - - public static func updateTheme(flags: Int32, format: String, theme: Api.InputTheme, slug: String?, title: String?, document: Api.InputDocument?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(999203330) - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(format, buffer: buffer, boxed: false) - theme.serialize(buffer, true) - if Int(flags) & Int(1 << 0) != 0 {serializeString(slug!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {serializeString(title!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 2) != 0 {document!.serialize(buffer, true)} - return (FunctionDescription(name: "account.updateTheme", parameters: [("flags", flags), ("format", format), ("theme", theme), ("slug", slug), ("title", title), ("document", document)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Theme? in - let reader = BufferReader(buffer) - var result: Api.Theme? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Theme - } - return result - }) - } - public static func saveTheme(theme: Api.InputTheme, unsave: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() buffer.appendInt32(-229175188) @@ -5835,6 +5965,90 @@ public extension Api { return result }) } + + public static func setContentSettings(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1250643605) + serializeInt32(flags, buffer: buffer, boxed: false) + return (FunctionDescription(name: "account.setContentSettings", parameters: [("flags", flags)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } + + public static func getContentSettings() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1952756306) + + return (FunctionDescription(name: "account.getContentSettings", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.ContentSettings? in + let reader = BufferReader(buffer) + var result: Api.account.ContentSettings? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.account.ContentSettings + } + return result + }) + } + + public static func createTheme(flags: Int32, slug: String, title: String, document: Api.InputDocument?, settings: Api.InputThemeSettings?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-2077048289) + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(slug, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {document!.serialize(buffer, true)} + if Int(flags) & Int(1 << 3) != 0 {settings!.serialize(buffer, true)} + return (FunctionDescription(name: "account.createTheme", parameters: [("flags", flags), ("slug", slug), ("title", title), ("document", document), ("settings", settings)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Theme? in + let reader = BufferReader(buffer) + var result: Api.Theme? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Theme + } + return result + }) + } + + public static func updateTheme(flags: Int32, format: String, theme: Api.InputTheme, slug: String?, title: String?, document: Api.InputDocument?, settings: Api.InputThemeSettings?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1555261397) + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(format, buffer: buffer, boxed: false) + theme.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 {serializeString(slug!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeString(title!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {document!.serialize(buffer, true)} + if Int(flags) & Int(1 << 3) != 0 {settings!.serialize(buffer, true)} + return (FunctionDescription(name: "account.updateTheme", parameters: [("flags", flags), ("format", format), ("theme", theme), ("slug", slug), ("title", title), ("document", document), ("settings", settings)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Theme? in + let reader = BufferReader(buffer) + var result: Api.Theme? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Theme + } + return result + }) + } + + public static func getMultiWallPapers(wallpapers: [Api.InputWallPaper]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.WallPaper]>) { + let buffer = Buffer() + buffer.appendInt32(1705865692) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(wallpapers.count)) + for item in wallpapers { + item.serialize(buffer, true) + } + return (FunctionDescription(name: "account.getMultiWallPapers", parameters: [("wallpapers", wallpapers)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.WallPaper]? in + let reader = BufferReader(buffer) + var result: [Api.WallPaper]? + if let _ = reader.readInt32() { + result = Api.parseVector(reader, elementSignature: 0, elementType: Api.WallPaper.self) + } + return result + }) + } } public struct wallet { public static func sendLiteRequest(body: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { diff --git a/submodules/TelegramBaseController/Sources/LocationBroadcastActionSheetItem.swift b/submodules/TelegramBaseController/Sources/LocationBroadcastActionSheetItem.swift index b39b7b96c8..02b59611a2 100644 --- a/submodules/TelegramBaseController/Sources/LocationBroadcastActionSheetItem.swift +++ b/submodules/TelegramBaseController/Sources/LocationBroadcastActionSheetItem.swift @@ -50,7 +50,7 @@ private let avatarFont = avatarPlaceholderFont(size: 15.0) public class LocationBroadcastActionSheetItemNode: ActionSheetItemNode { private let theme: ActionSheetControllerTheme - public static let defaultFont: UIFont = Font.regular(20.0) + private let defaultFont: UIFont private var item: LocationBroadcastActionSheetItem? @@ -61,6 +61,7 @@ public class LocationBroadcastActionSheetItemNode: ActionSheetItemNode { override public init(theme: ActionSheetControllerTheme) { self.theme = theme + self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) self.button = HighlightTrackingButton() @@ -99,10 +100,12 @@ public class LocationBroadcastActionSheetItemNode: ActionSheetItemNode { func setItem(_ item: LocationBroadcastActionSheetItem) { self.item = item - let textColor: UIColor = self.theme.primaryTextColor - self.label.attributedText = NSAttributedString(string: item.title, font: ActionSheetButtonNode.defaultFont, textColor: textColor) + let defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) - self.avatarNode.setPeer(account: item.context.account, theme: (item.context.sharedContext.currentPresentationData.with { $0 }).theme, peer: item.peer) + let textColor: UIColor = self.theme.primaryTextColor + self.label.attributedText = NSAttributedString(string: item.title, font: defaultFont, textColor: textColor) + + self.avatarNode.setPeer(context: item.context, theme: (item.context.sharedContext.currentPresentationData.with { $0 }).theme, peer: item.peer) self.timerNode.update(backgroundColor: self.theme.controlAccentColor.withAlphaComponent(0.4), foregroundColor: self.theme.controlAccentColor, textColor: self.theme.controlAccentColor, beginTimestamp: item.beginTimestamp, timeout: item.timeout, strings: item.strings) diff --git a/submodules/TelegramBaseController/Sources/LocationBroadcastNavigationAccessoryPanel.swift b/submodules/TelegramBaseController/Sources/LocationBroadcastNavigationAccessoryPanel.swift index d75b0ee04f..59430f3883 100644 --- a/submodules/TelegramBaseController/Sources/LocationBroadcastNavigationAccessoryPanel.swift +++ b/submodules/TelegramBaseController/Sources/LocationBroadcastNavigationAccessoryPanel.swift @@ -135,13 +135,13 @@ final class LocationBroadcastNavigationAccessoryPanel: ASDisplayNode { } else { let otherString: String if filteredPeers.count == 1 { - otherString = peers[0].compactDisplayTitle + otherString = peers[0].compactDisplayTitle.replacingOccurrences(of: "*", with: "") } else { otherString = self.strings.Conversation_LiveLocationMembersCount(Int32(peers.count)) } let rawText: String if filteredPeers.count != peers.count { - rawText = self.strings.Conversation_LiveLocationYouAnd(otherString).0.replacingOccurrences(of: "*", with: "**") + rawText = self.strings.Conversation_LiveLocationYouAndOther(otherString).0 } else { rawText = otherString } diff --git a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift index 31d6158e39..a9fd7074dc 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -42,8 +42,8 @@ private class MediaHeaderItemNode: ASDisplayNode { switch displayData { case let .music(title, performer, _, long): rateButtonHidden = !long - let titleText: String = title ?? "Unknown Track" - let subtitleText: String = performer ?? "Unknown Artist" + let titleText: String = title ?? strings.MediaPlayer_UnknownTrack + let subtitleText: String = performer ?? strings.MediaPlayer_UnknownArtist titleString = NSAttributedString(string: titleText, font: titleFont, textColor: theme.rootController.navigationBar.primaryTextColor) subtitleString = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: theme.rootController.navigationBar.secondaryTextColor) diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index b09f37ef9a..6bd0c6ca87 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -309,7 +309,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { presentLiveLocationController(context: strongSelf.context, peerId: messages[0].id.peerId, controller: strongSelf) } else { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -375,7 +375,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { closePeerId = peerId } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index 4f338d5622..1181b67ea5 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -132,7 +132,7 @@ public final class CallController: ViewController { } } } else { - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] for output in availableOutputs { let title: String @@ -158,7 +158,7 @@ public final class CallController: ViewController { } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) @@ -216,11 +216,13 @@ public final class CallController: ViewController { } |> deliverOnMainQueue).start(next: { [weak self] callsTabTip in if let strongSelf = self { if callsTabTip == 2 { - let controller = callSuggestTabController(sharedContext: strongSelf.sharedContext) - strongSelf.present(controller, in: .window(.root)) + Queue.mainQueue().after(1.0) { + let controller = callSuggestTabController(sharedContext: strongSelf.sharedContext) + strongSelf.present(controller, in: .window(.root)) + } } if callsTabTip < 3 { - let _ = ApplicationSpecificNotice.incrementCallsTabTips(accountManager: strongSelf.sharedContext.accountManager, count: 4).start() + let _ = ApplicationSpecificNotice.incrementCallsTabTips(accountManager: strongSelf.sharedContext.accountManager).start() } } }) diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNode.swift b/submodules/TelegramCallsUI/Sources/CallControllerNode.swift index 76496d7ce5..597b11e526 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNode.swift @@ -232,10 +232,18 @@ final class CallControllerNode: ASDisplayNode { text += "\n\(self.statusNode.subtitle)" } statusValue = .text(text) - case let .active(timestamp, reception, keyVisualHash): + case .active(let timestamp, let reception, let keyVisualHash), .reconnecting(let timestamp, let reception, let keyVisualHash): let strings = self.presentationData.strings + var isReconnecting = false + if case .reconnecting = callState { + isReconnecting = true + } statusValue = .timer({ value in - return strings.Call_StatusOngoing(value).0 + if isReconnecting { + return strings.Call_StatusConnecting + } else { + return strings.Call_StatusOngoing(value).0 + } }, timestamp) if self.keyTextData?.0 != keyVisualHash { let text = stringForEmojiHashOfData(keyVisualHash, 4)! diff --git a/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift b/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift index d0ba42de1d..92ef6fe615 100644 --- a/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift +++ b/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift @@ -61,11 +61,13 @@ private enum CallFeedbackReason: Int32, CaseIterable { private final class CallFeedbackControllerArguments { let updateComment: (String) -> Void + let scrollToComment: () -> Void let toggleReason: (CallFeedbackReason, Bool) -> Void let toggleIncludeLogs: (Bool) -> Void - init(updateComment: @escaping (String) -> Void, toggleReason: @escaping (CallFeedbackReason, Bool) -> Void, toggleIncludeLogs: @escaping (Bool) -> Void) { + init(updateComment: @escaping (String) -> Void, scrollToComment: @escaping () -> Void, toggleReason: @escaping (CallFeedbackReason, Bool) -> Void, toggleIncludeLogs: @escaping (Bool) -> Void) { self.updateComment = updateComment + self.scrollToComment = scrollToComment self.toggleReason = toggleReason self.toggleIncludeLogs = toggleIncludeLogs } @@ -77,6 +79,18 @@ private enum CallFeedbackControllerSection: Int32 { case logs } +private enum CallFeedbackControllerEntryTag: ItemListItemTag { + case comment + + func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? CallFeedbackControllerEntryTag { + return self == other + } else { + return false + } + } +} + private enum CallFeedbackControllerEntry: ItemListNodeEntry { case reasonsHeader(PresentationTheme, String) case reason(PresentationTheme, CallFeedbackReason, String, Bool) @@ -149,25 +163,29 @@ private enum CallFeedbackControllerEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! CallFeedbackControllerArguments switch self { case let .reasonsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .reason(theme, reason, title, value): - return ItemListSwitchItem(theme: theme, title: title, value: value, maximumNumberOfLines: 2, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, maximumNumberOfLines: 2, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleReason(reason, value) }) case let .comment(theme, text, placeholder): - return ItemListMultilineInputItem(theme: theme, text: text, placeholder: placeholder, maxLength: nil, sectionId: self.section, style: .blocks, textUpdated: { updatedText in + return ItemListMultilineInputItem(presentationData: presentationData, text: text, placeholder: placeholder, maxLength: nil, sectionId: self.section, style: .blocks, textUpdated: { updatedText in arguments.updateComment(updatedText) - }) + }, updatedFocus: { focused in + if focused { + arguments.scrollToComment() + } + }, tag: CallFeedbackControllerEntryTag.comment) case let .includeLogs(theme, title, value): - return ItemListSwitchItem(theme: theme, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleIncludeLogs(value) }) case let .includeLogsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -222,9 +240,13 @@ public func callFeedbackController(sharedContext: SharedAccountContext, account: var presentControllerImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? + var ensureItemVisibleImpl: ((CallFeedbackControllerEntryTag, Bool) -> Void)? let arguments = CallFeedbackControllerArguments(updateComment: { value in updateState { $0.withUpdatedComment(value) } + ensureItemVisibleImpl?(.comment, false) + }, scrollToComment: { + ensureItemVisibleImpl?(.comment, true) }, toggleReason: { reason, value in updateState { current in var reasons = current.reasons @@ -240,37 +262,37 @@ public func callFeedbackController(sharedContext: SharedAccountContext, account: }) let signal = combineLatest(sharedContext.presentationData, statePromise.get()) - |> deliverOnMainQueue - |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { - dismissImpl?() - }) - let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.CallFeedback_Send), style: .bold, enabled: true, action: { - var comment = state.comment - var hashtags = "" - for reason in CallFeedbackReason.allCases { - if state.reasons.contains(reason) { - if !hashtags.isEmpty { - hashtags.append(" ") - } - hashtags.append("#\(reason.hashtag)") + |> deliverOnMainQueue + |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + dismissImpl?() + }) + let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.CallFeedback_Send), style: .bold, enabled: true, action: { + var comment = state.comment + var hashtags = "" + for reason in CallFeedbackReason.allCases { + if state.reasons.contains(reason) { + if !hashtags.isEmpty { + hashtags.append(" ") } + hashtags.append("#\(reason.hashtag)") } - if !comment.isEmpty && !state.reasons.isEmpty { - comment.append("\n") - } - comment.append(hashtags) - - let _ = rateCallAndSendLogs(account: account, callId: callId, starsCount: rating, comment: comment, userInitiated: userInitiated, includeLogs: state.includeLogs).start() - dismissImpl?() - - presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .starSuccess(presentationData.strings.CallFeedback_Success))) - }) + } + if !comment.isEmpty && !state.reasons.isEmpty { + comment.append("\n") + } + comment.append(hashtags) - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.CallFeedback_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: callFeedbackControllerEntries(theme: presentationData.theme, strings: presentationData.strings, state: state), style: .blocks, animateChanges: false) + let _ = rateCallAndSendLogs(account: account, callId: callId, starsCount: rating, comment: comment, userInitiated: userInitiated, includeLogs: state.includeLogs).start() + dismissImpl?() - return (controllerState, (listState, arguments)) + presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .starSuccess(presentationData.strings.CallFeedback_Success))) + }) + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.CallFeedback_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: callFeedbackControllerEntries(theme: presentationData.theme, strings: presentationData.strings, state: state), style: .blocks, animateChanges: false) + + return (controllerState, (listState, arguments)) } @@ -283,5 +305,28 @@ public func callFeedbackController(sharedContext: SharedAccountContext, account: controller?.view.endEditing(true) controller?.dismiss() } + ensureItemVisibleImpl = { [weak controller] targetTag, animated in + controller?.afterLayout({ + guard let controller = controller else { + return + } + + var resultItemNode: ListViewItemNode? + let state = stateValue.with({ $0 }) + let _ = controller.frameForItemNode({ itemNode in + if let itemNode = itemNode as? ItemListItemNode { + if let tag = itemNode.tag, tag.isEqual(to: targetTag) { + resultItemNode = itemNode as? ListViewItemNode + return true + } + } + return false + }) + + if let resultItemNode = resultItemNode { + controller.ensureItemNodeVisible(resultItemNode, animated: animated) + } + }) + } return controller } diff --git a/submodules/TelegramCallsUI/Sources/CallRatingController.swift b/submodules/TelegramCallsUI/Sources/CallRatingController.swift index 2bbe0acae1..79ba03527f 100644 --- a/submodules/TelegramCallsUI/Sources/CallRatingController.swift +++ b/submodules/TelegramCallsUI/Sources/CallRatingController.swift @@ -277,7 +277,7 @@ public func callRatingController(sharedContext: SharedAccountContext, account: A dismissImpl?(true) })] - contentNode = CallRatingAlertContentNode(theme: AlertControllerTheme(presentationTheme: theme), ptheme: theme, strings: strings, actions: actions, dismiss: { + contentNode = CallRatingAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, actions: actions, dismiss: { dismissImpl?(true) }, apply: { rating in dismissImpl?(true) @@ -288,7 +288,7 @@ public func callRatingController(sharedContext: SharedAccountContext, account: A } }) - let controller = AlertController(theme: AlertControllerTheme(presentationTheme: theme), contentNode: contentNode!) + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) dismissImpl = { [weak controller] animated in if animated { controller?.dismissAnimated() diff --git a/submodules/TelegramCallsUI/Sources/CallRouteActionSheetItem.swift b/submodules/TelegramCallsUI/Sources/CallRouteActionSheetItem.swift index 173e147897..708287c54d 100644 --- a/submodules/TelegramCallsUI/Sources/CallRouteActionSheetItem.swift +++ b/submodules/TelegramCallsUI/Sources/CallRouteActionSheetItem.swift @@ -35,7 +35,7 @@ public class CallRouteActionSheetItem: ActionSheetItem { public class CallRouteActionSheetItemNode: ActionSheetItemNode { private let theme: ActionSheetControllerTheme - public static let defaultFont: UIFont = Font.regular(20.0) + private let defaultFont: UIFont private var item: CallRouteActionSheetItem? @@ -48,6 +48,7 @@ public class CallRouteActionSheetItemNode: ActionSheetItemNode { override public init(theme: ActionSheetControllerTheme) { self.theme = theme + self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) self.button = HighlightTrackingButton() self.button.isAccessibilityElement = false @@ -115,7 +116,9 @@ public class CallRouteActionSheetItemNode: ActionSheetItemNode { func setItem(_ item: CallRouteActionSheetItem) { self.item = item - self.label.attributedText = NSAttributedString(string: item.title, font: ActionSheetButtonNode.defaultFont, textColor: self.theme.standardActionTextColor) + let defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) + + self.label.attributedText = NSAttributedString(string: item.title, font: defaultFont, textColor: self.theme.standardActionTextColor) if let icon = item.icon { self.iconNode.image = generateTintedImage(image: icon, color: self.theme.standardActionTextColor) } else { diff --git a/submodules/TelegramCallsUI/Sources/CallSuggestTabController.swift b/submodules/TelegramCallsUI/Sources/CallSuggestTabController.swift index ba5f52d066..cb4c9b859f 100644 --- a/submodules/TelegramCallsUI/Sources/CallSuggestTabController.swift +++ b/submodules/TelegramCallsUI/Sources/CallSuggestTabController.swift @@ -219,9 +219,9 @@ func callSuggestTabController(sharedContext: SharedAccountContext) -> AlertContr }).start() })] - contentNode = CallSuggestTabAlertContentNode(theme: AlertControllerTheme(presentationTheme: theme), ptheme: theme, strings: strings, actions: actions) + contentNode = CallSuggestTabAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, actions: actions) - let controller = AlertController(theme: AlertControllerTheme(presentationTheme: theme), contentNode: contentNode!) + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) dismissImpl = { [weak controller] animated in if animated { controller?.dismissAnimated() diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index 2710fea018..5fcfaefa4a 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -419,14 +419,14 @@ public final class PresentationCallImpl: PresentationCall { Logger.shared.log("PresentationCall", "reportIncomingCall device in DND mode") Queue.mainQueue().async { if let strongSelf = self { - strongSelf.callSessionManager.drop(internalId: strongSelf.internalId, reason: .busy) + strongSelf.callSessionManager.drop(internalId: strongSelf.internalId, reason: .busy, debugLog: .single(nil)) } } } else { Logger.shared.log("PresentationCall", "reportIncomingCall error \(error)") Queue.mainQueue().async { if let strongSelf = self { - strongSelf.callSessionManager.drop(internalId: strongSelf.internalId, reason: .hangUp) + strongSelf.callSessionManager.drop(internalId: strongSelf.internalId, reason: .hangUp, debugLog: .single(nil)) } } } @@ -451,7 +451,7 @@ public final class PresentationCallImpl: PresentationCall { presentationState = .connecting(keyVisualHash) case .failed: presentationState = nil - self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect) + self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil)) case .connected: let timestamp: Double if let activeTimestamp = self.activeTimestamp { @@ -461,6 +461,15 @@ public final class PresentationCallImpl: PresentationCall { self.activeTimestamp = timestamp } presentationState = .active(timestamp, reception, keyVisualHash) + case .reconnecting: + let timestamp: Double + if let activeTimestamp = self.activeTimestamp { + timestamp = activeTimestamp + } else { + timestamp = CFAbsoluteTimeGetCurrent() + self.activeTimestamp = timestamp + } + presentationState = .reconnecting(timestamp, reception, keyVisualHash) } } else { presentationState = .connecting(keyVisualHash) @@ -484,12 +493,14 @@ public final class PresentationCallImpl: PresentationCall { case let .terminated(id, _, options): self.audioSessionShouldBeActive.set(true) if wasActive { - self.ongoingContext.stop(callId: id, sendDebugLogs: options.contains(.sendDebugLogs)) + let debugLogValue = Promise() + self.ongoingContext.stop(callId: id, sendDebugLogs: options.contains(.sendDebugLogs), debugLogValue: debugLogValue) } default: self.audioSessionShouldBeActive.set(false) if wasActive { - self.ongoingContext.stop() + let debugLogValue = Promise() + self.ongoingContext.stop(debugLogValue: debugLogValue) } } if case .terminated = sessionState.state, !wasTerminated { @@ -518,7 +529,7 @@ public final class PresentationCallImpl: PresentationCall { } if let presentationState = presentationState { self.statePromise.set(presentationState) - self.updateTone(presentationState, previous: previous) + self.updateTone(presentationState, callContextState: callContextState, previous: previous) } if !self.shouldPresentCallRating { @@ -528,9 +539,11 @@ public final class PresentationCallImpl: PresentationCall { } } - private func updateTone(_ state: PresentationCallState, previous: CallSession?) { + private func updateTone(_ state: PresentationCallState, callContextState: OngoingCallContextState?, previous: CallSession?) { var tone: PresentationCallTone? - if let previous = previous { + if let callContextState = callContextState, case .reconnecting = callContextState { + tone = .connecting + } else if let previous = previous { switch previous.state { case .accepting, .active, .dropping, .requesting: switch state { @@ -602,15 +615,17 @@ public final class PresentationCallImpl: PresentationCall { } public func hangUp() -> Signal { - self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp) - self.ongoingContext.stop() + let debugLogValue = Promise() + self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp, debugLog: debugLogValue.get()) + self.ongoingContext.stop(debugLogValue: debugLogValue) return self.hungUpPromise.get() } public func rejectBusy() { - self.callSessionManager.drop(internalId: self.internalId, reason: .busy) - self.ongoingContext.stop() + self.callSessionManager.drop(internalId: self.internalId, reason: .busy, debugLog: .single(nil)) + let debugLog = Promise() + self.ongoingContext.stop(debugLogValue: debugLog) } public func toggleIsMuted() { diff --git a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift index 9b2053834c..6bfda0df93 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift @@ -35,12 +35,17 @@ private enum CurrentCall { public final class PresentationCallManagerImpl: PresentationCallManager { private let getDeviceAccessData: () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void) + private let isMediaPlaying: () -> Bool + private let resumeMediaPlayback: () -> Void private let accountManager: AccountManager private let audioSession: ManagedAudioSession private let callKitIntegration: CallKitIntegration? - private var currentCall: PresentationCallImpl? + private var currentCallValue: PresentationCallImpl? + private var currentCall: PresentationCallImpl? { + return self.currentCallValue + } private var currentCallDisposable = MetaDisposable() private let removeCurrentCallDisposable = MetaDisposable() @@ -64,15 +69,20 @@ public final class PresentationCallManagerImpl: PresentationCallManager { private var callSettings: VoiceCallSettings? private var callSettingsDisposable: Disposable? + private var resumeMedia: Bool = false + public static var voipMaxLayer: Int32 { return OngoingCallContext.maxLayer } - public init(accountManager: AccountManager, getDeviceAccessData: @escaping () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void), audioSession: ManagedAudioSession, activeAccounts: Signal<[Account], NoError>) { + public init(accountManager: AccountManager, getDeviceAccessData: @escaping () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void), isMediaPlaying: @escaping () -> Bool, resumeMediaPlayback: @escaping () -> Void, audioSession: ManagedAudioSession, activeAccounts: Signal<[Account], NoError>) { self.getDeviceAccessData = getDeviceAccessData self.accountManager = accountManager self.audioSession = audioSession + self.isMediaPlaying = isMediaPlaying + self.resumeMediaPlayback = resumeMediaPlayback + var startCallImpl: ((Account, UUID, String) -> Signal)? var answerCallImpl: ((UUID) -> Void)? var endCallImpl: ((UUID) -> Signal)? @@ -252,14 +262,14 @@ public final class PresentationCallManagerImpl: PresentationCallManager { let enableCallKit = true let call = PresentationCallImpl(account: account, audioSession: self.audioSession, callSessionManager: account.callSessionManager, callKitIntegration: enableCallKit ? callKitIntegrationIfEnabled(self.callKitIntegration, settings: self.callSettings) : nil, serializedData: configuration.serializedData, dataSaving: effectiveDataSaving(for: self.callSettings, autodownloadSettings: autodownloadSettings), derivedState: derivedState, getDeviceAccessData: self.getDeviceAccessData, initialState: callSession, internalId: ringingState.id, peerId: ringingState.peerId, isOutgoing: false, peer: peer, proxyServer: self.proxyServer, currentNetworkType: .none, updatedNetworkType: account.networkType) - self.currentCall = call + self.updateCurrentCall(call) self.currentCallPromise.set(.single(call)) self.hasActiveCallsPromise.set(true) self.removeCurrentCallDisposable.set((call.canBeRemoved |> deliverOnMainQueue).start(next: { [weak self, weak call] value in if value, let strongSelf = self, let call = call { if strongSelf.currentCall === call { - strongSelf.currentCall = nil + strongSelf.updateCurrentCall(nil) strongSelf.currentCallPromise.set(.single(nil)) strongSelf.hasActiveCallsPromise.set(false) } @@ -282,14 +292,14 @@ public final class PresentationCallManagerImpl: PresentationCallManager { let autodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings] as? AutodownloadSettings ?? .defaultSettings let call = PresentationCallImpl(account: firstState.0, audioSession: strongSelf.audioSession, callSessionManager: firstState.0.callSessionManager, callKitIntegration: enableCallKit ? callKitIntegrationIfEnabled(strongSelf.callKitIntegration, settings: strongSelf.callSettings) : nil, serializedData: configuration.serializedData, dataSaving: effectiveDataSaving(for: strongSelf.callSettings, autodownloadSettings: autodownloadSettings), derivedState: derivedState, getDeviceAccessData: strongSelf.getDeviceAccessData, initialState: nil, internalId: firstState.2.id, peerId: firstState.2.peerId, isOutgoing: false, peer: firstState.1, proxyServer: strongSelf.proxyServer, currentNetworkType: firstState.4, updatedNetworkType: firstState.0.networkType) - strongSelf.currentCall = call + strongSelf.updateCurrentCall(call) strongSelf.currentCallPromise.set(.single(call)) strongSelf.hasActiveCallsPromise.set(true) strongSelf.removeCurrentCallDisposable.set((call.canBeRemoved |> deliverOnMainQueue).start(next: { [weak self, weak call] value in if value, let strongSelf = self, let call = call { if strongSelf.currentCall === call { - strongSelf.currentCall = nil + strongSelf.updateCurrentCall(nil) strongSelf.currentCallPromise.set(.single(nil)) strongSelf.hasActiveCallsPromise.set(false) } @@ -299,7 +309,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager { } else { for (account, _, state, _, _) in ringingStates { if state.id != self.currentCall?.internalId { - account.callSessionManager.drop(internalId: state.id, reason: .missed) + account.callSessionManager.drop(internalId: state.id, reason: .missed, debugLog: .single(nil)) } } } @@ -412,14 +422,14 @@ public final class PresentationCallManagerImpl: PresentationCallManager { let autodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings] as? AutodownloadSettings ?? .defaultSettings let call = PresentationCallImpl(account: account, audioSession: strongSelf.audioSession, callSessionManager: account.callSessionManager, callKitIntegration: callKitIntegrationIfEnabled(strongSelf.callKitIntegration, settings: strongSelf.callSettings), serializedData: configuration.serializedData, dataSaving: effectiveDataSaving(for: strongSelf.callSettings, autodownloadSettings: autodownloadSettings), derivedState: derivedState, getDeviceAccessData: strongSelf.getDeviceAccessData, initialState: nil, internalId: internalId, peerId: peerId, isOutgoing: true, peer: nil, proxyServer: strongSelf.proxyServer, currentNetworkType: currentNetworkType, updatedNetworkType: account.networkType) - strongSelf.currentCall = call + strongSelf.updateCurrentCall(call) strongSelf.currentCallPromise.set(.single(call)) strongSelf.hasActiveCallsPromise.set(true) strongSelf.removeCurrentCallDisposable.set((call.canBeRemoved |> deliverOnMainQueue).start(next: { [weak call] value in if value, let strongSelf = self, let call = call { if strongSelf.currentCall === call { - strongSelf.currentCall = nil + strongSelf.updateCurrentCall(nil) strongSelf.currentCallPromise.set(.single(nil)) strongSelf.hasActiveCallsPromise.set(false) } @@ -432,4 +442,19 @@ public final class PresentationCallManagerImpl: PresentationCallManager { } } } + + private func updateCurrentCall(_ value: PresentationCallImpl?) { + let wasEmpty = self.currentCallValue == nil + let isEmpty = value == nil + if wasEmpty && !isEmpty { + self.resumeMedia = self.isMediaPlaying() + } + + self.currentCallValue = value + + if !wasEmpty && isEmpty && self.resumeMedia { + self.resumeMedia = false + self.resumeMediaPlayback() + } + } } diff --git a/submodules/TelegramCore/Sources/Account.swift b/submodules/TelegramCore/Sources/Account.swift index f1fecc8907..d28d1ee0a1 100644 --- a/submodules/TelegramCore/Sources/Account.swift +++ b/submodules/TelegramCore/Sources/Account.swift @@ -61,6 +61,12 @@ public class UnauthorizedAccount { public let testingEnvironment: Bool public let postbox: Postbox public let network: Network + private let stateManager: UnauthorizedAccountStateManager + + private let updateLoginTokenPipe = ValuePipe() + public var updateLoginTokenEvents: Signal { + return self.updateLoginTokenPipe.signal() + } public var masterDatacenterId: Int32 { return Int32(self.network.mtProto.datacenterId) @@ -76,6 +82,10 @@ public class UnauthorizedAccount { self.testingEnvironment = testingEnvironment self.postbox = postbox self.network = network + let updateLoginTokenPipe = self.updateLoginTokenPipe + self.stateManager = UnauthorizedAccountStateManager(network: network, updateLoginToken: { + updateLoginTokenPipe.putNext(Void()) + }) network.shouldKeepConnection.set(self.shouldBeServiceTaskMaster.get() |> map { mode -> Bool in @@ -99,6 +109,8 @@ public class UnauthorizedAccount { } network.context.beginExplicitBackupAddressDiscovery() }) + + self.stateManager.reset() } public func changedMasterDatacenterId(accountManager: AccountManager, masterDatacenterId: Int32) -> Signal { @@ -822,6 +834,7 @@ public class Account { public private(set) var callSessionManager: CallSessionManager! public private(set) var viewTracker: AccountViewTracker! public private(set) var pendingMessageManager: PendingMessageManager! + public private(set) var pendingUpdateMessageManager: PendingUpdateMessageManager! public private(set) var messageMediaPreuploadManager: MessageMediaPreuploadManager! private(set) var mediaReferenceRevalidationContext: MediaReferenceRevalidationContext! private var peerInputActivityManager: PeerInputActivityManager! @@ -852,6 +865,11 @@ public class Account { public var networkType: Signal { return self.networkTypeValue.get() } + private let atomicCurrentNetworkType = Atomic(value: .none) + public var immediateNetworkType: NetworkType { + return self.atomicCurrentNetworkType.with { $0 } + } + private var networkTypeDisposable: Disposable? private let _loggedOut = ValuePromise(false, ignoreRepeated: true) public var loggedOut: Signal { @@ -906,6 +924,7 @@ public class Account { self.messageMediaPreuploadManager = MessageMediaPreuploadManager() self.mediaReferenceRevalidationContext = MediaReferenceRevalidationContext() self.pendingMessageManager = PendingMessageManager(network: network, postbox: postbox, accountPeerId: peerId, auxiliaryMethods: auxiliaryMethods, stateManager: self.stateManager, localInputActivityManager: self.localInputActivityManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.mediaReferenceRevalidationContext) + self.pendingUpdateMessageManager = PendingUpdateMessageManager(postbox: postbox, network: network, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, mediaReferenceRevalidationContext: self.mediaReferenceRevalidationContext) self.network.loggedOut = { [weak self] in Logger.shared.log("Account", "network logged out") @@ -958,6 +977,10 @@ public class Account { |> distinctUntilChanged) self.networkTypeValue.set(currentNetworkType()) + let atomicCurrentNetworkType = self.atomicCurrentNetworkType + self.networkTypeDisposable = self.networkTypeValue.get().start(next: { value in + let _ = atomicCurrentNetworkType.swap(value) + }) let serviceTasksMasterBecomeMaster = self.shouldBeServiceTaskMaster.get() |> distinctUntilChanged @@ -1021,10 +1044,12 @@ public class Account { self.managedOperationsDisposable.add(managedApplyPendingMessageReactionsActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedSynchronizeEmojiKeywordsOperations(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedApplyPendingScheduledMessagesActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) + //self.managedOperationsDisposable.add(managedChatListFilters(postbox: self.postbox, network: self.network).start()) let importantBackgroundOperations: [Signal] = [ managedSynchronizeChatInputStateOperations(postbox: self.postbox, network: self.network) |> map { $0 ? AccountRunningImportantTasks.other : [] }, self.pendingMessageManager.hasPendingMessages |> map { !$0.isEmpty ? AccountRunningImportantTasks.pendingMessages : [] }, + self.pendingUpdateMessageManager.updatingMessageMedia |> map { !$0.isEmpty ? AccountRunningImportantTasks.pendingMessages : [] }, self.accountPresenceManager.isPerformingUpdate() |> map { $0 ? AccountRunningImportantTasks.other : [] }, self.notificationAutolockReportManager.isPerformingUpdate() |> map { $0 ? AccountRunningImportantTasks.other : [] } ] @@ -1112,6 +1137,7 @@ public class Account { self.managedOperationsDisposable.dispose() self.storageSettingsDisposable?.dispose() self.smallLogPostDisposable.dispose() + self.networkTypeDisposable?.dispose() } private func postSmallLogIfNeeded() { @@ -1234,4 +1260,5 @@ public func setupAccount(_ account: Account, fetchCachedResourceRepresentation: account.transformOutgoingMessageMedia = transformOutgoingMessageMedia account.pendingMessageManager.transformOutgoingMessageMedia = transformOutgoingMessageMedia + account.pendingUpdateMessageManager.transformOutgoingMessageMedia = transformOutgoingMessageMedia } diff --git a/submodules/TelegramCore/Sources/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/AccountIntermediateState.swift index 41a9f05278..85ae665380 100644 --- a/submodules/TelegramCore/Sources/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/AccountIntermediateState.swift @@ -113,6 +113,7 @@ struct AccountMutableState { var storedMessages: Set var readInboxMaxIds: [PeerId: MessageId] var namespacesWithHolesFromPreviousState: [PeerId: Set] + var updatedOutgoingUniqueMessageIds: [Int64: Int32] var storedMessagesByPeerIdAndTimestamp: [PeerId: Set] var displayAlerts: [(text: String, isDropAuth: Bool)] = [] @@ -126,6 +127,8 @@ struct AccountMutableState { var externallyUpdatedPeerId = Set() + var authorizationListUpdated: Bool = false + init(initialState: AccountInitialState, initialPeers: [PeerId: Peer], initialReferencedMessageIds: Set, initialStoredMessages: Set, initialReadInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set]) { self.initialState = initialState self.state = initialState.state @@ -138,9 +141,10 @@ struct AccountMutableState { self.storedMessagesByPeerIdAndTimestamp = storedMessagesByPeerIdAndTimestamp self.branchOperationIndex = 0 self.namespacesWithHolesFromPreviousState = [:] + self.updatedOutgoingUniqueMessageIds = [:] } - init(initialState: AccountInitialState, operations: [AccountStateMutationOperation], state: AuthorizedAccountState.State, peers: [PeerId: Peer], chatStates: [PeerId: PeerChatState], peerChatInfos: [PeerId: PeerChatInfo], referencedMessageIds: Set, storedMessages: Set, readInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set], namespacesWithHolesFromPreviousState: [PeerId: Set], displayAlerts: [(text: String, isDropAuth: Bool)], branchOperationIndex: Int) { + init(initialState: AccountInitialState, operations: [AccountStateMutationOperation], state: AuthorizedAccountState.State, peers: [PeerId: Peer], chatStates: [PeerId: PeerChatState], peerChatInfos: [PeerId: PeerChatInfo], referencedMessageIds: Set, storedMessages: Set, readInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set], namespacesWithHolesFromPreviousState: [PeerId: Set], updatedOutgoingUniqueMessageIds: [Int64: Int32], displayAlerts: [(text: String, isDropAuth: Bool)], branchOperationIndex: Int) { self.initialState = initialState self.operations = operations self.state = state @@ -152,12 +156,13 @@ struct AccountMutableState { self.readInboxMaxIds = readInboxMaxIds self.storedMessagesByPeerIdAndTimestamp = storedMessagesByPeerIdAndTimestamp self.namespacesWithHolesFromPreviousState = namespacesWithHolesFromPreviousState + self.updatedOutgoingUniqueMessageIds = updatedOutgoingUniqueMessageIds self.displayAlerts = displayAlerts self.branchOperationIndex = branchOperationIndex } func branch() -> AccountMutableState { - return AccountMutableState(initialState: self.initialState, operations: self.operations, state: self.state, peers: self.peers, chatStates: self.chatStates, peerChatInfos: self.peerChatInfos, referencedMessageIds: self.referencedMessageIds, storedMessages: self.storedMessages, readInboxMaxIds: self.readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: self.storedMessagesByPeerIdAndTimestamp, namespacesWithHolesFromPreviousState: self.namespacesWithHolesFromPreviousState, displayAlerts: self.displayAlerts, branchOperationIndex: self.operations.count) + return AccountMutableState(initialState: self.initialState, operations: self.operations, state: self.state, peers: self.peers, chatStates: self.chatStates, peerChatInfos: self.peerChatInfos, referencedMessageIds: self.referencedMessageIds, storedMessages: self.storedMessages, readInboxMaxIds: self.readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: self.storedMessagesByPeerIdAndTimestamp, namespacesWithHolesFromPreviousState: self.namespacesWithHolesFromPreviousState, updatedOutgoingUniqueMessageIds: self.updatedOutgoingUniqueMessageIds, displayAlerts: self.displayAlerts, branchOperationIndex: self.operations.count) } mutating func merge(_ other: AccountMutableState) { @@ -178,6 +183,7 @@ struct AccountMutableState { self.namespacesWithHolesFromPreviousState[peerId]!.insert(namespace) } } + self.updatedOutgoingUniqueMessageIds.merge(other.updatedOutgoingUniqueMessageIds, uniquingKeysWith: { lhs, _ in lhs }) self.displayAlerts.append(contentsOf: other.displayAlerts) } @@ -404,7 +410,7 @@ struct AccountMutableState { mutating func addOperation(_ operation: AccountStateMutationOperation) { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, /*.UpdateMessageReactions,*/ .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll/*, .UpdateMessageReactions*/, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme: break case let .AddMessages(messages, location): for message in messages { @@ -502,9 +508,10 @@ struct AccountMutableState { } struct AccountFinalState { - let state: AccountMutableState - let shouldPoll: Bool - let incomplete: Bool + var state: AccountMutableState + var shouldPoll: Bool + var incomplete: Bool + var discard: Bool } struct AccountReplayedFinalState { @@ -533,12 +540,13 @@ struct AccountFinalStateEvents { let updatedMaxMessageId: Int32? let updatedQts: Int32? let externallyUpdatedPeerId: Set + let authorizationListUpdated: Bool var isEmpty: Bool { - return self.addedIncomingMessageIds.isEmpty && self.wasScheduledMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty + return self.addedIncomingMessageIds.isEmpty && self.wasScheduledMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated } - init(addedIncomingMessageIds: [MessageId] = [], wasScheduledMessageIds: [MessageId] = [], updatedTypingActivities: [PeerId: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set()) { + init(addedIncomingMessageIds: [MessageId] = [], wasScheduledMessageIds: [MessageId] = [], updatedTypingActivities: [PeerId: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false) { self.addedIncomingMessageIds = addedIncomingMessageIds self.wasScheduledMessageIds = wasScheduledMessageIds self.updatedTypingActivities = updatedTypingActivities @@ -551,6 +559,7 @@ struct AccountFinalStateEvents { self.updatedMaxMessageId = updatedMaxMessageId self.updatedQts = updatedQts self.externallyUpdatedPeerId = externallyUpdatedPeerId + self.authorizationListUpdated = authorizationListUpdated } init(state: AccountReplayedFinalState) { @@ -566,6 +575,7 @@ struct AccountFinalStateEvents { self.updatedMaxMessageId = state.state.state.updatedMaxMessageId self.updatedQts = state.state.state.updatedQts self.externallyUpdatedPeerId = state.state.state.externallyUpdatedPeerId + self.authorizationListUpdated = state.state.state.authorizationListUpdated } func union(with other: AccountFinalStateEvents) -> AccountFinalStateEvents { @@ -589,7 +599,8 @@ struct AccountFinalStateEvents { } let externallyUpdatedPeerId = self.externallyUpdatedPeerId.union(other.externallyUpdatedPeerId) + let authorizationListUpdated = self.authorizationListUpdated || other.authorizationListUpdated - return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId) + return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated) } } diff --git a/submodules/TelegramCore/Sources/AccountManager.swift b/submodules/TelegramCore/Sources/AccountManager.swift index 1dd2d29748..95a689aa1d 100644 --- a/submodules/TelegramCore/Sources/AccountManager.swift +++ b/submodules/TelegramCore/Sources/AccountManager.swift @@ -149,6 +149,10 @@ private var declaredEncodables: Void = { declareEncodable(RestrictedContentMessageAttribute.self, f: { RestrictedContentMessageAttribute(decoder: $0) }) declareEncodable(SendScheduledMessageImmediatelyAction.self, f: { SendScheduledMessageImmediatelyAction(decoder: $0) }) declareEncodable(WalletCollection.self, f: { WalletCollection(decoder: $0) }) + declareEncodable(EmbeddedMediaStickersMessageAttribute.self, f: { EmbeddedMediaStickersMessageAttribute(decoder: $0) }) + declareEncodable(TelegramMediaWebpageAttribute.self, f: { TelegramMediaWebpageAttribute(decoder: $0) }) + declareEncodable(CachedPollOptionResult.self, f: { CachedPollOptionResult(decoder: $0) }) + //declareEncodable(ChatListFiltersState.self, f: { ChatListFiltersState(decoder: $0) }) return }() diff --git a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift index dc9f3f6f5f..6dd6553c6f 100644 --- a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift @@ -962,6 +962,10 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo attributes.append(AutoremoveTimeoutMessageAttribute(timeout: expirationTimer, countdownBeginTime: nil)) } + if type.hasPrefix("auth") { + updatedState.authorizationListUpdated = true + } + let message = StoreMessage(peerId: peerId, namespace: Namespaces.Message.Local, globallyUniqueId: nil, groupingKey: nil, timestamp: date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: peerId, text: messageText, attributes: attributes, media: medias) updatedState.addMessages([message], location: .UpperHistoryBlock) } @@ -1291,8 +1295,13 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo } case let .updatePeerLocated(peers): var peersNearby: [PeerNearby] = [] - for case let .peerLocated(peer, expires, distance) in peers { - peersNearby.append(PeerNearby(id: peer.peerId, expires: expires, distance: distance)) + for peer in peers { + switch peer { + case let .peerLocated(peer, expires, distance): + peersNearby.append(.peer(id: peer.peerId, expires: expires, distance: distance)) + case let .peerSelfLocated(expires): + peersNearby.append(.selfPeer(expires: expires)) + } } updatedState.updatePeersNearby(peersNearby) case let .updateNewScheduledMessage(apiMessage): @@ -1309,6 +1318,8 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo if let theme = TelegramTheme(apiTheme: theme) { updatedState.updateTheme(theme) } + case let .updateMessageID(id, randomId): + updatedState.updatedOutgoingUniqueMessageIds[randomId] = id default: break } @@ -1364,7 +1375,7 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo |> mapToSignal { resultingState -> Signal in return resolveMissingPeerChatInfos(network: network, state: resultingState) |> map { resultingState, resolveError -> AccountFinalState in - return AccountFinalState(state: resultingState, shouldPoll: shouldPoll || hadError || resolveError, incomplete: missingUpdates) + return AccountFinalState(state: resultingState, shouldPoll: shouldPoll || hadError || resolveError, incomplete: missingUpdates, discard: resolveError) } } } @@ -1590,7 +1601,7 @@ func keepPollingChannel(postbox: Postbox, network: Network, peerId: PeerId, stat |> mapToSignal { resultingState -> Signal in return resolveMissingPeerChatInfos(network: network, state: resultingState) |> map { resultingState, _ -> AccountFinalState in - return AccountFinalState(state: resultingState, shouldPoll: false, incomplete: false) + return AccountFinalState(state: resultingState, shouldPoll: false, incomplete: false, discard: false) } } |> mapToSignal { finalState -> Signal in @@ -1761,7 +1772,13 @@ private func resetChannels(network: Network, peers: [Peer], state: AccountMutabl private func pollChannel(network: Network, peer: Peer, state: AccountMutableState) -> Signal<(AccountMutableState, Bool, Int32?), NoError> { if let inputChannel = apiInputChannel(peer) { - let limit: Int32 = 20 + let limit: Int32 + #if DEBUG + limit = 1 + #else + limit = 20 + #endif + let pollPts: Int32 if let channelState = state.chatStates[peer.id] as? ChannelState { pollPts = channelState.pts @@ -2025,7 +2042,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) var currentAddScheduledMessages: OptimizeAddMessagesState? for operation in operations { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, /*.UpdateMessageReactions,*/ .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll/*, .UpdateMessageReactions*/, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme: if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) } @@ -2084,7 +2101,7 @@ private func recordPeerActivityTimestamp(peerId: PeerId, timestamp: Int32, into } } -func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountPeerId: PeerId, mediaBox: MediaBox, encryptionProvider: EncryptionProvider, transaction: Transaction, auxiliaryMethods: AccountAuxiliaryMethods, finalState: AccountFinalState) -> AccountReplayedFinalState? { +func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountPeerId: PeerId, mediaBox: MediaBox, encryptionProvider: EncryptionProvider, transaction: Transaction, auxiliaryMethods: AccountAuxiliaryMethods, finalState: AccountFinalState, removePossiblyDeliveredMessagesUniqueIds: [Int64: PeerId]) -> AccountReplayedFinalState? { let verified = verifyTransaction(transaction, finalState: finalState.state) if !verified { Logger.shared.log("State", "failed to verify final state") @@ -2278,18 +2295,26 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP } } case let .DeleteMessagesWithGlobalIds(ids): + var resourceIds: [WrappedMediaResourceId] = [] transaction.deleteMessagesWithGlobalIds(ids, forEachMedia: { media in - processRemovedMedia(mediaBox, media) + addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) }) + if !resourceIds.isEmpty { + let _ = mediaBox.removeCachedResources(Set(resourceIds)).start() + } case let .DeleteMessages(ids): deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: ids) case let .UpdateMinAvailableMessage(id): if let message = transaction.getMessage(id) { updatePeerChatInclusionWithMinTimestamp(transaction: transaction, id: id.peerId, minTimestamp: message.timestamp, forceRootGroupIfNotExists: false) } + var resourceIds: [WrappedMediaResourceId] = [] transaction.deleteMessagesInRange(peerId: id.peerId, namespace: id.namespace, minId: 1, maxId: id.id, forEachMedia: { media in - processRemovedMedia(mediaBox, media) + addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) }) + if !resourceIds.isEmpty { + let _ = mediaBox.removeCachedResources(Set(resourceIds)).start() + } case let .UpdatePeerChatInclusion(peerId, groupId, changedGroup): let currentInclusion = transaction.getPeerChatListInclusion(peerId) var currentPinningIndex: UInt16? @@ -2324,17 +2349,28 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP case let .UpdateMessagePoll(pollId, apiPoll, results): if let poll = transaction.getMedia(pollId) as? TelegramMediaPoll { var updatedPoll = poll - if let apiPoll = apiPoll { - switch apiPoll { - case let .poll(id, flags, question, answers): - updatedPoll = TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0) - } - } - let resultsMin: Bool switch results { - case let .pollResults(pollResults): - resultsMin = (pollResults.flags & (1 << 0)) != 0 + case let .pollResults(pollResults): + resultsMin = (pollResults.flags & (1 << 0)) != 0 + } + if let apiPoll = apiPoll { + switch apiPoll { + case let .poll(id, flags, question, answers): + let publicity: TelegramMediaPollPublicity + if (flags & (1 << 1)) != 0 { + publicity = .public + } else { + publicity = .anonymous + } + let kind: TelegramMediaPollKind + if (flags & (1 << 3)) != 0 { + kind = .quiz + } else { + kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0) + } + updatedPoll = TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: poll.results, isClosed: (flags & (1 << 0)) != 0) + } } updatedPoll = updatedPoll.withUpdatedResults(TelegramMediaPollResults(apiResults: results), min: resultsMin) updateMessageMedia(transaction: transaction, id: pollId, media: updatedPoll) @@ -2960,5 +2996,12 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP addedIncomingMessageIds.append(contentsOf: addedSecretMessageIds) + for (uniqueId, messageIdValue) in finalState.state.updatedOutgoingUniqueMessageIds { + if let peerId = removePossiblyDeliveredMessagesUniqueIds[uniqueId] { + let messageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageIdValue) + deleteMessagesInteractively(transaction: transaction, stateManager: nil, postbox: postbox, messageIds: [messageId], type: .forEveryone, deleteAllInGroup: false, removeIfPossiblyDelivered: false) + } + } + return AccountReplayedFinalState(state: finalState, addedIncomingMessageIds: addedIncomingMessageIds, wasScheduledMessageIds: wasScheduledMessageIds, addedSecretMessageIds: addedSecretMessageIds, updatedTypingActivities: updatedTypingActivities, updatedWebpages: updatedWebpages, updatedCalls: updatedCalls, updatedPeersNearby: updatedPeersNearby, isContactUpdates: isContactUpdates, delayNotificatonsUntil: delayNotificatonsUntil) } diff --git a/submodules/TelegramCore/Sources/AccountStateManager.swift b/submodules/TelegramCore/Sources/AccountStateManager.swift index 0b70467eb2..3a7a952951 100644 --- a/submodules/TelegramCore/Sources/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/AccountStateManager.swift @@ -57,7 +57,8 @@ public final class AccountStateManager { private let shouldKeepOnlinePresence: Signal private let peerInputActivityManager: PeerInputActivityManager - private let auxiliaryMethods: AccountAuxiliaryMethods + let auxiliaryMethods: AccountAuxiliaryMethods + var transformOutgoingMessageMedia: TransformOutgoingMessageMedia? private var updateService: UpdateMessageService? private let updateServiceDisposable = MetaDisposable() @@ -75,6 +76,8 @@ public final class AccountStateManager { private let operationDisposable = MetaDisposable() private var operationTimer: SignalKitTimer? + private var removePossiblyDeliveredMessagesUniqueIds: [Int64: PeerId] = [:] + private var nextId: Int32 = 0 private func getNextId() -> Int32 { self.nextId += 1 @@ -130,6 +133,11 @@ public final class AccountStateManager { return self.significantStateUpdateCompletedPipe.signal() } + private let authorizationListUpdatesPipe = ValuePipe() + var authorizationListUpdates: Signal { + return self.authorizationListUpdatesPipe.signal() + } + private var updatedWebpageContexts: [MediaId: UpdatedWebpageSubscriberContext] = [:] private var updatedPeersNearbyContext = UpdatedPeersNearbySubscriberContext() @@ -364,6 +372,7 @@ public final class AccountStateManager { case let .pollDifference(currentEvents): self.operationTimer?.invalidate() self.currentIsUpdatingValue = true + let queue = self.queue let accountManager = self.accountManager let postbox = self.postbox let network = self.network @@ -379,7 +388,7 @@ public final class AccountStateManager { } } |> take(1) - |> mapToSignal { state -> Signal<(difference: Api.updates.Difference?, finalStatte: AccountReplayedFinalState?, skipBecauseOfError: Bool), NoError> in + |> mapToSignal { [weak self] state -> Signal<(difference: Api.updates.Difference?, finalStatte: AccountReplayedFinalState?, skipBecauseOfError: Bool), NoError> in if let authorizedState = state.state { var flags: Int32 = 0 var ptsTotalLimit: Int32? = nil @@ -418,15 +427,17 @@ public final class AccountStateManager { return .single((nil, nil, false)) } else { return finalStateWithDifference(postbox: postbox, network: network, state: state, difference: difference) + |> deliverOn(queue) |> mapToSignal { finalState -> Signal<(difference: Api.updates.Difference?, finalStatte: AccountReplayedFinalState?, skipBecauseOfError: Bool), NoError> in if !finalState.state.preCachedResources.isEmpty { for (resource, data) in finalState.state.preCachedResources { mediaBox.storeResourceData(resource.id, data: data) } } + let removePossiblyDeliveredMessagesUniqueIds = self?.removePossiblyDeliveredMessagesUniqueIds ?? Dictionary() return postbox.transaction { transaction -> (difference: Api.updates.Difference?, finalStatte: AccountReplayedFinalState?, skipBecauseOfError: Bool) in let startTime = CFAbsoluteTimeGetCurrent() - let replayedState = replayFinalState(accountManager: accountManager, postbox: postbox, accountPeerId: accountPeerId, mediaBox: mediaBox, encryptionProvider: network.encryptionProvider, transaction: transaction, auxiliaryMethods: auxiliaryMethods, finalState: finalState) + let replayedState = replayFinalState(accountManager: accountManager, postbox: postbox, accountPeerId: accountPeerId, mediaBox: mediaBox, encryptionProvider: network.encryptionProvider, transaction: transaction, auxiliaryMethods: auxiliaryMethods, finalState: finalState, removePossiblyDeliveredMessagesUniqueIds: removePossiblyDeliveredMessagesUniqueIds) let deltaTime = CFAbsoluteTimeGetCurrent() - startTime if deltaTime > 1.0 { Logger.shared.log("State", "replayFinalState took \(deltaTime)s") @@ -530,23 +541,30 @@ public final class AccountStateManager { let mediaBox = postbox.mediaBox let queue = self.queue let signal = initialStateWithUpdateGroups(postbox: postbox, groups: groups) - |> mapToSignal { state -> Signal<(AccountReplayedFinalState?, AccountFinalState), NoError> in + |> mapToSignal { [weak self] state -> Signal<(AccountReplayedFinalState?, AccountFinalState), NoError> in return finalStateWithUpdateGroups(postbox: postbox, network: network, state: state, groups: groups) + |> deliverOn(queue) |> mapToSignal { finalState in - if !finalState.state.preCachedResources.isEmpty { + if !finalState.discard && !finalState.state.preCachedResources.isEmpty { for (resource, data) in finalState.state.preCachedResources { postbox.mediaBox.storeResourceData(resource.id, data: data) } } + let removePossiblyDeliveredMessagesUniqueIds = self?.removePossiblyDeliveredMessagesUniqueIds ?? Dictionary() + return postbox.transaction { transaction -> AccountReplayedFinalState? in - let startTime = CFAbsoluteTimeGetCurrent() - let result = replayFinalState(accountManager: accountManager, postbox: postbox, accountPeerId: accountPeerId, mediaBox: mediaBox, encryptionProvider: network.encryptionProvider, transaction: transaction, auxiliaryMethods: auxiliaryMethods, finalState: finalState) - let deltaTime = CFAbsoluteTimeGetCurrent() - startTime - if deltaTime > 1.0 { - Logger.shared.log("State", "replayFinalState took \(deltaTime)s") + if finalState.discard { + return nil + } else { + let startTime = CFAbsoluteTimeGetCurrent() + let result = replayFinalState(accountManager: accountManager, postbox: postbox, accountPeerId: accountPeerId, mediaBox: mediaBox, encryptionProvider: network.encryptionProvider, transaction: transaction, auxiliaryMethods: auxiliaryMethods, finalState: finalState, removePossiblyDeliveredMessagesUniqueIds: removePossiblyDeliveredMessagesUniqueIds) + let deltaTime = CFAbsoluteTimeGetCurrent() - startTime + if deltaTime > 1.0 { + Logger.shared.log("State", "replayFinalState took \(deltaTime)s") + } + return result } - return result } |> map({ ($0, finalState) }) |> deliverOn(queue) @@ -698,6 +716,10 @@ public final class AccountStateManager { if !events.externallyUpdatedPeerId.isEmpty { self.externallyUpdatedPeerIdsPipe.putNext(Array(events.externallyUpdatedPeerId)) } + + if events.authorizationListUpdated { + self.authorizationListUpdatesPipe.putNext(Void()) + } case let .pollCompletion(pollId, preMessageIds, preSubscribers): if self.operations.count > 1 { self.operations.removeFirst() @@ -745,9 +767,10 @@ public final class AccountStateManager { let mediaBox = self.postbox.mediaBox let network = self.network let auxiliaryMethods = self.auxiliaryMethods + let removePossiblyDeliveredMessagesUniqueIds = self.removePossiblyDeliveredMessagesUniqueIds let signal = self.postbox.transaction { transaction -> AccountReplayedFinalState? in let startTime = CFAbsoluteTimeGetCurrent() - let result = replayFinalState(accountManager: accountManager, postbox: postbox, accountPeerId: accountPeerId, mediaBox: mediaBox, encryptionProvider: network.encryptionProvider, transaction: transaction, auxiliaryMethods: auxiliaryMethods, finalState: finalState) + let result = replayFinalState(accountManager: accountManager, postbox: postbox, accountPeerId: accountPeerId, mediaBox: mediaBox, encryptionProvider: network.encryptionProvider, transaction: transaction, auxiliaryMethods: auxiliaryMethods, finalState: finalState, removePossiblyDeliveredMessagesUniqueIds: removePossiblyDeliveredMessagesUniqueIds) let deltaTime = CFAbsoluteTimeGetCurrent() - startTime if deltaTime > 1.0 { Logger.shared.log("State", "replayFinalState took \(deltaTime)s") @@ -1006,6 +1029,12 @@ public final class AccountStateManager { } completion(nil) } + + func removePossiblyDeliveredMessages(uniqueIds: [Int64: PeerId]) { + self.queue.async { + self.removePossiblyDeliveredMessagesUniqueIds.merge(uniqueIds, uniquingKeysWith: { _, rhs in rhs }) + } + } } public func messagesForNotification(transaction: Transaction, id: MessageId, alwaysReturnMessage: Bool) -> (messages: [Message], notify: Bool, sound: PeerMessageSound, displayContents: Bool) { diff --git a/submodules/TelegramCore/Sources/AccountViewTracker.swift b/submodules/TelegramCore/Sources/AccountViewTracker.swift index 02275089c9..77d5c28d65 100644 --- a/submodules/TelegramCore/Sources/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/AccountViewTracker.swift @@ -253,6 +253,10 @@ public final class AccountViewTracker { private var nextUpdatedViewCountDisposableId: Int32 = 0 private var updatedViewCountDisposables = DisposableDict() + private var updatedSeenLiveLocationMessageIdsAndTimestamps: [MessageId: Int32] = [:] + private var nextSeenLiveLocationDisposableId: Int32 = 0 + private var seenLiveLocationDisposables = DisposableDict() + private var updatedUnsupportedMediaMessageIdsAndTimestamps: [MessageId: Int32] = [:] private var nextUpdatedUnsupportedMediaDisposableId: Int32 = 0 private var updatedUnsupportedMediaDisposables = DisposableDict() @@ -584,6 +588,61 @@ public final class AccountViewTracker { } } + public func updateSeenLiveLocationForMessageIds(messageIds: Set) { + self.queue.async { + var addedMessageIds: [MessageId] = [] + let timestamp = Int32(CFAbsoluteTimeGetCurrent()) + for messageId in messageIds { + let messageTimestamp = self.updatedSeenLiveLocationMessageIdsAndTimestamps[messageId] + if messageTimestamp == nil || messageTimestamp! < timestamp - 1 * 60 { + self.updatedSeenLiveLocationMessageIdsAndTimestamps[messageId] = timestamp + addedMessageIds.append(messageId) + } + } + if !addedMessageIds.isEmpty { + for (peerId, messageIds) in messagesIdsGroupedByPeerId(Set(addedMessageIds)) { + let disposableId = self.nextSeenLiveLocationDisposableId + self.nextSeenLiveLocationDisposableId += 1 + + if let account = self.account { + let signal = (account.postbox.transaction { transaction -> Signal in + if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { + let request: Signal + switch inputPeer { + case .inputPeerChat, .inputPeerSelf, .inputPeerUser: + request = account.network.request(Api.functions.messages.readMessageContents(id: messageIds.map { $0.id })) + |> map { _ in true } + case let .inputPeerChannel(channelId, accessHash): + request = account.network.request(Api.functions.channels.readMessageContents(channel: .inputChannel(channelId: channelId, accessHash: accessHash), id: messageIds.map { $0.id })) + |> map { _ in true } + default: + return .complete() + } + + return request + |> `catch` { _ -> Signal in + return .single(false) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } else { + return .complete() + } + } + |> switchToLatest) + |> afterDisposed { [weak self] in + self?.queue.async { + self?.seenLiveLocationDisposables.set(nil, forKey: disposableId) + } + } + self.seenLiveLocationDisposables.set(signal.start(), forKey: disposableId) + } + } + } + } + } + public func updateUnsupportedMediaForMessageIds(messageIds: Set) { self.queue.async { var addedMessageIds: [MessageId] = [] @@ -772,9 +831,22 @@ public final class AccountViewTracker { self.cachedDataContexts[peerId] = context } context.timestamp = CFAbsoluteTimeGetCurrent() - if let account = self.account { - context.disposable.set(combineLatest(fetchAndUpdateSupplementalCachedPeerData(peerId: peerId, network: account.network, postbox: account.postbox), fetchAndUpdateCachedPeerData(accountPeerId: account.peerId, peerId: peerId, network: account.network, postbox: account.postbox)).start()) + guard let account = self.account else { + return } + let queue = self.queue + context.disposable.set(combineLatest(fetchAndUpdateSupplementalCachedPeerData(peerId: peerId, network: account.network, postbox: account.postbox), fetchAndUpdateCachedPeerData(accountPeerId: account.peerId, peerId: peerId, network: account.network, postbox: account.postbox)).start(next: { [weak self] supplementalStatus, cachedStatus in + queue.async { + guard let strongSelf = self else { + return + } + if !supplementalStatus || !cachedStatus { + if let existingContext = strongSelf.cachedDataContexts[peerId] { + existingContext.timestamp = nil + } + } + } + })) } } @@ -801,9 +873,22 @@ public final class AccountViewTracker { context.viewIds.insert(viewId) if dataUpdated { - if let account = self.account { - context.disposable.set(combineLatest(fetchAndUpdateSupplementalCachedPeerData(peerId: peerId, network: account.network, postbox: account.postbox), fetchAndUpdateCachedPeerData(accountPeerId: account.peerId, peerId: peerId, network: account.network, postbox: account.postbox)).start()) + guard let account = self.account else { + return } + let queue = self.queue + context.disposable.set(combineLatest(fetchAndUpdateSupplementalCachedPeerData(peerId: peerId, network: account.network, postbox: account.postbox), fetchAndUpdateCachedPeerData(accountPeerId: account.peerId, peerId: peerId, network: account.network, postbox: account.postbox)).start(next: { [weak self] supplementalStatus, cachedStatus in + queue.async { + guard let strongSelf = self else { + return + } + if !supplementalStatus || !cachedStatus { + if let existingContext = strongSelf.cachedDataContexts[peerId] { + existingContext.timestamp = nil + } + } + } + })) } } } @@ -979,7 +1064,7 @@ public final class AccountViewTracker { } } - public func aroundMessageHistoryViewForLocation(_ chatLocation: ChatLocation, index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, count: Int, fixedCombinedReadStates: MessageHistoryViewReadState?, tagMask: MessageTags? = nil, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + public func aroundMessageHistoryViewForLocation(_ chatLocation: ChatLocation, index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, count: Int, clipHoles: Bool = true, fixedCombinedReadStates: MessageHistoryViewReadState?, tagMask: MessageTags? = nil, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { if let account = self.account { let inputAnchor: HistoryViewInputAnchor switch index { @@ -990,7 +1075,7 @@ public final class AccountViewTracker { case let .message(index): inputAnchor = .index(index) } - let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: inputAnchor, count: count, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tagMask: tagMask, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData)) + let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: inputAnchor, count: count, clipHoles: clipHoles, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tagMask: tagMask, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData)) return wrappedMessageHistorySignal(chatLocation: chatLocation, signal: signal, addHoleIfNeeded: false) } else { return .never() @@ -1136,7 +1221,7 @@ public final class AccountViewTracker { return true } - let key = PostboxViewKey.globalMessageTags(globalTag: type == .all ? GlobalMessageTags.Calls : GlobalMessageTags.MissedCalls, position: index, count: 200, groupingPredicate: groupingPredicate) + let key = PostboxViewKey.globalMessageTags(globalTag: type == .all ? GlobalMessageTags.Calls : GlobalMessageTags.MissedCalls, position: index, count: count, groupingPredicate: groupingPredicate) let signal = account.postbox.combinedView(keys: [key]) |> map { view -> GlobalMessageTagsView in let messageView = view.views[key] as! GlobalMessageTagsView return messageView @@ -1245,17 +1330,17 @@ public final class AccountViewTracker { }) } - public func tailChatListView(groupId: PeerGroupId, count: Int) -> Signal<(ChatListView, ViewUpdateType), NoError> { + public func tailChatListView(groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)? = nil, count: Int) -> Signal<(ChatListView, ViewUpdateType), NoError> { if let account = self.account { - return self.wrappedChatListView(signal: account.postbox.tailChatListView(groupId: groupId, count: count, summaryComponents: ChatListEntrySummaryComponents(tagSummary: ChatListEntryMessageTagSummaryComponent(tag: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud), actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent(type: PendingMessageActionType.consumeUnseenPersonalMessage, namespace: Namespaces.Message.Cloud)))) + return self.wrappedChatListView(signal: account.postbox.tailChatListView(groupId: groupId, filterPredicate: filterPredicate, count: count, summaryComponents: ChatListEntrySummaryComponents(tagSummary: ChatListEntryMessageTagSummaryComponent(tag: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud), actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent(type: PendingMessageActionType.consumeUnseenPersonalMessage, namespace: Namespaces.Message.Cloud)))) } else { return .never() } } - public func aroundChatListView(groupId: PeerGroupId, index: ChatListIndex, count: Int) -> Signal<(ChatListView, ViewUpdateType), NoError> { + public func aroundChatListView(groupId: PeerGroupId, filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool)? = nil, index: ChatListIndex, count: Int) -> Signal<(ChatListView, ViewUpdateType), NoError> { if let account = self.account { - return self.wrappedChatListView(signal: account.postbox.aroundChatListView(groupId: groupId, index: index, count: count, summaryComponents: ChatListEntrySummaryComponents(tagSummary: ChatListEntryMessageTagSummaryComponent(tag: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud), actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent(type: PendingMessageActionType.consumeUnseenPersonalMessage, namespace: Namespaces.Message.Cloud)))) + return self.wrappedChatListView(signal: account.postbox.aroundChatListView(groupId: groupId, filterPredicate: filterPredicate, index: index, count: count, summaryComponents: ChatListEntrySummaryComponents(tagSummary: ChatListEntryMessageTagSummaryComponent(tag: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud), actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent(type: PendingMessageActionType.consumeUnseenPersonalMessage, namespace: Namespaces.Message.Cloud)))) } else { return .never() } diff --git a/submodules/TelegramCore/Sources/ActiveSessionsContext.swift b/submodules/TelegramCore/Sources/ActiveSessionsContext.swift index 7beeaba0a8..c060382a5a 100644 --- a/submodules/TelegramCore/Sources/ActiveSessionsContext.swift +++ b/submodules/TelegramCore/Sources/ActiveSessionsContext.swift @@ -8,7 +8,7 @@ public struct ActiveSessionsContextState: Equatable { public var sessions: [RecentAccountSession] } -public final class ActiveSessionsContext { +private final class ActiveSessionsContextImpl { private let account: Account private var _state: ActiveSessionsContextState { didSet { @@ -18,13 +18,14 @@ public final class ActiveSessionsContext { } } private let _statePromise = Promise() - public var state: Signal { + var state: Signal { return self._statePromise.get() } private let disposable = MetaDisposable() + private var authorizationListUpdatesDisposable: Disposable? - public init(account: Account) { + init(account: Account) { assert(Queue.mainQueue().isCurrent()) self.account = account @@ -32,14 +33,20 @@ public final class ActiveSessionsContext { self._statePromise.set(.single(self._state)) self.loadMore() + + self.authorizationListUpdatesDisposable = (account.stateManager.authorizationListUpdates + |> deliverOnMainQueue).start(next: { [weak self] _ in + self?.loadMore() + }) } deinit { assert(Queue.mainQueue().isCurrent()) self.disposable.dispose() + self.authorizationListUpdatesDisposable?.dispose() } - public func loadMore() { + func loadMore() { assert(Queue.mainQueue().isCurrent()) if self._state.isLoadingMore { @@ -59,7 +66,23 @@ public final class ActiveSessionsContext { })) } - public func remove(hash: Int64) -> Signal { + func addSession(_ session: RecentAccountSession) { + var mergedSessions = self._state.sessions + var found = false + for i in 0 ..< mergedSessions.count { + if mergedSessions[i].hash == session.hash { + found = true + break + } + } + if !found { + mergedSessions.insert(session, at: 0) + } + + self._state = ActiveSessionsContextState(isLoadingMore: self._state.isLoadingMore, sessions: mergedSessions) + } + + func remove(hash: Int64) -> Signal { assert(Queue.mainQueue().isCurrent()) return terminateAccountSession(account: self.account, hash: hash) @@ -82,7 +105,7 @@ public final class ActiveSessionsContext { } } - public func removeOther() -> Signal { + func removeOther() -> Signal { return terminateOtherAccountSessions(account: self.account) |> deliverOnMainQueue |> mapToSignal { [weak self] _ -> Signal in @@ -97,3 +120,173 @@ public final class ActiveSessionsContext { } } } + +public final class ActiveSessionsContext { + private let impl: QueueLocalObject + + public var state: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.state.start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } + + public init(account: Account) { + self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { + return ActiveSessionsContextImpl(account: account) + }) + } + + public func loadMore() { + self.impl.with { impl in + impl.loadMore() + } + } + + func addSession(_ session: RecentAccountSession) { + self.impl.with { impl in + impl.addSession(session) + } + } + + public func remove(hash: Int64) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.remove(hash: hash).start(error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + })) + } + return disposable + } + } + + public func removeOther() -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.removeOther().start(error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + })) + } + return disposable + } + } +} + +public struct WebSessionsContextState: Equatable { + public var isLoadingMore: Bool + public var sessions: [WebAuthorization] + public var peers: [PeerId: Peer] + + public static func ==(lhs: WebSessionsContextState, rhs: WebSessionsContextState) -> Bool { + if lhs.isLoadingMore != rhs.isLoadingMore { + return false + } + if lhs.sessions != rhs.sessions { + return false + } + if !arePeerDictionariesEqual(lhs.peers, rhs.peers) { + return false + } + return true + } +} + +public final class WebSessionsContext { + private let account: Account + private var _state: WebSessionsContextState { + didSet { + if self._state != oldValue { + self._statePromise.set(.single(self._state)) + } + } + } + private let _statePromise = Promise() + public var state: Signal { + return self._statePromise.get() + } + + private let disposable = MetaDisposable() + + public init(account: Account) { + assert(Queue.mainQueue().isCurrent()) + + self.account = account + self._state = WebSessionsContextState(isLoadingMore: false, sessions: [], peers: [:]) + self._statePromise.set(.single(self._state)) + + self.loadMore() + } + + deinit { + assert(Queue.mainQueue().isCurrent()) + self.disposable.dispose() + } + + public func loadMore() { + assert(Queue.mainQueue().isCurrent()) + + if self._state.isLoadingMore { + return + } + self._state = WebSessionsContextState(isLoadingMore: true, sessions: self._state.sessions, peers: self._state.peers) + self.disposable.set((webSessions(network: account.network) + |> map { result -> (sessions: [WebAuthorization], peers: [PeerId: Peer], canLoadMore: Bool) in + return (result.0, result.1, false) + } + |> deliverOnMainQueue).start(next: { [weak self] (sessions, peers, canLoadMore) in + guard let strongSelf = self else { + return + } + + strongSelf._state = WebSessionsContextState(isLoadingMore: false, sessions: sessions, peers: peers) + })) + } + + public func remove(hash: Int64) -> Signal { + assert(Queue.mainQueue().isCurrent()) + + return terminateWebSession(network: self.account.network, hash: hash) + |> deliverOnMainQueue + |> mapToSignal { [weak self] _ -> Signal in + guard let strongSelf = self else { + return .complete() + } + + var mergedSessions = strongSelf._state.sessions + for i in 0 ..< mergedSessions.count { + if mergedSessions[i].hash == hash { + mergedSessions.remove(at: i) + break + } + } + + strongSelf._state = WebSessionsContextState(isLoadingMore: strongSelf._state.isLoadingMore, sessions: mergedSessions, peers: strongSelf._state.peers) + return .complete() + } + } + + public func removeAll() -> Signal { + return terminateAllWebSessions(network: self.account.network) + |> deliverOnMainQueue + |> mapToSignal { [weak self] _ -> Signal in + guard let strongSelf = self else { + return .complete() + } + + strongSelf._state = WebSessionsContextState(isLoadingMore: strongSelf._state.isLoadingMore, sessions: [], peers: [:]) + return .complete() + } + } +} + diff --git a/submodules/TelegramCore/Sources/AddPeerMember.swift b/submodules/TelegramCore/Sources/AddPeerMember.swift index 5b3b6cac71..4bc8e65ca0 100644 --- a/submodules/TelegramCore/Sources/AddPeerMember.swift +++ b/submodules/TelegramCore/Sources/AddPeerMember.swift @@ -10,6 +10,7 @@ public enum AddGroupMemberError { case generic case groupFull case privacy + case tooManyChannels } public func addGroupMember(account: Account, peerId: PeerId, memberId: PeerId) -> Signal { @@ -19,12 +20,14 @@ public func addGroupMember(account: Account, peerId: PeerId, memberId: PeerId) - return account.network.request(Api.functions.messages.addChatUser(chatId: group.id.id, userId: inputUser, fwdLimit: 100)) |> mapError { error -> AddGroupMemberError in switch error.errorDescription { - case "USERS_TOO_MUCH": - return .groupFull - case "USER_PRIVACY_RESTRICTED": - return .privacy - default: - return .generic + case "USERS_TOO_MUCH": + return .groupFull + case "USER_PRIVACY_RESTRICTED": + return .privacy + case "USER_CHANNELS_TOO_MUCH": + return .tooManyChannels + default: + return .generic } } |> mapToSignal { result -> Signal in @@ -95,7 +98,7 @@ public func addChannelMember(account: Account, peerId: PeerId, memberId: PeerId) return .fail(.tooMuchJoined) case "USERS_TOO_MUCH": return .fail(.limitExceeded) - case "USER_PRIVACY_RESTRICTED": + case "USER_PRIVACY_RESTRICTED", "USER_NOT_MUTUAL_CONTACT": return .fail(.restricted) case "USER_BOT": return .fail(.bot(memberId)) @@ -191,7 +194,7 @@ public func addChannelMembers(account: Account, peerId: PeerId, memberIds: [Peer switch error.errorDescription { case "CHANNELS_TOO_MUCH": return .tooMuchJoined - case "USER_PRIVACY_RESTRICTED": + case "USER_PRIVACY_RESTRICTED", "USER_NOT_MUTUAL_CONTACT": return .restricted case "USERS_TOO_MUCH": return .limitExceeded diff --git a/submodules/TelegramCore/Sources/AddressNames.swift b/submodules/TelegramCore/Sources/AddressNames.swift index d98ddd5239..4a8092a39e 100644 --- a/submodules/TelegramCore/Sources/AddressNames.swift +++ b/submodules/TelegramCore/Sources/AddressNames.swift @@ -99,7 +99,7 @@ public func addressNameAvailability(account: Account, domain: AddressNameDomain, return .single(.invalid) } case .theme: - return account.network.request(Api.functions.account.createTheme(slug: name, title: "", document: .inputDocumentEmpty)) + return account.network.request(Api.functions.account.createTheme(flags: 0, slug: name, title: "", document: .inputDocumentEmpty, settings: nil)) |> map { _ -> AddressNameAvailability in return .available } @@ -162,7 +162,7 @@ public func updateAddressName(account: Account, domain: AddressNameDomain, name: } case let .theme(theme): let flags: Int32 = 1 << 0 - return account.network.request(Api.functions.account.updateTheme(flags: flags, format: telegramThemeFormat, theme: .inputTheme(id: theme.id, accessHash: theme.accessHash), slug: nil, title: nil, document: nil)) + return account.network.request(Api.functions.account.updateTheme(flags: flags, format: telegramThemeFormat, theme: .inputTheme(id: theme.id, accessHash: theme.accessHash), slug: nil, title: nil, document: nil, settings: nil)) |> mapError { _ -> UpdateAddressNameError in return .generic } diff --git a/submodules/TelegramCore/Sources/ApiUtils.swift b/submodules/TelegramCore/Sources/ApiUtils.swift index 8b4b19cb23..f7beff73f4 100644 --- a/submodules/TelegramCore/Sources/ApiUtils.swift +++ b/submodules/TelegramCore/Sources/ApiUtils.swift @@ -4,7 +4,7 @@ import TelegramApi import SyncCore -extension PeerReference { +public extension PeerReference { var id: PeerId { switch self { case let .user(id, _): @@ -15,7 +15,9 @@ extension PeerReference { return PeerId(namespace: Namespaces.Peer.CloudChannel, id: id) } } - +} + +extension PeerReference { var inputPeer: Api.InputPeer { switch self { case let .user(id, accessHash): diff --git a/submodules/TelegramCore/Sources/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/ApplyUpdateMessage.swift index 21480f45a2..10e51ef71a 100644 --- a/submodules/TelegramCore/Sources/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/ApplyUpdateMessage.swift @@ -5,7 +5,7 @@ import SwiftSignalKit import SyncCore -func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox) { +func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox, force: Bool) { if let fromImage = from as? TelegramMediaImage, let toImage = to as? TelegramMediaImage { let fromSmallestRepresentation = smallestImageRepresentation(fromImage.representations) if let fromSmallestRepresentation = fromSmallestRepresentation, let toSmallestRepresentation = smallestImageRepresentation(toImage.representations) { @@ -23,7 +23,7 @@ func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox) { if let fromPreview = smallestImageRepresentation(fromFile.previewRepresentations), let toPreview = smallestImageRepresentation(toFile.previewRepresentations) { postbox.mediaBox.moveResourceData(from: fromPreview.resource.id, to: toPreview.resource.id) } - if (fromFile.size == toFile.size || fromFile.resource.size == toFile.resource.size) && fromFile.mimeType == toFile.mimeType { + if (force || fromFile.size == toFile.size || fromFile.resource.size == toFile.resource.size) && fromFile.mimeType == toFile.mimeType { postbox.mediaBox.moveResourceData(from: fromFile.resource.id, to: toFile.resource.id) } } @@ -35,7 +35,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes var apiMessage: Api.Message? for resultMessage in result.messages { - if let id = resultMessage.id(namespace: Namespaces.Message.allScheduled.contains(message.id.namespace) ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) { + if let id = resultMessage.id() { if id.peerId == message.id.peerId { apiMessage = resultMessage break @@ -43,7 +43,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes } } - if let apiMessage = apiMessage, let id = apiMessage.id(namespace: message.scheduleTime != nil ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) { + if let apiMessage = apiMessage, let id = apiMessage.id() { messageId = id.id } else { messageId = result.rawMessageIds.first @@ -80,7 +80,14 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes transaction.updateMessage(message.id, update: { currentMessage in let updatedId: MessageId if let messageId = messageId { - let namespace = Namespaces.Message.allScheduled.contains(message.id.namespace) ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud + var namespace: MessageId.Namespace = Namespaces.Message.Cloud + if let updatedTimestamp = updatedTimestamp { + if message.scheduleTime != nil && message.scheduleTime == updatedTimestamp { + namespace = Namespaces.Message.ScheduledCloud + } + } else if Namespaces.Message.allScheduled.contains(message.id.namespace) { + namespace = Namespaces.Message.ScheduledCloud + } updatedId = MessageId(peerId: currentMessage.id.peerId, namespace: namespace, id: messageId) } else { updatedId = currentMessage.id @@ -114,6 +121,15 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes updatedAttributes.append(TextEntitiesMessageAttribute(entities: messageTextEntitiesFromApiEntities(entities))) } + if Namespaces.Message.allScheduled.contains(message.id.namespace) && updatedId.namespace == Namespaces.Message.Cloud { + for i in 0 ..< updatedAttributes.count { + if updatedAttributes[i] is OutgoingScheduleInfoMessageAttribute { + updatedAttributes.remove(at: i) + break + } + } + } + attributes = updatedAttributes text = currentMessage.text @@ -136,11 +152,11 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes } if let fromMedia = currentMessage.media.first, let toMedia = media.first { - applyMediaResourceChanges(from: fromMedia, to: toMedia, postbox: postbox) + applyMediaResourceChanges(from: fromMedia, to: toMedia, postbox: postbox, force: false) } if forwardInfo == nil { - inner: for media in message.media { + inner: for media in media { if let file = media as? TelegramMediaFile { for attribute in file.attributes { switch attribute { @@ -215,7 +231,7 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage let updatedRawMessageIds = result.updatedRawMessageIds var namespace = Namespaces.Message.Cloud - if let message = messages.first, Namespaces.Message.allScheduled.contains(message.id.namespace) { + if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { namespace = Namespaces.Message.ScheduledCloud } @@ -225,7 +241,7 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage resultMessages[id] = resultMessage } } - + var mapping: [(Message, MessageIndex, StoreMessage)] = [] for message in messages { @@ -295,7 +311,7 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage } if let fromMedia = currentMessage.media.first, let toMedia = media.first { - applyMediaResourceChanges(from: fromMedia, to: toMedia, postbox: postbox) + applyMediaResourceChanges(from: fromMedia, to: toMedia, postbox: postbox, force: false) } if storeForwardInfo == nil { diff --git a/submodules/TelegramCore/Sources/AuthTransfer.swift b/submodules/TelegramCore/Sources/AuthTransfer.swift new file mode 100644 index 0000000000..e661aa715d --- /dev/null +++ b/submodules/TelegramCore/Sources/AuthTransfer.swift @@ -0,0 +1,167 @@ +import Foundation +import Postbox +import TelegramApi +import SyncCore +import SwiftSignalKit + +public struct AuthTransferExportedToken { + public let value: Data + public let validUntil: Int32 +} + +public enum ExportAuthTransferTokenError { + case generic + case limitExceeded +} + +public enum ExportAuthTransferTokenResult { + case displayToken(AuthTransferExportedToken) + case changeAccountAndRetry(UnauthorizedAccount) + case loggedIn + case passwordRequested(UnauthorizedAccount) +} + +public func exportAuthTransferToken(accountManager: AccountManager, account: UnauthorizedAccount, otherAccountUserIds: [Int32], syncContacts: Bool) -> Signal { + return account.network.request(Api.functions.auth.exportLoginToken(apiId: account.networkArguments.apiId, apiHash: account.networkArguments.apiHash, exceptIds: otherAccountUserIds)) + |> map(Optional.init) + |> `catch` { error -> Signal in + if error.errorDescription == "SESSION_PASSWORD_NEEDED" { + return account.network.request(Api.functions.account.getPassword(), automaticFloodWait: false) + |> mapError { error -> ExportAuthTransferTokenError in + if error.errorDescription.hasPrefix("FLOOD_WAIT") { + return .limitExceeded + } else { + return .generic + } + } + |> mapToSignal { result -> Signal in + switch result { + case let .password(password): + return account.postbox.transaction { transaction -> Api.auth.LoginToken? in + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .passwordEntry(hint: password.hint ?? "", number: nil, code: nil, suggestReset: false, syncContacts: syncContacts))) + return nil + } + |> castError(ExportAuthTransferTokenError.self) + } + } + } else { + return .fail(.generic) + } + } + |> mapToSignal { result -> Signal in + guard let result = result else { + return .single(.passwordRequested(account)) + } + switch result { + case let .loginToken(expires, token): + return .single(.displayToken(AuthTransferExportedToken(value: token.makeData(), validUntil: expires))) + case let .loginTokenMigrateTo(dcId, token): + let updatedAccount = account.changedMasterDatacenterId(accountManager: accountManager, masterDatacenterId: dcId) + return updatedAccount + |> castError(ExportAuthTransferTokenError.self) + |> mapToSignal { updatedAccount -> Signal in + return updatedAccount.network.request(Api.functions.auth.importLoginToken(token: token)) + |> map(Optional.init) + |> `catch` { error -> Signal in + if error.errorDescription == "SESSION_PASSWORD_NEEDED" { + return updatedAccount.network.request(Api.functions.account.getPassword(), automaticFloodWait: false) + |> mapError { error -> ExportAuthTransferTokenError in + if error.errorDescription.hasPrefix("FLOOD_WAIT") { + return .limitExceeded + } else { + return .generic + } + } + |> mapToSignal { result -> Signal in + switch result { + case let .password(password): + return updatedAccount.postbox.transaction { transaction -> Api.auth.LoginToken? in + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: updatedAccount.testingEnvironment, masterDatacenterId: updatedAccount.masterDatacenterId, contents: .passwordEntry(hint: password.hint ?? "", number: nil, code: nil, suggestReset: false, syncContacts: syncContacts))) + return nil + } + |> castError(ExportAuthTransferTokenError.self) + } + } + } else { + return .fail(.generic) + } + } + |> mapToSignal { result -> Signal in + guard let result = result else { + return .single(.passwordRequested(updatedAccount)) + } + switch result { + case let .loginTokenSuccess(authorization): + switch authorization { + case let .authorization(_, _, user): + return updatedAccount.postbox.transaction { transaction -> Signal in + let user = TelegramUser(user: user) + let state = AuthorizedAccountState(isTestingEnvironment: updatedAccount.testingEnvironment, masterDatacenterId: updatedAccount.masterDatacenterId, peerId: user.id, state: nil) + initializedAppSettingsAfterLogin(transaction: transaction, appVersion: updatedAccount.networkArguments.appVersion, syncContacts: syncContacts) + transaction.setState(state) + return accountManager.transaction { transaction -> ExportAuthTransferTokenResult in + switchToAuthorizedAccount(transaction: transaction, account: updatedAccount) + return .loggedIn + } + |> castError(ExportAuthTransferTokenError.self) + } + |> castError(ExportAuthTransferTokenError.self) + |> switchToLatest + default: + return .fail(.generic) + } + default: + return .single(.changeAccountAndRetry(updatedAccount)) + } + } + } + case let .loginTokenSuccess(authorization): + switch authorization { + case let .authorization(_, _, user): + return account.postbox.transaction { transaction -> Signal in + let user = TelegramUser(user: user) + let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil) + initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts) + transaction.setState(state) + return accountManager.transaction { transaction -> ExportAuthTransferTokenResult in + switchToAuthorizedAccount(transaction: transaction, account: account) + return .loggedIn + } + |> castError(ExportAuthTransferTokenError.self) + } + |> castError(ExportAuthTransferTokenError.self) + |> switchToLatest + case let .authorizationSignUpRequired: + return .fail(.generic) + } + } + } +} + +public enum ApproveAuthTransferTokenError { + case generic + case invalid + case expired + case alreadyAccepted +} + +public func approveAuthTransferToken(account: Account, token: Data, activeSessionsContext: ActiveSessionsContext) -> Signal { + return account.network.request(Api.functions.auth.acceptLoginToken(token: Buffer(data: token))) + |> mapError { error -> ApproveAuthTransferTokenError in + switch error.errorDescription { + case "AUTH_TOKEN_INVALID": + return .invalid + case "AUTH_TOKEN_EXPIRED": + return .expired + case "AUTH_TOKEN_ALREADY_ACCEPTED": + return .alreadyAccepted + default: + return .generic + } + } + |> mapToSignal { authorization -> Signal in + let session = RecentAccountSession(apiAuthorization: authorization) + activeSessionsContext.addSession(session) + return .single(session) + } +} diff --git a/submodules/TelegramCore/Sources/Authorization.swift b/submodules/TelegramCore/Sources/Authorization.swift index 6708108d95..89926bef3c 100644 --- a/submodules/TelegramCore/Sources/Authorization.swift +++ b/submodules/TelegramCore/Sources/Authorization.swift @@ -15,7 +15,7 @@ public enum AuthorizationCodeRequestError { case timeout } -private func switchToAuthorizedAccount(transaction: AccountManagerModifier, account: UnauthorizedAccount) { +func switchToAuthorizedAccount(transaction: AccountManagerModifier, account: UnauthorizedAccount) { let nextSortOrder = (transaction.getRecords().map({ record -> Int32 in for attribute in record.attributes { if let attribute = attribute as? AccountSortOrderAttribute { diff --git a/submodules/TelegramCore/Sources/AutodownloadSettings.swift b/submodules/TelegramCore/Sources/AutodownloadSettings.swift index 141a6ac9d1..ff23b1d15c 100644 --- a/submodules/TelegramCore/Sources/AutodownloadSettings.swift +++ b/submodules/TelegramCore/Sources/AutodownloadSettings.swift @@ -22,8 +22,8 @@ public func updateAutodownloadSettingsInteractively(accountManager: AccountManag extension AutodownloadPresetSettings { init(apiAutodownloadSettings: Api.AutoDownloadSettings) { switch apiAutodownloadSettings { - case let .autoDownloadSettings(flags, photoSizeMax, videoSizeMax, fileSizeMax): - self.init(disabled: (flags & (1 << 0)) != 0, photoSizeMax: photoSizeMax, videoSizeMax: videoSizeMax, fileSizeMax: fileSizeMax, preloadLargeVideo: (flags & (1 << 1)) != 0, lessDataForPhoneCalls: (flags & (1 << 3)) != 0) + case let .autoDownloadSettings(flags, photoSizeMax, videoSizeMax, fileSizeMax, videoUploadMaxbitrate): + self.init(disabled: (flags & (1 << 0)) != 0, photoSizeMax: photoSizeMax, videoSizeMax: videoSizeMax, fileSizeMax: fileSizeMax, preloadLargeVideo: (flags & (1 << 1)) != 0, lessDataForPhoneCalls: (flags & (1 << 3)) != 0, videoUploadMaxbitrate: videoUploadMaxbitrate) } } } @@ -48,6 +48,6 @@ func apiAutodownloadPresetSettings(_ autodownloadPresetSettings: AutodownloadPre if autodownloadPresetSettings.lessDataForPhoneCalls { flags |= (1 << 3) } - return .autoDownloadSettings(flags: flags, photoSizeMax: autodownloadPresetSettings.photoSizeMax, videoSizeMax: autodownloadPresetSettings.videoSizeMax, fileSizeMax: autodownloadPresetSettings.fileSizeMax) + return .autoDownloadSettings(flags: flags, photoSizeMax: autodownloadPresetSettings.photoSizeMax, videoSizeMax: autodownloadPresetSettings.videoSizeMax, fileSizeMax: autodownloadPresetSettings.fileSizeMax, videoUploadMaxbitrate: autodownloadPresetSettings.videoUploadMaxbitrate) } diff --git a/submodules/TelegramCore/Sources/BankCards.swift b/submodules/TelegramCore/Sources/BankCards.swift new file mode 100644 index 0000000000..b600491d83 --- /dev/null +++ b/submodules/TelegramCore/Sources/BankCards.swift @@ -0,0 +1,59 @@ +import Foundation +import Postbox +import TelegramApi +import SyncCore +import MtProtoKit +import SwiftSignalKit + +public struct BankCardUrl { + public let title: String + public let url: String +} + +public struct BankCardInfo { + public let title: String + public let urls: [BankCardUrl] +} + +public func getBankCardInfo(account: Account, cardNumber: String) -> Signal { + return currentWebDocumentsHostDatacenterId(postbox: account.postbox, isTestingEnvironment: false) + |> mapToSignal { datacenterId in + let signal: Signal + if account.network.datacenterId != datacenterId { + signal = account.network.download(datacenterId: Int(datacenterId), isMedia: false, tag: nil) + |> castError(MTRpcError.self) + |> mapToSignal { worker in + return worker.request(Api.functions.payments.getBankCardData(number: cardNumber)) + } + } else { + signal = account.network.request(Api.functions.payments.getBankCardData(number: cardNumber)) + } + return signal + |> map { result -> BankCardInfo? in + return BankCardInfo(apiBankCardData: result) + } + |> `catch` { _ -> Signal in + return .single(nil) + } + } +} + +extension BankCardUrl { + init(apiBankCardOpenUrl: Api.BankCardOpenUrl) { + switch apiBankCardOpenUrl { + case let .bankCardOpenUrl(url, name): + self.title = name + self.url = url + } + } +} + +extension BankCardInfo { + init(apiBankCardData: Api.payments.BankCardData) { + switch apiBankCardData { + case let .bankCardData(title, urls): + self.title = title + self.urls = urls.map { BankCardUrl(apiBankCardOpenUrl: $0) } + } + } +} diff --git a/submodules/TelegramCore/Sources/CallSessionManager.swift b/submodules/TelegramCore/Sources/CallSessionManager.swift index ccef1bc02e..a84206744b 100644 --- a/submodules/TelegramCore/Sources/CallSessionManager.swift +++ b/submodules/TelegramCore/Sources/CallSessionManager.swift @@ -360,7 +360,7 @@ private final class CallSessionManagerContext { } } - func drop(internalId: CallSessionInternalId, reason: DropCallReason) { + func drop(internalId: CallSessionInternalId, reason: DropCallReason, debugLog: Signal) { if let context = self.contexts[internalId] { var dropData: (CallSessionStableId, Int64, DropCallSessionReason)? var wasRinging = false @@ -421,10 +421,21 @@ private final class CallSessionManagerContext { if let (id, accessHash, reason) = dropData { self.contextIdByStableId.removeValue(forKey: id) - context.state = .dropping((dropCallSession(network: self.network, addUpdates: self.addUpdates, stableId: id, accessHash: accessHash, reason: reason) |> deliverOn(self.queue)).start(next: { [weak self] reportRating, sendDebugLogs in + context.state = .dropping((dropCallSession(network: self.network, addUpdates: self.addUpdates, stableId: id, accessHash: accessHash, reason: reason) + |> deliverOn(self.queue)).start(next: { [weak self] reportRating, sendDebugLogs in if let strongSelf = self { if let context = strongSelf.contexts[internalId] { context.state = .terminated(id: id, accessHash: accessHash, reason: .ended(.hungUp), reportRating: reportRating, sendDebugLogs: sendDebugLogs) + if sendDebugLogs { + let network = strongSelf.network + let _ = (debugLog + |> timeout(5.0, queue: strongSelf.queue, alternate: .single(nil)) + |> deliverOnMainQueue).start(next: { debugLog in + if let debugLog = debugLog { + let _ = saveCallDebugLog(network: network, callId: CallId(id: id, accessHash: accessHash), log: debugLog).start() + } + }) + } strongSelf.contextUpdated(internalId: internalId) if context.isEmpty { strongSelf.contexts.removeValue(forKey: internalId) @@ -443,14 +454,14 @@ private final class CallSessionManagerContext { func drop(stableId: CallSessionStableId, reason: DropCallReason) { if let internalId = self.contextIdByStableId[stableId] { self.contextIdByStableId.removeValue(forKey: stableId) - self.drop(internalId: internalId, reason: reason) + self.drop(internalId: internalId, reason: reason, debugLog: .single(nil)) } } func dropAll() { let contexts = self.contexts for (internalId, _) in contexts { - self.drop(internalId: internalId, reason: .hangUp) + self.drop(internalId: internalId, reason: .hangUp, debugLog: .single(nil)) } } @@ -463,7 +474,7 @@ private final class CallSessionManagerContext { if case .accepting = context.state { switch result { case .failed: - strongSelf.drop(internalId: internalId, reason: .disconnect) + strongSelf.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) case let .success(call): switch call { case let .waiting(config): @@ -474,7 +485,7 @@ private final class CallSessionManagerContext { context.state = .active(id: id, accessHash: accessHash, beginTimestamp: timestamp, key: key, keyId: keyId, keyVisualHash: keyVisualHash, connections: connections, maxLayer: maxLayer, allowsP2P: allowsP2P) strongSelf.contextUpdated(internalId: internalId) } else { - strongSelf.drop(internalId: internalId, reason: .disconnect) + strongSelf.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) } } } @@ -502,7 +513,7 @@ private final class CallSessionManagerContext { case let .requested(_, accessHash, a, gA, config, _): let p = config.p.makeData() if !MTCheckIsSafeGAOrB(self.network.encryptionProvider, gA, p) { - self.drop(internalId: internalId, reason: .disconnect) + self.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) } var key = MTExp(self.network.encryptionProvider, gB.makeData(), a, p)! @@ -528,13 +539,13 @@ private final class CallSessionManagerContext { if let updatedCall = updatedCall { strongSelf.updateSession(updatedCall, completion: { _ in }) } else { - strongSelf.drop(internalId: internalId, reason: .disconnect) + strongSelf.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) } } })) self.contextUpdated(internalId: internalId) default: - self.drop(internalId: internalId, reason: .disconnect) + self.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) } } else { assertionFailure() @@ -610,10 +621,10 @@ private final class CallSessionManagerContext { self.contextUpdated(internalId: internalId) } } else { - self.drop(internalId: internalId, reason: .disconnect) + self.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) } } else { - self.drop(internalId: internalId, reason: .disconnect) + self.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) } case let .confirming(id, accessHash, key, keyId, keyVisualHash, _): switch callProtocol { @@ -761,9 +772,9 @@ public final class CallSessionManager { } } - public func drop(internalId: CallSessionInternalId, reason: DropCallReason) { + public func drop(internalId: CallSessionInternalId, reason: DropCallReason, debugLog: Signal) { self.withContext { context in - context.drop(internalId: internalId, reason: reason) + context.drop(internalId: internalId, reason: reason, debugLog: debugLog) } } diff --git a/submodules/TelegramCore/Sources/ChangePeerNotificationSettings.swift b/submodules/TelegramCore/Sources/ChangePeerNotificationSettings.swift index 163ffe3637..da0bd74e53 100644 --- a/submodules/TelegramCore/Sources/ChangePeerNotificationSettings.swift +++ b/submodules/TelegramCore/Sources/ChangePeerNotificationSettings.swift @@ -27,7 +27,7 @@ public func togglePeerMuted(account: Account, peerId: PeerId) -> Signal Bool { + if lhs.period != rhs.period { + return false + } + if lhs.followers != rhs.followers { + return false + } + if lhs.viewsPerPost != rhs.viewsPerPost { + return false + } + if lhs.sharesPerPost != rhs.sharesPerPost { + return false + } + if lhs.enabledNotifications != rhs.enabledNotifications { + return false + } + if lhs.viewsBySource != rhs.viewsBySource { + return false + } + if lhs.newFollowersBySource != rhs.newFollowersBySource { + return false + } + if lhs.languages != rhs.languages { + return false + } + if lhs.growthGraph != rhs.growthGraph { + return false + } + if lhs.followersGraph != rhs.followersGraph { + return false + } + if lhs.muteGraph != rhs.muteGraph { + return false + } + if lhs.topHoursGraph != rhs.topHoursGraph { + return false + } + if lhs.interactionsGraph != rhs.interactionsGraph { + return false + } + return true + } + + public func withUpdatedGrowthGraph(_ growthGraph: ChannelStatsGraph) -> ChannelStats { + return ChannelStats(period: self.period, followers: self.followers, viewsPerPost: self.viewsPerPost, sharesPerPost: self.sharesPerPost, enabledNotifications: self.enabledNotifications, viewsBySource: self.viewsBySource, newFollowersBySource: self.newFollowersBySource, languages: self.languages, growthGraph: growthGraph, followersGraph: self.followersGraph, muteGraph: self.muteGraph, topHoursGraph: self.topHoursGraph, interactionsGraph: self.interactionsGraph) + } + + public func withUpdatedFollowersGraph(_ followersGraph: ChannelStatsGraph) -> ChannelStats { + return ChannelStats(period: self.period, followers: self.followers, viewsPerPost: self.viewsPerPost, sharesPerPost: self.sharesPerPost, enabledNotifications: self.enabledNotifications, viewsBySource: self.viewsBySource, newFollowersBySource: self.newFollowersBySource, languages: self.languages, growthGraph: self.growthGraph, followersGraph: followersGraph, muteGraph: self.muteGraph, topHoursGraph: self.topHoursGraph, interactionsGraph: self.interactionsGraph) + } + + public func withUpdatedMuteGraph(_ muteGraph: ChannelStatsGraph) -> ChannelStats { + return ChannelStats(period: self.period, followers: self.followers, viewsPerPost: self.viewsPerPost, sharesPerPost: self.sharesPerPost, enabledNotifications: self.enabledNotifications, viewsBySource: self.viewsBySource, newFollowersBySource: self.newFollowersBySource, languages: self.languages, growthGraph: self.growthGraph, followersGraph: self.followersGraph, muteGraph: muteGraph, topHoursGraph: self.topHoursGraph, interactionsGraph: self.interactionsGraph) + } + + public func withUpdatedTopHoursGraph(_ viewsByHourGraph: ChannelStatsGraph) -> ChannelStats { + return ChannelStats(period: self.period, followers: self.followers, viewsPerPost: self.viewsPerPost, sharesPerPost: self.sharesPerPost, enabledNotifications: self.enabledNotifications, viewsBySource: self.viewsBySource, newFollowersBySource: self.newFollowersBySource, languages: self.languages, growthGraph: self.growthGraph, followersGraph: self.followersGraph, muteGraph: self.muteGraph, topHoursGraph: viewsByHourGraph, interactionsGraph: self.interactionsGraph) + } + + public func withUpdatedInteractionsGraph(_ interactionsGraph: ChannelStatsGraph) -> ChannelStats { + return ChannelStats(period: self.period, followers: self.followers, viewsPerPost: self.viewsPerPost, sharesPerPost: self.sharesPerPost, enabledNotifications: self.enabledNotifications, viewsBySource: self.viewsBySource, newFollowersBySource: self.newFollowersBySource, languages: self.languages, growthGraph: self.growthGraph, followersGraph: self.followersGraph, muteGraph: self.muteGraph, topHoursGraph: self.topHoursGraph, interactionsGraph: interactionsGraph) + } +} + +public struct ChannelStatsContextState: Equatable { + public var stats: ChannelStats? +} + +private func requestStats(network: Network, datacenterId: Int32, peer: Peer, dark: Bool = false) -> Signal { + return .never() + /*guard let inputChannel = apiInputChannel(peer) else { + return .never() + } + + var flags: Int32 = 0 + if dark { + flags |= (1 << 1) + } + + let signal: Signal + if network.datacenterId != datacenterId { + signal = network.download(datacenterId: Int(datacenterId), isMedia: false, tag: nil) + |> castError(MTRpcError.self) + |> mapToSignal { worker in + return worker.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel)) + } + } else { + signal = network.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel)) + } + + return signal + |> map { result -> ChannelStats? in + return ChannelStats(apiBroadcastStats: result) + } + |> `catch` { _ -> Signal in + return .single(nil) + }*/ +} + +private func requestGraph(network: Network, datacenterId: Int32, token: String) -> Signal { + return .never() + /*let signal: Signal + if network.datacenterId != datacenterId { + signal = network.download(datacenterId: Int(datacenterId), isMedia: false, tag: nil) + |> castError(MTRpcError.self) + |> mapToSignal { worker in + return worker.request(Api.functions.stats.loadAsyncGraph(token: token)) + } + } else { + signal = network.request(Api.functions.stats.loadAsyncGraph(token: token)) + } + + return signal + |> map { result -> ChannelStatsGraph? in + return ChannelStatsGraph(apiStatsGraph: result) + } + |> `catch` { _ -> Signal in + return .single(nil) + }*/ +} + +private final class ChannelStatsContextImpl { + private let network: Network + private let peer: Peer + private let datacenterId: Int32 + + private var _state: ChannelStatsContextState { + didSet { + if self._state != oldValue { + self._statePromise.set(.single(self._state)) + } + } + } + private let _statePromise = Promise() + var state: Signal { + return self._statePromise.get() + } + + private let disposable = MetaDisposable() + private let disposables = DisposableDict() + + init(network: Network, datacenterId: Int32, peer: Peer) { + assert(Queue.mainQueue().isCurrent()) + + self.network = network + self.peer = peer + self.datacenterId = datacenterId + self._state = ChannelStatsContextState(stats: nil) + self._statePromise.set(.single(self._state)) + + self.load() + } + + deinit { + assert(Queue.mainQueue().isCurrent()) + self.disposable.dispose() + self.disposables.dispose() + } + + private func load() { + assert(Queue.mainQueue().isCurrent()) + + self.disposable.set((requestStats(network: self.network, datacenterId: self.datacenterId, peer: self.peer) + |> deliverOnMainQueue).start(next: { [weak self] stats in + if let strongSelf = self { + strongSelf._state = ChannelStatsContextState(stats: stats) + strongSelf._statePromise.set(.single(strongSelf._state)) + } + })) + } + + func loadGrowthGraph() { + guard let stats = self._state.stats else { + return + } + if case let .OnDemand(token) = stats.growthGraph { + self.disposables.set((requestGraph(network: self.network, datacenterId: self.datacenterId, token: token) + |> deliverOnMainQueue).start(next: { [weak self] graph in + if let strongSelf = self, let graph = graph { + strongSelf._state = ChannelStatsContextState(stats: strongSelf._state.stats?.withUpdatedGrowthGraph(graph)) + strongSelf._statePromise.set(.single(strongSelf._state)) + } + }), forKey: token) + } + } + + func loadFollowersGraph() { + guard let stats = self._state.stats else { + return + } + if case let .OnDemand(token) = stats.followersGraph { + self.disposables.set((requestGraph(network: self.network, datacenterId: self.datacenterId, token: token) + |> deliverOnMainQueue).start(next: { [weak self] graph in + if let strongSelf = self, let graph = graph { + strongSelf._state = ChannelStatsContextState(stats: strongSelf._state.stats?.withUpdatedFollowersGraph(graph)) + strongSelf._statePromise.set(.single(strongSelf._state)) + } + }), forKey: token) + } + } + + func loadMuteGraph() { + guard let stats = self._state.stats else { + return + } + if case let .OnDemand(token) = stats.muteGraph { + self.disposables.set((requestGraph(network: self.network, datacenterId: self.datacenterId, token: token) + |> deliverOnMainQueue).start(next: { [weak self] graph in + if let strongSelf = self, let graph = graph { + strongSelf._state = ChannelStatsContextState(stats: strongSelf._state.stats?.withUpdatedMuteGraph(graph)) + strongSelf._statePromise.set(.single(strongSelf._state)) + } + }), forKey: token) + } + } + + func loadTopHoursGraph() { + guard let stats = self._state.stats else { + return + } + if case let .OnDemand(token) = stats.topHoursGraph { + self.disposables.set((requestGraph(network: self.network, datacenterId: self.datacenterId, token: token) + |> deliverOnMainQueue).start(next: { [weak self] graph in + if let strongSelf = self, let graph = graph { + strongSelf._state = ChannelStatsContextState(stats: strongSelf._state.stats?.withUpdatedTopHoursGraph(graph)) + strongSelf._statePromise.set(.single(strongSelf._state)) + } + }), forKey: token) + } + } + + func loadInteractionsGraph() { + guard let stats = self._state.stats else { + return + } + if case let .OnDemand(token) = stats.interactionsGraph { + self.disposables.set((requestGraph(network: self.network, datacenterId: self.datacenterId, token: token) + |> deliverOnMainQueue).start(next: { [weak self] graph in + if let strongSelf = self, let graph = graph { + strongSelf._state = ChannelStatsContextState(stats: strongSelf._state.stats?.withUpdatedInteractionsGraph(graph)) + strongSelf._statePromise.set(.single(strongSelf._state)) + } + }), forKey: token) + } + } +} + +public final class ChannelStatsContext { + private let impl: QueueLocalObject + + public var state: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.state.start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } + + public init(network: Network, datacenterId: Int32, peer: Peer) { + self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { + return ChannelStatsContextImpl(network: network, datacenterId: datacenterId, peer: peer) + }) + } + + public func loadGrowthGraph() { + self.impl.with { impl in + impl.loadGrowthGraph() + } + } + + public func loadFollowersGraph() { + self.impl.with { impl in + impl.loadFollowersGraph() + } + } + + public func loadMuteGraph() { + self.impl.with { impl in + impl.loadMuteGraph() + } + } + + public func loadTopHoursGraph() { + self.impl.with { impl in + impl.loadTopHoursGraph() + } + } + + public func loadInteractionsGraph() { + self.impl.with { impl in + impl.loadInteractionsGraph() + } + } +} + +/*extension ChannelStatsGraph { + init(apiStatsGraph: Api.StatsGraph) { + switch apiStatsGraph { + case let .statsGraph(json): + if case let .dataJSON(string) = json { + self = .Loaded(data: string) + } else { + self = .Failed(error: "") + } + case let .statsGraphError(error): + self = .Failed(error: error) + case let .statsGraphAsync(token): + self = .OnDemand(token: token) + } + } +} + +extension ChannelStatsDateRange { + init(apiStatsDateRangeDays: Api.StatsDateRangeDays) { + switch apiStatsDateRangeDays { + case let .statsDateRangeDays(minDate, maxDate): + self = ChannelStatsDateRange(minDate: minDate, maxDate: maxDate) + } + } +} + +extension ChannelStatsValue { + init(apiStatsAbsValueAndPrev: Api.StatsAbsValueAndPrev) { + switch apiStatsAbsValueAndPrev { + case let .statsAbsValueAndPrev(current, previous): + self = ChannelStatsValue(current: current, previous: previous) + } + } +} + +extension ChannelStatsNamedValue { + init(apiStatsRowAbsValueAndPrev: Api.StatsRowAbsValueAndPrev) { + switch apiStatsRowAbsValueAndPrev { + case let .statsRowAbsValueAndPrev(id, title, shortTitle, values): + self = ChannelStatsNamedValue(id: id, title: title, shortTitle: shortTitle, value: ChannelStatsValue(apiStatsAbsValueAndPrev: values)) + } + } +} + +extension ChannelStatsPercentValue { + init(apiPercentValue: Api.StatsPercentValue) { + switch apiPercentValue { + case let .statsPercentValue(part, total): + self = ChannelStatsPercentValue(fraction: part, total: total) + } + } +} + +extension ChannelStats { + convenience init(apiBroadcastStats: Api.stats.BroadcastStats) { + switch apiBroadcastStats { + case let .broadcastStats(period, followers, viewsPerPost, sharesPerPost, enabledNotifications, viewsBySource, newFollowersBySource, languages, growthGraph, followersGraph, muteGraph, topHoursGraph, interactionsGraph): + self.init(period: ChannelStatsDateRange(apiStatsDateRangeDays: period), followers: ChannelStatsValue(apiStatsAbsValueAndPrev: followers), viewsPerPost: ChannelStatsValue(apiStatsAbsValueAndPrev: viewsPerPost), sharesPerPost: ChannelStatsValue(apiStatsAbsValueAndPrev: sharesPerPost), enabledNotifications: ChannelStatsPercentValue(apiPercentValue: enabledNotifications), viewsBySource: viewsBySource.map { ChannelStatsNamedValue(apiStatsRowAbsValueAndPrev: $0) }, newFollowersBySource: newFollowersBySource.map { ChannelStatsNamedValue(apiStatsRowAbsValueAndPrev: $0) }, languages: languages.map { ChannelStatsNamedValue(apiStatsRowAbsValueAndPrev: $0) }, growthGraph: ChannelStatsGraph(apiStatsGraph: growthGraph), followersGraph: ChannelStatsGraph(apiStatsGraph: followersGraph), muteGraph: ChannelStatsGraph(apiStatsGraph: muteGraph), topHoursGraph: ChannelStatsGraph(apiStatsGraph: topHoursGraph), interactionsGraph: ChannelStatsGraph(apiStatsGraph: interactionsGraph)) + } + } +} +*/ diff --git a/submodules/TelegramCore/Sources/ChatHistoryPreloadManager.swift b/submodules/TelegramCore/Sources/ChatHistoryPreloadManager.swift index 907ded383c..0cced57f03 100644 --- a/submodules/TelegramCore/Sources/ChatHistoryPreloadManager.swift +++ b/submodules/TelegramCore/Sources/ChatHistoryPreloadManager.swift @@ -4,7 +4,7 @@ import SwiftSignalKit import SyncCore -public struct HistoryPreloadIndex: Comparable { +public struct HistoryPreloadIndex: Comparable, CustomStringConvertible { public let index: ChatListIndex? public let hasUnread: Bool public let isMuted: Bool @@ -49,9 +49,13 @@ public struct HistoryPreloadIndex: Comparable { return true } } + + public var description: String { + return "index: \(String(describing: self.index)), hasUnread: \(self.hasUnread), isMuted: \(self.isMuted), isPriority: \(self.isPriority)" + } } -private struct HistoryPreloadHole: Hashable, Comparable { +private struct HistoryPreloadHole: Hashable, Comparable, CustomStringConvertible { let preloadIndex: HistoryPreloadIndex let hole: MessageOfInterestHole @@ -66,6 +70,10 @@ private struct HistoryPreloadHole: Hashable, Comparable { var hashValue: Int { return self.preloadIndex.index.hashValue &* 31 &+ self.hole.hashValue } + + var description: String { + return "(preloadIndex: \(self.preloadIndex), hole: \(self.hole))" + } } private final class HistoryPreloadEntry: Comparable { @@ -90,6 +98,9 @@ private final class HistoryPreloadEntry: Comparable { self.isStarted = true let hole = self.hole.hole + + Logger.shared.log("HistoryPreload", "start hole \(hole)") + let signal: Signal = .complete() |> delay(0.3, queue: queue) |> then( @@ -98,8 +109,8 @@ private final class HistoryPreloadEntry: Comparable { |> deliverOn(queue) |> mapToSignal { download -> Signal in switch hole.hole { - case let .peer(peerHole): - return fetchMessageHistoryHole(accountPeerId: accountPeerId, source: .download(download), postbox: postbox, peerId: peerHole.peerId, namespace: peerHole.namespace, direction: hole.direction, space: .everywhere, limit: 60) + case let .peer(peerHole): + return fetchMessageHistoryHole(accountPeerId: accountPeerId, source: .download(download), postbox: postbox, peerId: peerHole.peerId, namespace: peerHole.namespace, direction: hole.direction, space: .everywhere, count: 60) } } ) @@ -320,13 +331,12 @@ final class ChatHistoryPreloadManager { return } #if DEBUG - if true { - //return - } + return #endif + var indices: [(ChatHistoryPreloadIndex, Bool, Bool)] = [] for entry in view.0.entries { - if case let .MessageEntry(index, _, readState, notificationSettings, _, _, _, _) = entry { + if case let .MessageEntry(index, _, readState, notificationSettings, _, _, _, _, _) = entry { var hasUnread = false if let readState = readState { hasUnread = readState.count != 0 @@ -396,7 +406,7 @@ final class ChatHistoryPreloadManager { let key: PostboxViewKey switch index.entity { case let .peer(peerId): - key = .messageOfInterestHole(location: .peer(peerId), namespace: Namespaces.Message.Cloud, count: 60) + key = .messageOfInterestHole(location: .peer(peerId), namespace: Namespaces.Message.Cloud, count: 70) } view.disposable.set((self.postbox.combinedView(keys: [key]) |> deliverOn(self.queue)).start(next: { [weak self] next in @@ -422,6 +432,14 @@ final class ChatHistoryPreloadManager { } let updatedHole = view.currentHole + + let holeIsUpdated = previousHole != updatedHole + + switch index.entity { + case let .peer(peerId): + Logger.shared.log("HistoryPreload", "view \(peerId) hole \(updatedHole) isUpdated: \(holeIsUpdated)") + } + if previousHole != updatedHole { strongSelf.update(from: previousHole, to: updatedHole) } @@ -449,7 +467,11 @@ final class ChatHistoryPreloadManager { private func update(from previousHole: HistoryPreloadHole?, to updatedHole: HistoryPreloadHole?) { assert(self.queue.isCurrent()) - if previousHole == updatedHole { + let isHoleUpdated = previousHole != updatedHole + + Logger.shared.log("HistoryPreload", "update from \(String(describing: previousHole)) to \(String(describing: updatedHole)), isUpdated: \(isHoleUpdated)") + + if !isHoleUpdated { return } @@ -483,9 +505,12 @@ final class ChatHistoryPreloadManager { } if self.canPreloadHistoryValue { + Logger.shared.log("HistoryPreload", "will start") for i in 0 ..< min(3, self.entries.count) { self.entries[i].startIfNeeded(postbox: self.postbox, accountPeerId: self.accountPeerId, download: self.download.get() |> take(1), queue: self.queue) } + } else { + Logger.shared.log("HistoryPreload", "will not start, canPreloadHistoryValue = false") } } } diff --git a/submodules/TelegramCore/Sources/ChatListFiltering.swift b/submodules/TelegramCore/Sources/ChatListFiltering.swift new file mode 100644 index 0000000000..f9966602c1 --- /dev/null +++ b/submodules/TelegramCore/Sources/ChatListFiltering.swift @@ -0,0 +1,379 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi + +import SyncCore + +public struct ChatListFilterPeerCategories: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let privateChats = ChatListFilterPeerCategories(rawValue: 1 << 0) + public static let secretChats = ChatListFilterPeerCategories(rawValue: 1 << 1) + public static let privateGroups = ChatListFilterPeerCategories(rawValue: 1 << 2) + public static let bots = ChatListFilterPeerCategories(rawValue: 1 << 3) + public static let publicGroups = ChatListFilterPeerCategories(rawValue: 1 << 4) + public static let channels = ChatListFilterPeerCategories(rawValue: 1 << 5) + + public static let all: ChatListFilterPeerCategories = [ + .privateChats, + .secretChats, + .privateGroups, + .bots, + .publicGroups, + .channels + ] +} + +private struct ChatListFilterPeerApiCategories: OptionSet { + var rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue + } + + static let privateChats = ChatListFilterPeerApiCategories(rawValue: 1 << 0) + static let secretChats = ChatListFilterPeerApiCategories(rawValue: 1 << 1) + static let privateGroups = ChatListFilterPeerApiCategories(rawValue: 1 << 2) + static let publicGroups = ChatListFilterPeerApiCategories(rawValue: 1 << 3) + static let channels = ChatListFilterPeerApiCategories(rawValue: 1 << 4) + static let bots = ChatListFilterPeerApiCategories(rawValue: 1 << 5) +} + +extension ChatListFilterPeerCategories { + init(apiFlags: Int32) { + let flags = ChatListFilterPeerApiCategories(rawValue: apiFlags) + var result: ChatListFilterPeerCategories = [] + if flags.contains(.privateChats) { + result.insert(.privateChats) + } + if flags.contains(.secretChats) { + result.insert(.secretChats) + } + if flags.contains(.privateGroups) { + result.insert(.privateGroups) + } + if flags.contains(.publicGroups) { + result.insert(.publicGroups) + } + if flags.contains(.channels) { + result.insert(.channels) + } + if flags.contains(.bots) { + result.insert(.bots) + } + self = result + } + + var apiFlags: Int32 { + var result: ChatListFilterPeerApiCategories = [] + if self.contains(.privateChats) { + result.insert(.privateChats) + } + if self.contains(.secretChats) { + result.insert(.secretChats) + } + if self.contains(.privateGroups) { + result.insert(.privateGroups) + } + if self.contains(.publicGroups) { + result.insert(.publicGroups) + } + if self.contains(.channels) { + result.insert(.channels) + } + if self.contains(.bots) { + result.insert(.bots) + } + return result.rawValue + } +} + +public struct ChatListFilter: PostboxCoding, Equatable { + public var id: Int32 + public var title: String? + public var categories: ChatListFilterPeerCategories + public var excludeMuted: Bool + public var excludeRead: Bool + public var includePeers: [PeerId] + + public init( + id: Int32, + title: String?, + categories: ChatListFilterPeerCategories, + excludeMuted: Bool, + excludeRead: Bool, + includePeers: [PeerId] + ) { + self.id = id + self.title = title + self.categories = categories + self.excludeMuted = excludeMuted + self.excludeRead = excludeRead + self.includePeers = includePeers + } + + public init(decoder: PostboxDecoder) { + self.id = decoder.decodeInt32ForKey("id", orElse: 0) + self.title = decoder.decodeOptionalStringForKey("title") + self.categories = ChatListFilterPeerCategories(rawValue: decoder.decodeInt32ForKey("categories", orElse: 0)) + self.excludeMuted = decoder.decodeInt32ForKey("excludeMuted", orElse: 0) != 0 + self.excludeRead = decoder.decodeInt32ForKey("excludeRead", orElse: 0) != 0 + self.includePeers = decoder.decodeInt64ArrayForKey("includePeers").map(PeerId.init) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.id, forKey: "id") + if let title = self.title { + encoder.encodeString(title, forKey: "title") + } else { + encoder.encodeNil(forKey: "title") + } + encoder.encodeInt32(self.categories.rawValue, forKey: "categories") + encoder.encodeInt32(self.excludeMuted ? 1 : 0, forKey: "excludeMuted") + encoder.encodeInt32(self.excludeRead ? 1 : 0, forKey: "excludeRead") + encoder.encodeInt64Array(self.includePeers.map { $0.toInt64() }, forKey: "includePeers") + } +} + +/*extension ChatListFilter { + init(apiFilter: Api.DialogFilter) { + switch apiFilter { + case let .dialogFilter(flags, id, title, includePeers): + self.init( + id: id, + title: title.isEmpty ? nil : title, + categories: ChatListFilterPeerCategories(apiFlags: flags), + excludeMuted: (flags & (1 << 11)) != 0, + excludeRead: (flags & (1 << 12)) != 0, + includePeers: includePeers.compactMap { peer -> PeerId? in + switch peer { + case let .inputPeerUser(userId, _): + return PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + case let .inputPeerChat(chatId): + return PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) + case let .inputPeerChannel(channelId, _): + return PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + default: + return nil + } + } + ) + } + } + + func apiFilter(transaction: Transaction) -> Api.DialogFilter { + var flags: Int32 = 0 + if self.excludeMuted { + flags |= 1 << 11 + } + if self.excludeRead { + flags |= 1 << 12 + } + flags |= self.categories.apiFlags + return .dialogFilter(flags: flags, id: self.id, title: self.title ?? "", includePeers: self.includePeers.compactMap { peerId -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + }) + } +}*/ + +public enum RequestUpdateChatListFilterError { + case generic +} + +/*public func requestUpdateChatListFilter(account: Account, id: Int32, filter: ChatListFilter?) -> Signal { + return account.postbox.transaction { transaction -> Api.DialogFilter? in + return filter?.apiFilter(transaction: transaction) + } + |> castError(RequestUpdateChatListFilterError.self) + |> mapToSignal { inputFilter -> Signal in + var flags: Int32 = 0 + if inputFilter != nil { + flags |= 1 << 0 + } + return account.network.request(Api.functions.messages.updateDialogFilter(flags: flags, id: id, filter: inputFilter)) + |> mapError { _ -> RequestUpdateChatListFilterError in + return .generic + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } +} + +public enum RequestUpdateChatListFilterOrderError { + case generic +} + +public func requestUpdateChatListFilterOrder(account: Account, ids: [Int32]) -> Signal { + return account.network.request(Api.functions.messages.updateDialogFiltersOrder(order: ids)) + |> mapError { _ -> RequestUpdateChatListFilterOrderError in + return .generic + } + |> mapToSignal { _ -> Signal in + return .complete() + } +} + +private enum RequestChatListFiltersError { + case generic +} + +private func requestChatListFilters(postbox: Postbox, network: Network) -> Signal<[ChatListFilter], RequestChatListFiltersError> { + return network.request(Api.functions.messages.getDialogFilters()) + |> mapError { _ -> RequestChatListFiltersError in + return .generic + } + |> mapToSignal { result -> Signal<[ChatListFilter], RequestChatListFiltersError> in + return postbox.transaction { transaction -> ([ChatListFilter], [Api.InputPeer]) in + var filters: [ChatListFilter] = [] + var missingPeers: [Api.InputPeer] = [] + var missingPeerIds = Set() + for apiFilter in result { + let filter = ChatListFilter(apiFilter: apiFilter) + filters.append(filter) + switch apiFilter { + case let .dialogFilter(_, _, _, includePeers): + for peer in includePeers { + var peerId: PeerId? + switch peer { + case let .inputPeerUser(userId, _): + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + case let .inputPeerChat(chatId): + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) + case let .inputPeerChannel(channelId, _): + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + default: + break + } + if let peerId = peerId { + if transaction.getPeer(peerId) == nil && !missingPeerIds.contains(peerId) { + missingPeerIds.insert(peerId) + missingPeers.append(peer) + } + } + } + } + } + return (filters, missingPeers) + } + |> castError(RequestChatListFiltersError.self) + |> mapToSignal { filtersAndMissingPeers -> Signal<[ChatListFilter], RequestChatListFiltersError> in + let (filters, missingPeers) = filtersAndMissingPeers + return .single(filters) + } + } +} + +func managedChatListFilters(postbox: Postbox, network: Network) -> Signal { + return requestChatListFilters(postbox: postbox, network: network) + |> `catch` { _ -> Signal<[ChatListFilter], NoError> in + return .complete() + } + |> mapToSignal { filters -> Signal in + return postbox.transaction { transaction in + transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFilters, { entry in + var settings = entry as? ChatListFiltersState ?? ChatListFiltersState.default + settings.filters = filters + return settings + }) + } + |> ignoreValues + } +} + +public func replaceRemoteChatListFilters(account: Account) -> Signal { + return requestChatListFilters(postbox: account.postbox, network: account.network) + |> `catch` { _ -> Signal<[ChatListFilter], NoError> in + return .complete() + } + |> mapToSignal { remoteFilters -> Signal in + var deleteSignals: [Signal] = [] + for filter in remoteFilters { + deleteSignals.append(requestUpdateChatListFilter(account: account, id: filter.id, filter: nil) + |> `catch` { _ -> Signal in + return .complete() + } + |> ignoreValues) + } + + let addFilters = account.postbox.transaction { transaction -> [(Int32, ChatListFilter)] in + let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default + return settings.filters.map { filter -> (Int32, ChatListFilter) in + return (filter.id, filter) + } + } + |> mapToSignal { filters -> Signal in + var signals: [Signal] = [] + for (id, filter) in filters { + signals.append(requestUpdateChatListFilter(account: account, id: id, filter: filter) + |> `catch` { _ -> Signal in + return .complete() + } + |> ignoreValues) + } + return combineLatest(signals) + |> ignoreValues + } + + return combineLatest( + deleteSignals + ) + |> ignoreValues + |> then( + addFilters + ) + } +} + +public struct ChatListFiltersState: PreferencesEntry, Equatable { + public var filters: [ChatListFilter] + public var remoteFilters: [ChatListFilter]? + + public static var `default` = ChatListFiltersState(filters: [], remoteFilters: nil) + + public init(filters: [ChatListFilter], remoteFilters: [ChatListFilter]?) { + self.filters = filters + self.remoteFilters = remoteFilters + } + + public init(decoder: PostboxDecoder) { + self.filters = decoder.decodeObjectArrayWithDecoderForKey("filters") + self.remoteFilters = decoder.decodeOptionalObjectArrayWithDecoderForKey("remoteFilters") + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeObjectArray(self.filters, forKey: "filters") + if let remoteFilters = self.remoteFilters { + encoder.encodeObjectArray(remoteFilters, forKey: "remoteFilters") + } else { + encoder.encodeNil(forKey: "remoteFilters") + } + } + + public func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? ChatListFiltersState, self == to { + return true + } else { + return false + } + } +} + +public func updateChatListFilterSettingsInteractively(postbox: Postbox, _ f: @escaping (ChatListFiltersState) -> ChatListFiltersState) -> Signal { + return postbox.transaction { transaction -> ChatListFiltersState in + var result: ChatListFiltersState? + transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFilters, { entry in + var settings = entry as? ChatListFiltersState ?? ChatListFiltersState.default + let updated = f(settings) + result = updated + return updated + }) + return result ?? .default + } +} +*/ diff --git a/submodules/TelegramCore/Sources/ChatUpdatingMessageMedia.swift b/submodules/TelegramCore/Sources/ChatUpdatingMessageMedia.swift new file mode 100644 index 0000000000..f3fa1baf1a --- /dev/null +++ b/submodules/TelegramCore/Sources/ChatUpdatingMessageMedia.swift @@ -0,0 +1,43 @@ +import Foundation +import Postbox +import SyncCore +import TelegramCore + +public final class ChatUpdatingMessageMedia: Equatable { + public let text: String + public let entities: TextEntitiesMessageAttribute? + public let disableUrlPreview: Bool + public let media: RequestEditMessageMedia + public let progress: Float + + init(text: String, entities: TextEntitiesMessageAttribute?, disableUrlPreview: Bool, media: RequestEditMessageMedia, progress: Float) { + self.text = text + self.entities = entities + self.disableUrlPreview = disableUrlPreview + self.media = media + self.progress = progress + } + + public static func ==(lhs: ChatUpdatingMessageMedia, rhs: ChatUpdatingMessageMedia) -> Bool { + if lhs.text != rhs.text { + return false + } + if lhs.entities != rhs.entities { + return false + } + if lhs.disableUrlPreview != rhs.disableUrlPreview { + return false + } + if lhs.media != rhs.media { + return false + } + if lhs.progress != rhs.progress { + return false + } + return true + } + + func withProgress(_ progress: Float) -> ChatUpdatingMessageMedia { + return ChatUpdatingMessageMedia(text: self.text, entities: self.entities, disableUrlPreview: self.disableUrlPreview, media: self.media, progress: progress) + } +} diff --git a/submodules/TelegramCore/Sources/ContentSettings.swift b/submodules/TelegramCore/Sources/ContentSettings.swift new file mode 100644 index 0000000000..6801fa8ecc --- /dev/null +++ b/submodules/TelegramCore/Sources/ContentSettings.swift @@ -0,0 +1,79 @@ +import Foundation +import Postbox +import TelegramApi +import SyncCore +import SwiftSignalKit + +public struct ContentSettings: Equatable { + public static var `default` = ContentSettings(ignoreContentRestrictionReasons: []) + + public var ignoreContentRestrictionReasons: Set + + public init(ignoreContentRestrictionReasons: Set) { + self.ignoreContentRestrictionReasons = ignoreContentRestrictionReasons + } +} + +private extension ContentSettings { + init(appConfiguration: AppConfiguration) { + var reasons: [String] = [] + if let data = appConfiguration.data, let reasonsData = data["ignore_restriction_reasons"] as? [String] { + reasons = reasonsData + } + self.init(ignoreContentRestrictionReasons: Set(reasons)) + } +} + +public func getContentSettings(transaction: Transaction) -> ContentSettings { + let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration) as? AppConfiguration ?? AppConfiguration.defaultValue + return ContentSettings(appConfiguration: appConfiguration) +} + +public func getContentSettings(postbox: Postbox) -> Signal { + return postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + |> map { view -> ContentSettings in + let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? AppConfiguration.defaultValue + return ContentSettings(appConfiguration: appConfiguration) + } + |> distinctUntilChanged +} + +public struct ContentSettingsConfiguration: Equatable { + public static var `default` = ContentSettingsConfiguration(sensitiveContentEnabled: false, canAdjustSensitiveContent: false) + + public var sensitiveContentEnabled: Bool + public var canAdjustSensitiveContent: Bool + + public init(sensitiveContentEnabled: Bool, canAdjustSensitiveContent: Bool) { + self.sensitiveContentEnabled = sensitiveContentEnabled + self.canAdjustSensitiveContent = canAdjustSensitiveContent + } +} + +public func contentSettingsConfiguration(network: Network) -> Signal { + return network.request(Api.functions.account.getContentSettings()) + |> map { result -> ContentSettingsConfiguration in + switch result { + case let .contentSettings(flags): + return ContentSettingsConfiguration(sensitiveContentEnabled: (flags & (1 << 0)) != 0, canAdjustSensitiveContent: (flags & (1 << 1)) != 0) + } + } + |> `catch` { _ -> Signal in + return .single(.default) + } +} + +public func updateRemoteContentSettingsConfiguration(postbox: Postbox, network: Network, sensitiveContentEnabled: Bool) -> Signal { + var flags: Int32 = 0 + if sensitiveContentEnabled { + flags |= 1 << 0 + } + return network.request(Api.functions.account.setContentSettings(flags: flags)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return updateAppConfigurationOnce(postbox: postbox, network: network) + |> ignoreValues + } +} diff --git a/submodules/TelegramCore/Sources/DeleteMessages.swift b/submodules/TelegramCore/Sources/DeleteMessages.swift index e271570313..14dfab3fd3 100644 --- a/submodules/TelegramCore/Sources/DeleteMessages.swift +++ b/submodules/TelegramCore/Sources/DeleteMessages.swift @@ -4,48 +4,64 @@ import SwiftSignalKit import SyncCore -private func removeMessageMedia(message: Message, mediaBox: MediaBox) { - for media in message.media { - if let image = media as? TelegramMediaImage { - let _ = mediaBox.removeCachedResources(Set(image.representations.map({ WrappedMediaResourceId($0.resource.id) }))).start() - } else if let file = media as? TelegramMediaFile { - let _ = mediaBox.removeCachedResources(Set(file.previewRepresentations.map({ WrappedMediaResourceId($0.resource.id) }))).start() - let _ = mediaBox.removeCachedResources(Set([WrappedMediaResourceId(file.resource.id)])).start() +func addMessageMediaResourceIdsToRemove(media: Media, resourceIds: inout [WrappedMediaResourceId]) { + if let image = media as? TelegramMediaImage { + for representation in image.representations { + resourceIds.append(WrappedMediaResourceId(representation.resource.id)) } + } else if let file = media as? TelegramMediaFile { + for representation in file.previewRepresentations { + resourceIds.append(WrappedMediaResourceId(representation.resource.id)) + } + resourceIds.append(WrappedMediaResourceId(file.resource.id)) + } +} + +func addMessageMediaResourceIdsToRemove(message: Message, resourceIds: inout [WrappedMediaResourceId]) { + for media in message.media { + addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) } } public func deleteMessages(transaction: Transaction, mediaBox: MediaBox, ids: [MessageId], deleteMedia: Bool = true) { + var resourceIds: [WrappedMediaResourceId] = [] if deleteMedia { for id in ids { if id.peerId.namespace == Namespaces.Peer.SecretChat { if let message = transaction.getMessage(id) { - removeMessageMedia(message: message, mediaBox: mediaBox) + addMessageMediaResourceIdsToRemove(message: message, resourceIds: &resourceIds) } } } } - transaction.deleteMessages(ids, forEachMedia: { media in - if deleteMedia { - processRemovedMedia(mediaBox, media) - } + if !resourceIds.isEmpty { + let _ = mediaBox.removeCachedResources(Set(resourceIds)).start() + } + transaction.deleteMessages(ids, forEachMedia: { _ in }) } public func deleteAllMessagesWithAuthor(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, authorId: PeerId, namespace: MessageId.Namespace) { + var resourceIds: [WrappedMediaResourceId] = [] transaction.removeAllMessagesWithAuthor(peerId, authorId: authorId, namespace: namespace, forEachMedia: { media in - processRemovedMedia(mediaBox, media) + addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) }) + if !resourceIds.isEmpty { + let _ = mediaBox.removeCachedResources(Set(resourceIds)).start() + } } public func clearHistory(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, namespaces: MessageIdNamespaces) { if peerId.namespace == Namespaces.Peer.SecretChat { + var resourceIds: [WrappedMediaResourceId] = [] transaction.withAllMessages(peerId: peerId, { message in - removeMessageMedia(message: message, mediaBox: mediaBox) + addMessageMediaResourceIdsToRemove(message: message, resourceIds: &resourceIds) return true }) + if !resourceIds.isEmpty { + let _ = mediaBox.removeCachedResources(Set(resourceIds)).start() + } } - transaction.clearHistory(peerId, namespaces: namespaces, forEachMedia: { media in - processRemovedMedia(mediaBox, media) + transaction.clearHistory(peerId, namespaces: namespaces, forEachMedia: { _ in }) } diff --git a/submodules/TelegramCore/Sources/DeleteMessagesInteractively.swift b/submodules/TelegramCore/Sources/DeleteMessagesInteractively.swift index dd3c1fe45d..12a257f601 100644 --- a/submodules/TelegramCore/Sources/DeleteMessagesInteractively.swift +++ b/submodules/TelegramCore/Sources/DeleteMessagesInteractively.swift @@ -6,63 +6,92 @@ import MtProtoKit import SyncCore -public func deleteMessagesInteractively(postbox: Postbox, messageIds initialMessageIds: [MessageId], type: InteractiveMessagesDeletionType, deleteAllInGroup: Bool = false) -> Signal { - return postbox.transaction { transaction -> Void in - var messageIds: [MessageId] = [] - if deleteAllInGroup { - for id in initialMessageIds { - if let group = transaction.getMessageGroup(id) ?? transaction.getMessageForwardedGroup(id) { - for message in group { - if !messageIds.contains(message.id) { - messageIds.append(message.id) - } +public func deleteMessagesInteractively(account: Account, messageIds: [MessageId], type: InteractiveMessagesDeletionType, deleteAllInGroup: Bool = false) -> Signal { + return account.postbox.transaction { transaction -> Void in + deleteMessagesInteractively(transaction: transaction, stateManager: account.stateManager, postbox: account.postbox, messageIds: messageIds, type: type, removeIfPossiblyDelivered: true) + } +} + +func deleteMessagesInteractively(transaction: Transaction, stateManager: AccountStateManager?, postbox: Postbox, messageIds initialMessageIds: [MessageId], type: InteractiveMessagesDeletionType, deleteAllInGroup: Bool = false, removeIfPossiblyDelivered: Bool) { + var messageIds: [MessageId] = [] + if deleteAllInGroup { + for id in initialMessageIds { + if let group = transaction.getMessageGroup(id) ?? transaction.getMessageForwardedGroup(id) { + for message in group { + if !messageIds.contains(message.id) { + messageIds.append(message.id) + } + } + } else { + messageIds.append(id) + } + } + } else { + messageIds = initialMessageIds + } + + var messageIdsByPeerId: [PeerId: [MessageId]] = [:] + for id in messageIds { + if messageIdsByPeerId[id.peerId] == nil { + messageIdsByPeerId[id.peerId] = [id] + } else { + messageIdsByPeerId[id.peerId]!.append(id) + } + } + + var uniqueIds: [Int64: PeerId] = [:] + + for (peerId, peerMessageIds) in messageIdsByPeerId { + for id in peerMessageIds { + if let message = transaction.getMessage(id) { + for attribute in message.attributes { + if let attribute = attribute as? OutgoingMessageInfoAttribute { + uniqueIds[attribute.uniqueId] = peerId } - } else { - messageIds.append(id) } } - } else { - messageIds = initialMessageIds } - var messageIdsByPeerId: [PeerId: [MessageId]] = [:] - for id in messageIds { - if messageIdsByPeerId[id.peerId] == nil { - messageIdsByPeerId[id.peerId] = [id] - } else { - messageIdsByPeerId[id.peerId]!.append(id) + if peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudUser { + let remoteMessageIds = peerMessageIds.filter { id in + if id.namespace == Namespaces.Message.Local { + return false + } + return true } - } - for (peerId, peerMessageIds) in messageIdsByPeerId { - if peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudUser { - cloudChatAddRemoveMessagesOperation(transaction: transaction, peerId: peerId, messageIds: peerMessageIds, type: CloudChatRemoveMessagesType(type)) - } else if peerId.namespace == Namespaces.Peer.SecretChat { - if let state = transaction.getPeerChatState(peerId) as? SecretChatState { - var layer: SecretChatLayer? - switch state.embeddedState { - case .terminated, .handshake: - break - case .basicLayer: - layer = .layer8 - case let .sequenceBasedLayer(sequenceState): - layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer + if !remoteMessageIds.isEmpty { + cloudChatAddRemoveMessagesOperation(transaction: transaction, peerId: peerId, messageIds: remoteMessageIds, type: CloudChatRemoveMessagesType(type)) + } + } else if peerId.namespace == Namespaces.Peer.SecretChat { + if let state = transaction.getPeerChatState(peerId) as? SecretChatState { + var layer: SecretChatLayer? + switch state.embeddedState { + case .terminated, .handshake: + break + case .basicLayer: + layer = .layer8 + case let .sequenceBasedLayer(sequenceState): + layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer + } + if let layer = layer { + var globallyUniqueIds: [Int64] = [] + for messageId in peerMessageIds { + if let message = transaction.getMessage(messageId), let globallyUniqueId = message.globallyUniqueId { + globallyUniqueIds.append(globallyUniqueId) + } } - if let layer = layer { - var globallyUniqueIds: [Int64] = [] - for messageId in peerMessageIds { - if let message = transaction.getMessage(messageId), let globallyUniqueId = message.globallyUniqueId { - globallyUniqueIds.append(globallyUniqueId) - } - } - let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.deleteMessages(layer: layer, actionGloballyUniqueId: arc4random64(), globallyUniqueIds: globallyUniqueIds), state: state) - if updatedState != state { - transaction.setPeerChatState(peerId, state: updatedState) - } + let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.deleteMessages(layer: layer, actionGloballyUniqueId: arc4random64(), globallyUniqueIds: globallyUniqueIds), state: state) + if updatedState != state { + transaction.setPeerChatState(peerId, state: updatedState) } } } } - deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: messageIds) + } + deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: messageIds) + + if !uniqueIds.isEmpty && removeIfPossiblyDelivered { + stateManager?.removePossiblyDeliveredMessages(uniqueIds: uniqueIds) } } @@ -147,9 +176,7 @@ public func clearAuthorHistory(account: Account, peerId: PeerId, memberId: PeerI |> `catch` { success -> Signal in if success { return account.postbox.transaction { transaction -> Void in - transaction.removeAllMessagesWithAuthor(peerId, authorId: memberId, namespace: Namespaces.Message.Cloud, forEachMedia: { media in - processRemovedMedia(account.postbox.mediaBox, media) - }) + deleteAllMessagesWithAuthor(transaction: transaction, mediaBox: account.postbox.mediaBox, peerId: peerId, authorId: memberId, namespace: Namespaces.Message.Cloud) } } else { return .complete() diff --git a/submodules/TelegramCore/Sources/EarliestUnseenPersonalMentionMessage.swift b/submodules/TelegramCore/Sources/EarliestUnseenPersonalMentionMessage.swift index 1a776e92da..54c5a0b320 100644 --- a/submodules/TelegramCore/Sources/EarliestUnseenPersonalMentionMessage.swift +++ b/submodules/TelegramCore/Sources/EarliestUnseenPersonalMentionMessage.swift @@ -49,7 +49,19 @@ public func earliestUnseenPersonalMentionMessage(account: Account, peerId: PeerI return .single(.result(message.id)) } } else { - return .single(.result(nil)) + return account.postbox.transaction { transaction -> EarliestUnseenPersonalMentionMessageResult in + if let topId = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud) { + transaction.replaceMessageTagSummary(peerId: peerId, tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud, count: 0, maxId: topId.id) + + transaction.removeHole(peerId: peerId, namespace: Namespaces.Message.Cloud, space: .tag(.unseenPersonalMessage), range: 1 ... (Int32.max - 1)) + let ids = transaction.getMessageIndicesWithTag(peerId: peerId, namespace: Namespaces.Message.Cloud, tag: .unseenPersonalMessage).map({ $0.id }) + for id in ids { + markUnseenPersonalMessage(transaction: transaction, id: id, addSynchronizeAction: false) + } + } + + return .result(nil) + } } } |> distinctUntilChanged diff --git a/submodules/TelegramCore/Sources/EnqueueMessage.swift b/submodules/TelegramCore/Sources/EnqueueMessage.swift index 4900cfc999..ef062598e0 100644 --- a/submodules/TelegramCore/Sources/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/EnqueueMessage.swift @@ -55,7 +55,7 @@ private func convertForwardedMediaForSecretChat(_ media: Media) -> Media { if let file = media as? TelegramMediaFile { return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), partialReference: file.partialReference, resource: file.resource, previewRepresentations: file.previewRepresentations, immediateThumbnailData: file.immediateThumbnailData, mimeType: file.mimeType, size: file.size, attributes: file.attributes) } else if let image = media as? TelegramMediaImage { - return TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: image.reference, partialReference: image.partialReference) + return TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: image.reference, partialReference: image.partialReference, flags: []) } else { return media } @@ -82,6 +82,8 @@ private func filterMessageAttributesForOutgoingMessage(_ attributes: [MessageAtt return true case _ as OutgoingScheduleInfoMessageAttribute: return true + case _ as EmbeddedMediaStickersMessageAttribute: + return true default: return false } @@ -267,6 +269,8 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } if let peer = transaction.getPeer(peerId), let accountPeer = transaction.getPeer(account.peerId) { + let peerPresence = transaction.getPeerPresence(peerId: peerId) + var storeMessages: [StoreMessage] = [] var timestamp = Int32(account.network.context.globalTime()) switch peerId.namespace { @@ -377,12 +381,19 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, var effectiveTimestamp = timestamp for attribute in attributes { if let attribute = attribute as? OutgoingScheduleInfoMessageAttribute { - messageNamespace = Namespaces.Message.ScheduledLocal - effectiveTimestamp = attribute.scheduleTime + if attribute.scheduleTime == scheduleWhenOnlineTimestamp, let presence = peerPresence as? TelegramUserPresence, case let .present(statusTimestamp) = presence.status, statusTimestamp >= timestamp { + } else { + messageNamespace = Namespaces.Message.ScheduledLocal + effectiveTimestamp = attribute.scheduleTime + } break } } + if messageNamespace != Namespaces.Message.ScheduledLocal { + attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) + } + if let peer = peer as? TelegramChannel { switch peer.info { case let .broadcast(info): @@ -515,11 +526,18 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, entitiesAttribute = attribute } if let attribute = attribute as? OutgoingScheduleInfoMessageAttribute { - messageNamespace = Namespaces.Message.ScheduledLocal - effectiveTimestamp = attribute.scheduleTime + if attribute.scheduleTime == scheduleWhenOnlineTimestamp, let presence = peerPresence as? TelegramUserPresence, case let .present(statusTimestamp) = presence.status, statusTimestamp >= timestamp { + } else { + messageNamespace = Namespaces.Message.ScheduledLocal + effectiveTimestamp = attribute.scheduleTime + } } } + if messageNamespace != Namespaces.Message.ScheduledLocal { + attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) + } + let (tags, globalTags) = tagsForStoreMessage(incoming: false, attributes: attributes, media: sourceMessage.media, textEntities: entitiesAttribute?.entities) let localGroupingKey: Int64? diff --git a/submodules/TelegramCore/Sources/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/FetchedMediaResource.swift index baed487729..8ab6ae7d41 100644 --- a/submodules/TelegramCore/Sources/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/FetchedMediaResource.swift @@ -140,7 +140,9 @@ private enum MediaReferenceRevalidationKey: Hashable { case stickerPack(stickerPack: StickerPackReference) case savedGifs case peer(peer: PeerReference) + case wallpaper(wallpaper: WallpaperReference) case wallpapers + case themes } private final class MediaReferenceRevalidationItemContext { @@ -379,13 +381,24 @@ final class MediaReferenceRevalidationContext { } } - func wallpapers(postbox: Postbox, network: Network, background: Bool) -> Signal<[TelegramWallpaper], RevalidateMediaReferenceError> { + func wallpapers(postbox: Postbox, network: Network, background: Bool, wallpaper: WallpaperReference?) -> Signal<[TelegramWallpaper], RevalidateMediaReferenceError> { return self.genericItem(key: .wallpapers, background: background, request: { next, error in - return (telegramWallpapers(postbox: postbox, network: network, forceUpdate: true) - |> last - |> mapError { _ -> RevalidateMediaReferenceError in - return .generic - }).start(next: { value in + let signal: Signal<[TelegramWallpaper]?, RevalidateMediaReferenceError> + if let wallpaper = wallpaper, case let .slug(slug) = wallpaper { + signal = getWallpaper(network: network, slug: slug) + |> mapError { _ -> RevalidateMediaReferenceError in + return .generic + } + |> map { [$0] } + } else { + signal = telegramWallpapers(postbox: postbox, network: network, forceUpdate: true) + |> last + |> mapError { _ -> RevalidateMediaReferenceError in + return .generic + } + } + return (signal + ).start(next: { value in if let value = value { next(value) } else { @@ -402,6 +415,26 @@ final class MediaReferenceRevalidationContext { } } } + + func themes(postbox: Postbox, network: Network, background: Bool) -> Signal<[TelegramTheme], RevalidateMediaReferenceError> { + return self.genericItem(key: .themes, background: background, request: { next, error in + return (telegramThemes(postbox: postbox, network: network, accountManager: nil, forceUpdate: true) + |> take(1) + |> mapError { _ -> RevalidateMediaReferenceError in + return .generic + }).start(next: { value in + next(value) + }, error: { _ in + error(.generic) + }) + }) |> mapToSignal { next -> Signal<[TelegramTheme], RevalidateMediaReferenceError> in + if let next = next as? [TelegramTheme] { + return .single(next) + } else { + return .fail(.generic) + } + } + } } struct RevalidatedMediaResource { @@ -412,12 +445,12 @@ struct RevalidatedMediaResource { func revalidateMediaResourceReference(postbox: Postbox, network: Network, revalidationContext: MediaReferenceRevalidationContext, info: TelegramCloudMediaResourceFetchInfo, resource: MediaResource) -> Signal { var updatedReference = info.reference if case let .media(media, resource) = updatedReference { - if case let .message(_, mediaValue) = media { + if case let .message(messageReference, mediaValue) = media { if let file = mediaValue as? TelegramMediaFile { if let partialReference = file.partialReference { updatedReference = partialReference.mediaReference(media.media).resourceReference(resource) } - if file.isSticker { + if file.isSticker, messageReference.isSecret == true { var stickerPackReference: StickerPackReference? for attribute in file.attributes { if case let .Sticker(sticker) = attribute { @@ -548,8 +581,8 @@ func revalidateMediaResourceReference(postbox: Postbox, network: Network, revali } return .fail(.generic) } - case .wallpaper: - return revalidationContext.wallpapers(postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation) + case let .wallpaper(wallpaper, _): + return revalidationContext.wallpapers(postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation, wallpaper: wallpaper) |> mapToSignal { wallpapers -> Signal in for wallpaper in wallpapers { switch wallpaper { @@ -586,6 +619,16 @@ func revalidateMediaResourceReference(postbox: Postbox, network: Network, revali } return .fail(.generic) } + case let .theme(themeReference, resource): + return revalidationContext.themes(postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation) + |> mapToSignal { themes -> Signal in + for theme in themes { + if let file = theme.file, file.resource.id.isEqual(to: resource.id) { + return .single(RevalidatedMediaResource(updatedResource: file.resource, updatedReference: nil)) + } + } + return .fail(.generic) + } case .standalone: return .fail(.generic) } diff --git a/submodules/TelegramCore/Sources/GroupsInCommon.swift b/submodules/TelegramCore/Sources/GroupsInCommon.swift index a81c7c9213..85e72c3aac 100644 --- a/submodules/TelegramCore/Sources/GroupsInCommon.swift +++ b/submodules/TelegramCore/Sources/GroupsInCommon.swift @@ -3,12 +3,164 @@ import Postbox import TelegramApi import SwiftSignalKit -public func groupsInCommon(account:Account, peerId:PeerId) -> Signal<[PeerId], NoError> { - return account.postbox.transaction { transaction -> Signal<[PeerId], NoError> in +public enum GroupsInCommonDataState: Equatable { + case loading + case ready(canLoadMore: Bool) +} + +public struct GroupsInCommonState: Equatable { + public var peers: [RenderedPeer] + public var count: Int? + public var dataState: GroupsInCommonDataState +} + +private final class GroupsInCommonContextImpl { + private let queue: Queue + private let account: Account + private let peerId: PeerId + + private let disposable = MetaDisposable() + + private var peers: [RenderedPeer] = [] + private var count: Int? + private var dataState: GroupsInCommonDataState = .ready(canLoadMore: true) + + private let stateValue = Promise() + var state: Signal { + return self.stateValue.get() + } + + init(queue: Queue, account: Account, peerId: PeerId) { + self.queue = queue + self.account = account + self.peerId = peerId + + self.loadMore(limit: 32) + } + + deinit { + self.disposable.dispose() + } + + func loadMore(limit: Int32) { + if case .ready(true) = self.dataState { + self.dataState = .loading + self.pushState() + + let maxId = self.peers.last?.peerId.id + let peerId = self.peerId + let network = self.account.network + let postbox = self.account.postbox + let signal: Signal<([Peer], Int), NoError> = self.account.postbox.transaction { transaction -> Api.InputUser? in + return transaction.getPeer(peerId).flatMap(apiInputUser) + } + |> mapToSignal { inputUser -> Signal<([Peer], Int), NoError> in + guard let inputUser = inputUser else { + return .single(([], 0)) + } + return network.request(Api.functions.messages.getCommonChats(userId: inputUser, maxId: maxId ?? 0, limit: limit)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<([Peer], Int), NoError> in + let chats: [Api.Chat] + let count: Int? + if let result = result { + switch result { + case let .chats(apiChats): + chats = apiChats + count = nil + case let .chatsSlice(apiCount, apiChats): + chats = apiChats + count = Int(apiCount) + } + } else { + chats = [] + count = nil + } + + + return postbox.transaction { transaction -> ([Peer], Int) in + var peers: [Peer] = [] + for chat in chats { + if let peer = parseTelegramGroupOrChannel(chat: chat) { + peers.append(peer) + } + } + updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer? in + return updated + }) + + return (peers, count ?? 0) + } + } + } + + self.disposable.set((signal + |> deliverOn(self.queue)).start(next: { [weak self] (peers, count) in + guard let strongSelf = self else { + return + } + var existingPeers = Set(strongSelf.peers.map { $0.peerId }) + for peer in peers { + if !existingPeers.contains(peer.id) { + existingPeers.insert(peer.id) + strongSelf.peers.append(RenderedPeer(peer: peer)) + } + } + + let updatedCount = max(strongSelf.peers.count, count) + strongSelf.count = updatedCount + strongSelf.dataState = .ready(canLoadMore: count != 0 && updatedCount > strongSelf.peers.count) + strongSelf.pushState() + })) + } + } + + private func pushState() { + self.stateValue.set(.single(GroupsInCommonState(peers: self.peers, count: self.count, dataState: self.dataState))) + } +} + +public final class GroupsInCommonContext { + private let queue: Queue = .mainQueue() + private let impl: QueueLocalObject + + public var state: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + + self.impl.with { impl in + disposable.set(impl.state.start(next: { value in + subscriber.putNext(value) + })) + } + + return disposable + } + } + + public init(account: Account, peerId: PeerId) { + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return GroupsInCommonContextImpl(queue: queue, account: account, peerId: peerId) + }) + } + + public func loadMore() { + self.impl.with { impl in + impl.loadMore(limit: 32) + } + } +} + +public func groupsInCommon(account: Account, peerId: PeerId) -> Signal<[Peer], NoError> { + return account.postbox.transaction { transaction -> Signal<[Peer], NoError> in if let peer = transaction.getPeer(peerId), let inputUser = apiInputUser(peer) { return account.network.request(Api.functions.messages.getCommonChats(userId: inputUser, maxId: 0, limit: 100)) |> retryRequest - |> mapToSignal { result -> Signal<[PeerId], NoError> in + |> mapToSignal { result -> Signal<[Peer], NoError> in let chats: [Api.Chat] switch result { case let .chats(chats: apiChats): @@ -17,8 +169,8 @@ public func groupsInCommon(account:Account, peerId:PeerId) -> Signal<[PeerId], N chats = apiChats } - return account.postbox.transaction { transaction -> [PeerId] in - var peers:[Peer] = [] + return account.postbox.transaction { transaction -> [Peer] in + var peers: [Peer] = [] for chat in chats { if let peer = parseTelegramGroupOrChannel(chat: chat) { peers.append(peer) @@ -27,7 +179,7 @@ public func groupsInCommon(account:Account, peerId:PeerId) -> Signal<[PeerId], N updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer? in return updated }) - return peers.map {$0.id} + return peers } } } else { diff --git a/submodules/TelegramCore/Sources/Holes.swift b/submodules/TelegramCore/Sources/Holes.swift index 711946cc3d..89e71dfeef 100644 --- a/submodules/TelegramCore/Sources/Holes.swift +++ b/submodules/TelegramCore/Sources/Holes.swift @@ -121,7 +121,9 @@ func withResolvedAssociatedMessages(postbox: Postbox, source: FetchMessageHistor |> switchToLatest } -func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryHoleSource, postbox: Postbox, peerId: PeerId, namespace: MessageId.Namespace, direction: MessageHistoryViewRelativeHoleDirection, space: MessageHistoryHoleSpace, limit: Int = 100) -> Signal { +func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryHoleSource, postbox: Postbox, peerId: PeerId, namespace: MessageId.Namespace, direction: MessageHistoryViewRelativeHoleDirection, space: MessageHistoryHoleSpace, count rawCount: Int) -> Signal { + let count = min(100, rawCount) + return postbox.stateView() |> mapToSignal { view -> Signal in if let state = view.state as? AuthorizedAccountState { @@ -136,8 +138,8 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH |> take(1) |> mapToSignal { peer in if let inputPeer = forceApiInputPeer(peer) { - print("fetchMessageHistoryHole for \(peer.debugDisplayTitle) \(direction) space \(space)") - Logger.shared.log("fetchMessageHistoryHole", "fetch for \(peer.debugDisplayTitle) \(direction) space \(space)") + print("fetchMessageHistoryHole for \(peer.id) \(peer.debugDisplayTitle) \(direction) space \(space)") + Logger.shared.log("fetchMessageHistoryHole", "fetch for \(peer.id) \(peer.debugDisplayTitle) \(direction) space \(space)") let request: Signal var implicitelyFillHole = false let minMaxRange: ClosedRange @@ -146,7 +148,7 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH case .everywhere: let offsetId: Int32 let addOffset: Int32 - let selectedLimit = limit + let selectedLimit = count let maxId: Int32 let minId: Int32 @@ -195,7 +197,7 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH if tag == .unseenPersonalMessage { let offsetId: Int32 let addOffset: Int32 - let selectedLimit = limit + let selectedLimit = count let maxId: Int32 let minId: Int32 @@ -241,7 +243,7 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH request = source.request(Api.functions.messages.getUnreadMentions(peer: inputPeer, offsetId: offsetId, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId)) } else if tag == .liveLocation { - let selectedLimit = limit + let selectedLimit = count switch direction { case .aroundId, .range: @@ -252,7 +254,7 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH } else if let filter = messageFilterForTagMask(tag) { let offsetId: Int32 let addOffset: Int32 - let selectedLimit = limit + let selectedLimit = count let maxId: Int32 let minId: Int32 @@ -364,7 +366,23 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH let _ = transaction.addMessages(storeMessages, location: .Random) let _ = transaction.addMessages(additionalMessages, location: .Random) let filledRange: ClosedRange - let ids = messages.compactMap({ $0.id()?.id }) + let ids = storeMessages.compactMap { message -> MessageId.Id? in + switch message.id { + case let .Id(id): + switch space { + case let .tag(tag): + if !message.tags.contains(tag) { + return nil + } else { + return id.id + } + case .everywhere: + return id.id + } + case .Partial: + return nil + } + } if ids.count == 0 || implicitelyFillHole { filledRange = minMaxRange } else { @@ -391,7 +409,7 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH }) updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences) - print("fetchMessageHistoryHole for \(peer.debugDisplayTitle) space \(space) done") + print("fetchMessageHistoryHole for \(peer.id) \(peer.debugDisplayTitle) space \(space) done") return }) diff --git a/submodules/TelegramCore/Sources/InactiveChannels.swift b/submodules/TelegramCore/Sources/InactiveChannels.swift new file mode 100644 index 0000000000..9d36592941 --- /dev/null +++ b/submodules/TelegramCore/Sources/InactiveChannels.swift @@ -0,0 +1,49 @@ + +import Foundation +import SwiftSignalKit +import Postbox +import TelegramApi + +public struct InactiveChannel : Equatable { + public let peer: Peer + public let lastActivityDate: Int32 + public let participantsCount: Int32? + + init(peer: Peer, lastActivityDate: Int32, participantsCount: Int32?) { + self.peer = peer + self.lastActivityDate = lastActivityDate + self.participantsCount = participantsCount + } + public static func ==(lhs: InactiveChannel, rhs: InactiveChannel) -> Bool { + return lhs.peer.isEqual(rhs.peer) && lhs.lastActivityDate == rhs.lastActivityDate && lhs.participantsCount == rhs.participantsCount + } +} + +public func inactiveChannelList(network: Network) -> Signal<[InactiveChannel], NoError> { + return network.request(Api.functions.channels.getInactiveChannels()) + |> retryRequest + |> map { result in + switch result { + case let .inactiveChats(dates, chats, users): + let channels = chats.compactMap { + parseTelegramGroupOrChannel(chat: $0) + } + var participantsCounts: [PeerId: Int32] = [:] + for chat in chats { + switch chat { + case let .channel(channel): + if let participantsCountValue = channel.participantsCount { + participantsCounts[chat.peerId] = channel.participantsCount + } + default: + break + } + } + var inactive: [InactiveChannel] = [] + for (i, channel) in channels.enumerated() { + inactive.append(InactiveChannel(peer: channel, lastActivityDate: dates[i], participantsCount: participantsCounts[channel.id])) + } + return inactive + } + } +} diff --git a/submodules/TelegramCore/Sources/JSON.swift b/submodules/TelegramCore/Sources/JSON.swift index a902ae10de..43dd62bebd 100644 --- a/submodules/TelegramCore/Sources/JSON.swift +++ b/submodules/TelegramCore/Sources/JSON.swift @@ -28,8 +28,7 @@ extension JSON { } } self = .array(values) - } - else if let value = object as? String { + } else if let value = object as? String { self = .string(value) } else if let value = object as? Int { self = .number(Double(value)) @@ -313,7 +312,7 @@ private extension Bool { } } -extension JSON { +public extension JSON { private init?(apiJson: Api.JSONValue, root: Bool) { switch (apiJson, root) { case (.jsonNull, false): diff --git a/submodules/TelegramCore/Sources/JoinChannel.swift b/submodules/TelegramCore/Sources/JoinChannel.swift index e230dd947c..6189c72da2 100644 --- a/submodules/TelegramCore/Sources/JoinChannel.swift +++ b/submodules/TelegramCore/Sources/JoinChannel.swift @@ -15,6 +15,9 @@ public func joinChannel(account: Account, peerId: PeerId) -> Signal take(1) |> castError(JoinChannelError.self) |> mapToSignal { peer -> Signal in + #if DEBUG + return .fail(.tooMuchJoined) + #endif if let inputChannel = apiInputChannel(peer) { return account.network.request(Api.functions.channels.joinChannel(channel: inputChannel)) |> mapError { error -> JoinChannelError in diff --git a/submodules/TelegramCore/Sources/MacInternalUpdater.swift b/submodules/TelegramCore/Sources/MacInternalUpdater.swift index 68a02b46e4..f9d48f6746 100644 --- a/submodules/TelegramCore/Sources/MacInternalUpdater.swift +++ b/submodules/TelegramCore/Sources/MacInternalUpdater.swift @@ -17,7 +17,7 @@ public func requestUpdatesXml(account: Account, source: String) -> Signal mapToSignal { peerId -> Signal in return account.postbox.transaction { transaction in return peerId != nil ? transaction.getPeer(peerId!) : nil - } |> castError(InternalUpdaterError.self) + } |> castError(InternalUpdaterError.self) } |> mapToSignal { peer in if let peer = peer, let inputPeer = apiInputPeer(peer) { @@ -78,7 +78,7 @@ public enum AppUpdateDownloadResult { case finished(String) } -public func downloadAppUpdate(account: Account, source: String, fileName: String) -> Signal { +public func downloadAppUpdate(account: Account, source: String, messageId: Int32) -> Signal { return resolvePeerByName(account: account, name: source) |> castError(InternalUpdaterError.self) |> mapToSignal { peerId -> Signal in @@ -87,12 +87,12 @@ public func downloadAppUpdate(account: Account, source: String, fileName: String } |> castError(InternalUpdaterError.self) } |> mapToSignal { peer in - if let peer = peer, let inputPeer = apiInputPeer(peer) { - return account.network.request(Api.functions.messages.getHistory(peer: inputPeer, offsetId: 0, offsetDate: 0, addOffset: 0, limit: 10, maxId: Int32.max, minId: 0, hash: 0)) + if let peer = peer, let inputChannel = apiInputChannel(peer) { + return account.network.request(Api.functions.channels.getMessages(channel: inputChannel, id: [Api.InputMessage.inputMessageID(id: messageId)])) |> retryRequest |> castError(InternalUpdaterError.self) - |> mapToSignal { result in - switch result { + |> mapToSignal { messages in + switch messages { case let .channelMessages(_, _, _, apiMessages, apiChats, apiUsers): var peers: [PeerId: Peer] = [:] @@ -113,11 +113,7 @@ public func downloadAppUpdate(account: Account, source: String, fileName: String }.sorted(by: { $0.id > $1.id }).first(where: { value -> Bool in - if let file = value.media.first as? TelegramMediaFile, file.fileName == fileName { - return true - } else { - return false - } + return value.media.first is TelegramMediaFile }).map { ($0, $0.media.first as! TelegramMediaFile )} if let (message, media) = messageAndFile { diff --git a/submodules/TelegramCore/Sources/ManageChannelDiscussionGroup.swift b/submodules/TelegramCore/Sources/ManageChannelDiscussionGroup.swift index 12ba154557..ab99f17e84 100644 --- a/submodules/TelegramCore/Sources/ManageChannelDiscussionGroup.swift +++ b/submodules/TelegramCore/Sources/ManageChannelDiscussionGroup.swift @@ -36,6 +36,7 @@ public enum ChannelDiscussionGroupError { case generic case groupHistoryIsCurrentlyPrivate case hasNotPermissions + case tooManyChannels } public func updateGroupDiscussionForChannel(network: Network, postbox: Postbox, channelId: PeerId?, groupId: PeerId?) -> Signal { diff --git a/submodules/TelegramCore/Sources/ManagedAppConfigurationUpdates.swift b/submodules/TelegramCore/Sources/ManagedAppConfigurationUpdates.swift index 78ef70938e..b7a757eee1 100644 --- a/submodules/TelegramCore/Sources/ManagedAppConfigurationUpdates.swift +++ b/submodules/TelegramCore/Sources/ManagedAppConfigurationUpdates.swift @@ -6,21 +6,31 @@ import MtProtoKit import SyncCore +func updateAppConfigurationOnce(postbox: Postbox, network: Network) -> Signal { + return network.request(Api.functions.help.getAppConfig()) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result = result else { + return .complete() + } + return postbox.transaction { transaction -> Void in + if let data = JSON(apiJson: result) { + updateAppConfiguration(transaction: transaction, { configuration -> AppConfiguration in + var configuration = configuration + configuration.data = data + return configuration + }) + } + } + } +} + func managedAppConfigurationUpdates(postbox: Postbox, network: Network) -> Signal { let poll = Signal { subscriber in - return (network.request(Api.functions.help.getAppConfig()) - |> retryRequest - |> mapToSignal { result -> Signal in - return postbox.transaction { transaction -> Void in - if let data = JSON(apiJson: result) { - updateAppConfiguration(transaction: transaction, { configuration -> AppConfiguration in - var configuration = configuration - configuration.data = data - return configuration - }) - } - } - }).start() + return updateAppConfigurationOnce(postbox: postbox, network: network).start() } - return (poll |> then(.complete() |> suspendAwareDelay(12.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart + return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart } diff --git a/submodules/TelegramCore/Sources/ManagedChatListHoles.swift b/submodules/TelegramCore/Sources/ManagedChatListHoles.swift index 8dbe238d06..c0a8885162 100644 --- a/submodules/TelegramCore/Sources/ManagedChatListHoles.swift +++ b/submodules/TelegramCore/Sources/ManagedChatListHoles.swift @@ -4,6 +4,7 @@ import SwiftSignalKit private final class ManagedChatListHolesState { private var holeDisposables: [ChatListHolesEntry: Disposable] = [:] + private var additionalLatestHoleDisposable: (ChatListHole, Disposable)? func clearDisposables() -> [Disposable] { let disposables = Array(self.holeDisposables.values) @@ -11,7 +12,7 @@ private final class ManagedChatListHolesState { return disposables } - func update(entries: Set) -> (removed: [Disposable], added: [ChatListHolesEntry: MetaDisposable]) { + func update(entries: Set, additionalLatestHole: ChatListHole?) -> (removed: [Disposable], added: [ChatListHolesEntry: MetaDisposable], addedAdditionalLatestHole: (ChatListHole, MetaDisposable)?) { var removed: [Disposable] = [] var added: [ChatListHolesEntry: MetaDisposable] = [:] @@ -30,7 +31,21 @@ private final class ManagedChatListHolesState { } } - return (removed, added) + var addedAdditionalLatestHole: (ChatListHole, MetaDisposable)? + if self.holeDisposables.isEmpty { + if self.additionalLatestHoleDisposable?.0 != additionalLatestHole { + if let (_, disposable) = self.additionalLatestHoleDisposable { + removed.append(disposable) + } + if let additionalLatestHole = additionalLatestHole { + let disposable = MetaDisposable() + self.additionalLatestHoleDisposable = (additionalLatestHole, disposable) + addedAdditionalLatestHole = (additionalLatestHole, disposable) + } + } + } + + return (removed, added, addedAdditionalLatestHole) } } @@ -38,9 +53,19 @@ func managedChatListHoles(network: Network, postbox: Postbox, accountPeerId: Pee return Signal { _ in let state = Atomic(value: ManagedChatListHolesState()) - let disposable = postbox.chatListHolesView().start(next: { view in - let (removed, added) = state.with { state -> (removed: [Disposable], added: [ChatListHolesEntry: MetaDisposable]) in - return state.update(entries: view.entries) + let topRootHoleKey = PostboxViewKey.allChatListHoles(.root) + let topRootHole = postbox.combinedView(keys: [topRootHoleKey]) + + let disposable = combineLatest(postbox.chatListHolesView(), topRootHole).start(next: { view, topRootHoleView in + var additionalLatestHole: ChatListHole? + if let topRootHole = topRootHoleView.views[topRootHoleKey] as? AllChatListHolesView { + #if os(macOS) + additionalLatestHole = topRootHole.latestHole + #endif + } + + let (removed, added, addedAdditionalLatestHole) = state.with { state in + return state.update(entries: view.entries, additionalLatestHole: additionalLatestHole) } for disposable in removed { @@ -50,6 +75,10 @@ func managedChatListHoles(network: Network, postbox: Postbox, accountPeerId: Pee for (entry, disposable) in added { disposable.set(fetchChatListHole(postbox: postbox, network: network, accountPeerId: accountPeerId, groupId: entry.groupId, hole: entry.hole).start()) } + + if let (hole, disposable) = addedAdditionalLatestHole { + disposable.set(fetchChatListHole(postbox: postbox, network: network, accountPeerId: accountPeerId, groupId: .root, hole: hole).start()) + } }) return ActionDisposable { diff --git a/submodules/TelegramCore/Sources/ManagedMessageHistoryHoles.swift b/submodules/TelegramCore/Sources/ManagedMessageHistoryHoles.swift index aa18476599..625fb1d7a7 100644 --- a/submodules/TelegramCore/Sources/ManagedMessageHistoryHoles.swift +++ b/submodules/TelegramCore/Sources/ManagedMessageHistoryHoles.swift @@ -49,8 +49,8 @@ func managedMessageHistoryHoles(accountPeerId: PeerId, network: Network, postbox for (entry, disposable) in added { switch entry.hole { - case let .peer(hole): - disposable.set(fetchMessageHistoryHole(accountPeerId: accountPeerId, source: .network(network), postbox: postbox, peerId: hole.peerId, namespace: hole.namespace, direction: entry.direction, space: entry.space).start()) + case let .peer(hole): + disposable.set(fetchMessageHistoryHole(accountPeerId: accountPeerId, source: .network(network), postbox: postbox, peerId: hole.peerId, namespace: hole.namespace, direction: entry.direction, space: entry.space, count: entry.count).start()) } } }) diff --git a/submodules/TelegramCore/Sources/ManagedSecretChatOutgoingOperations.swift b/submodules/TelegramCore/Sources/ManagedSecretChatOutgoingOperations.swift index 6ee05fe6e0..d490602165 100644 --- a/submodules/TelegramCore/Sources/ManagedSecretChatOutgoingOperations.swift +++ b/submodules/TelegramCore/Sources/ManagedSecretChatOutgoingOperations.swift @@ -676,6 +676,8 @@ private func decryptedEntities73(_ entities: [MessageTextEntity]?) -> [SecretApi break case .Underline: break + case .BankCard: + break case .Custom: break } @@ -723,6 +725,8 @@ private func decryptedEntities101(_ entities: [MessageTextEntity]?) -> [SecretAp result.append(.messageEntityBlockquote(offset: Int32(entity.range.lowerBound), length: Int32(entity.range.count))) case .Underline: result.append(.messageEntityUnderline(offset: Int32(entity.range.lowerBound), length: Int32(entity.range.count))) + case .BankCard: + break case .Custom: break } @@ -1415,7 +1419,7 @@ private func sendMessage(auxiliaryMethods: AccountAuxiliaryMethods, postbox: Pos } if let toMedia = toMedia { - applyMediaResourceChanges(from: fromMedia, to: toMedia, postbox: postbox) + applyMediaResourceChanges(from: fromMedia, to: toMedia, postbox: postbox, force: false) } } diff --git a/submodules/TelegramCore/Sources/MessageReactions.swift b/submodules/TelegramCore/Sources/MessageReactions.swift index 6bc15de67e..72e52dfa51 100644 --- a/submodules/TelegramCore/Sources/MessageReactions.swift +++ b/submodules/TelegramCore/Sources/MessageReactions.swift @@ -34,6 +34,7 @@ private enum RequestUpdateMessageReactionError { private func requestUpdateMessageReaction(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId) -> Signal { return .complete() + /*return postbox.transaction { transaction -> (Peer, String?)? in guard let peer = transaction.getPeer(messageId.peerId) else { return nil diff --git a/submodules/TelegramCore/Sources/MultipartFetch.swift b/submodules/TelegramCore/Sources/MultipartFetch.swift index 85eff452bc..1dc4acf550 100644 --- a/submodules/TelegramCore/Sources/MultipartFetch.swift +++ b/submodules/TelegramCore/Sources/MultipartFetch.swift @@ -385,7 +385,7 @@ private enum MultipartFetchSource { private final class MultipartFetchManager { let parallelParts: Int let defaultPartSize = 128 * 1024 - let partAlignment = 4 * 1024 + var partAlignment = 4 * 1024 var resource: TelegramMediaResource var resourceReference: MediaResourceReference? @@ -651,6 +651,7 @@ private final class MultipartFetchManager { case let .switchToCdn(id, token, key, iv, partHashes): switch strongSelf.source { case let .master(location, download): + strongSelf.partAlignment = Int(dataHashLength) strongSelf.source = .cdn(masterDatacenterId: location.datacenterId, fileToken: token, key: key, iv: iv, download: DownloadWrapper(consumerId: strongSelf.consumerId, datacenterId: id, isCdn: true, network: strongSelf.network), masterDownload: download, hashSource: MultipartCdnHashSource(queue: strongSelf.queue, fileToken: token, hashes: partHashes, masterDownload: download, continueInBackground: strongSelf.continueInBackground)) strongSelf.checkState() case .cdn, .none: diff --git a/submodules/TelegramCore/Sources/Network.swift b/submodules/TelegramCore/Sources/Network.swift index 4fca882c2f..24b84d3fff 100644 --- a/submodules/TelegramCore/Sources/Network.swift +++ b/submodules/TelegramCore/Sources/Network.swift @@ -399,6 +399,7 @@ func networkUsageStats(basePath: String, reset: ResetNetworkUsageStats) -> Signa public struct NetworkInitializationArguments { public let apiId: Int32 + public let apiHash: String public let languagesCategory: String public let appVersion: String public let voipMaxLayer: Int32 @@ -406,8 +407,9 @@ public struct NetworkInitializationArguments { public let autolockDeadine: Signal public let encryptionProvider: EncryptionProvider - public init(apiId: Int32, languagesCategory: String, appVersion: String, voipMaxLayer: Int32, appData: Signal, autolockDeadine: Signal, encryptionProvider: EncryptionProvider) { + public init(apiId: Int32, apiHash: String, languagesCategory: String, appVersion: String, voipMaxLayer: Int32, appData: Signal, autolockDeadine: Signal, encryptionProvider: EncryptionProvider) { self.apiId = apiId + self.apiHash = apiHash self.languagesCategory = languagesCategory self.appVersion = appVersion self.voipMaxLayer = voipMaxLayer @@ -512,7 +514,7 @@ func initializedNetwork(arguments: NetworkInitializationArguments, supplementary context.setDiscoverBackupAddressListSignal(MTBackupAddressSignals.fetchBackupIps(testingEnvironment, currentContext: context, additionalSource: wrappedAdditionalSource, phoneNumber: phoneNumber)) #if DEBUG - let _ = MTBackupAddressSignals.fetchBackupIps(testingEnvironment, currentContext: context, additionalSource: wrappedAdditionalSource, phoneNumber: phoneNumber).start(next: nil) + //let _ = MTBackupAddressSignals.fetchBackupIps(testingEnvironment, currentContext: context, additionalSource: wrappedAdditionalSource, phoneNumber: phoneNumber).start(next: nil) #endif let mtProto = MTProto(context: context, datacenterId: datacenterId, usageCalculationInfo: usageCalculationInfo(basePath: basePath, category: nil), requiredAuthToken: nil, authTokenMasterDatacenterId: 0)! @@ -586,13 +588,14 @@ func initializedNetwork(arguments: NetworkInitializationArguments, supplementary private final class NetworkHelper: NSObject, MTContextChangeListener { private let requestPublicKeys: (Int) -> Signal private let isContextNetworkAccessAllowedImpl: () -> Signal - private let contextProxyIdUpdated: (NetworkContextProxyId?) -> Void + private let contextLoggedOutUpdated: () -> Void - init(requestPublicKeys: @escaping (Int) -> Signal, isContextNetworkAccessAllowed: @escaping () -> Signal, contextProxyIdUpdated: @escaping (NetworkContextProxyId?) -> Void) { + init(requestPublicKeys: @escaping (Int) -> Signal, isContextNetworkAccessAllowed: @escaping () -> Signal, contextProxyIdUpdated: @escaping (NetworkContextProxyId?) -> Void, contextLoggedOutUpdated: @escaping () -> Void) { self.requestPublicKeys = requestPublicKeys self.isContextNetworkAccessAllowedImpl = isContextNetworkAccessAllowed self.contextProxyIdUpdated = contextProxyIdUpdated + self.contextLoggedOutUpdated = contextLoggedOutUpdated } func fetchContextDatacenterPublicKeys(_ context: MTContext!, datacenterId: Int) -> MTSignal! { @@ -625,6 +628,10 @@ private final class NetworkHelper: NSObject, MTContextChangeListener { let settings: MTSocksProxySettings? = apiEnvironment.socksProxySettings self.contextProxyIdUpdated(settings.flatMap(NetworkContextProxyId.init(settings:))) } + + func contextLoggedOut(_ context: MTContext!) { + self.contextLoggedOutUpdated() + } } struct NetworkContextProxyId: Equatable { @@ -772,6 +779,9 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate { } }, contextProxyIdUpdated: { value in _contextProxyId.set(value) + }, contextLoggedOutUpdated: { [weak self] in + Logger.shared.log("Network", "contextLoggedOut") + self?.loggedOut?() })) requestService.delegate = self diff --git a/submodules/TelegramCore/Sources/OutgoingMessageWithChatContextResult.swift b/submodules/TelegramCore/Sources/OutgoingMessageWithChatContextResult.swift index fb299307a9..9f7d61856a 100644 --- a/submodules/TelegramCore/Sources/OutgoingMessageWithChatContextResult.swift +++ b/submodules/TelegramCore/Sources/OutgoingMessageWithChatContextResult.swift @@ -55,7 +55,7 @@ public func outgoingMessageWithChatContextResult(to peerId: PeerId, results: Cha arc4random_buf(&randomId, 8) let thumbnailResource = thumbnail.resource let imageDimensions = thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128) - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: imageDimensions, resource: thumbnailResource)], immediateThumbnailData: nil, reference: nil, partialReference: nil) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: imageDimensions, resource: thumbnailResource)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) return .message(text: caption, attributes: attributes, mediaReference: .standalone(media: tmpImage), replyToMessageId: nil, localGroupingKey: nil) } else { return .message(text: caption, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil) diff --git a/submodules/TelegramCore/Sources/PeerAdmins.swift b/submodules/TelegramCore/Sources/PeerAdmins.swift index d5bc504a0c..416a0596e0 100644 --- a/submodules/TelegramCore/Sources/PeerAdmins.swift +++ b/submodules/TelegramCore/Sources/PeerAdmins.swift @@ -195,6 +195,8 @@ public func updateChannelAdminRights(account: Account, peerId: PeerId, adminId: ) } else if error.errorDescription == "USER_PRIVACY_RESTRICTED" { return .fail(.addMemberError(.restricted)) + } else if error.errorDescription == "USER_CHANNELS_TOO_MUCH" { + return .fail(.addMemberError(.tooMuchJoined)) } return .fail(.generic) } diff --git a/submodules/TelegramCore/Sources/PeerUtils.swift b/submodules/TelegramCore/Sources/PeerUtils.swift index bf457689a9..28571aa2ea 100644 --- a/submodules/TelegramCore/Sources/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/PeerUtils.swift @@ -1,6 +1,5 @@ import Foundation import Postbox - import SyncCore public extension Peer { @@ -17,7 +16,7 @@ public extension Peer { } } - func restrictionText(platform: String) -> String? { + func restrictionText(platform: String, contentSettings: ContentSettings) -> String? { var restrictionInfo: PeerAccessRestrictionInfo? switch self { case let user as TelegramUser: @@ -31,7 +30,9 @@ public extension Peer { if let restrictionInfo = restrictionInfo { for rule in restrictionInfo.rules { if rule.platform == "all" || rule.platform == platform { - return rule.text + if !contentSettings.ignoreContentRestrictionReasons.contains(rule.reason) { + return rule.text + } } } return nil diff --git a/submodules/TelegramCore/Sources/PeersNearby.swift b/submodules/TelegramCore/Sources/PeersNearby.swift index b872ec5837..99e8dedf7e 100644 --- a/submodules/TelegramCore/Sources/PeersNearby.swift +++ b/submodules/TelegramCore/Sources/PeersNearby.swift @@ -7,12 +7,69 @@ import SyncCore private typealias SignalKitTimer = SwiftSignalKit.Timer +public enum PeerNearby { + case selfPeer(expires: Int32) + case peer(id: PeerId, expires: Int32, distance: Int32) + + var expires: Int32 { + switch self { + case let .selfPeer(expires), let .peer(_, expires, _): + return expires + } + } +} +public enum PeerNearbyVisibilityUpdate { + case visible(latitude: Double, longitude: Double) + case location(latitude: Double, longitude: Double) + case invisible +} -public struct PeerNearby { - public let id: PeerId - public let expires: Int32 - public let distance: Int32 +public func peersNearbyUpdateVisibility(account: Account, update: PeerNearbyVisibilityUpdate, background: Bool) -> Signal { + var flags: Int32 = 0 + var geoPoint: Api.InputGeoPoint + var selfExpires: Int32? + + switch update { + case let .visible(latitude, longitude): + flags |= (1 << 0) + geoPoint = .inputGeoPoint(lat: latitude, long: longitude) + selfExpires = 0x7fffffff + case let .location(latitude, longitude): + geoPoint = .inputGeoPoint(lat: latitude, long: longitude) + case .invisible: + flags |= (1 << 0) + geoPoint = .inputGeoPointEmpty + selfExpires = 0 + } + + let _ = (account.postbox.transaction { transaction in + transaction.updatePreferencesEntry(key: PreferencesKeys.peersNearby, { entry in + var settings = entry as? PeersNearbyState ?? PeersNearbyState.default + if case .invisible = update { + settings.visibilityExpires = nil + } else if let expires = selfExpires { + settings.visibilityExpires = expires + } + return settings + }) + }).start() + + if background { + flags |= (1 << 1) + } + + return account.network.request(Api.functions.contacts.getLocated(flags: flags, geoPoint: geoPoint, selfExpires: selfExpires)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates = updates { + account.stateManager.addUpdates(updates) + } + return .complete() + } } public final class PeersNearbyContext { @@ -23,10 +80,10 @@ public final class PeersNearbyContext { private var entries: [PeerNearby]? - public init(network: Network, accountStateManager: AccountStateManager, coordinate: (latitude: Double, longitude: Double)) { + public init(network: Network, stateManager: AccountStateManager, coordinate: (latitude: Double, longitude: Double)) { let expiryExtension: Double = 10.0 - let poll = network.request(Api.functions.contacts.getLocated(geoPoint: .inputGeoPoint(lat: coordinate.latitude, long: coordinate.longitude))) + let poll = network.request(Api.functions.contacts.getLocated(flags: 0, geoPoint: .inputGeoPoint(lat: coordinate.latitude, long: coordinate.longitude), selfExpires: nil)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -39,23 +96,28 @@ public final class PeersNearbyContext { case let .updates(updates, _, _, _, _): for update in updates { if case let .updatePeerLocated(peers) = update { - for case let .peerLocated(peer, expires, distance) in peers { - peersNearby.append(PeerNearby(id: peer.peerId, expires: expires, distance: distance)) + for peer in peers { + switch peer { + case let .peerLocated(peer, expires, distance): + peersNearby.append(.peer(id: peer.peerId, expires: expires, distance: distance)) + case let .peerSelfLocated(expires): + peersNearby.append(.selfPeer(expires: expires)) + } } } } default: break } - accountStateManager.addUpdates(updates) + stateManager.addUpdates(updates) } return .single(peersNearby) |> then( - accountStateManager.updatedPeersNearby() + stateManager.updatedPeersNearby() |> castError(Void.self) ) } - + let error: Signal = .single(Void()) |> then(Signal.fail(Void()) |> suspendAwareDelay(25.0, queue: self.queue)) let combined = combineLatest(poll, error) |> map { data, _ -> [PeerNearby] in @@ -77,18 +139,37 @@ public final class PeersNearbyContext { let updatedEntries = updatedEntries.filter { Double($0.expires) + expiryExtension > timestamp } var existingPeerIds: [PeerId: Int] = [:] + var existingSelfPeer: Int? for i in 0 ..< entries.count { - existingPeerIds[entries[i].id] = i + if case let .peer(id, _, _) = entries[i] { + existingPeerIds[id] = i + } else if case .selfPeer = entries[i] { + existingSelfPeer = i + } } + var selfPeer: PeerNearby? for entry in updatedEntries { - if let index = existingPeerIds[entry.id] { - entries[index] = entry - } else { - entries.append(entry) + switch entry { + case let .selfPeer: + if let index = existingSelfPeer { + entries[index] = entry + } else { + selfPeer = entry + } + case let .peer(id, _, _): + if let index = existingPeerIds[id] { + entries[index] = entry + } else { + entries.append(entry) + } } } + if let peer = selfPeer { + entries.insert(peer, at: 0) + } + strongSelf.entries = entries for subscriber in strongSelf.subscribers.copyItems() { subscriber(strongSelf.entries) @@ -189,3 +270,33 @@ public func updateChannelGeoLocation(postbox: Postbox, network: Network, channel } } } + +public struct PeersNearbyState: PreferencesEntry, Equatable { + public var visibilityExpires: Int32? + + public static var `default` = PeersNearbyState(visibilityExpires: nil) + + public init(visibilityExpires: Int32?) { + self.visibilityExpires = visibilityExpires + } + + public init(decoder: PostboxDecoder) { + self.visibilityExpires = decoder.decodeOptionalInt32ForKey("expires") + } + + public func encode(_ encoder: PostboxEncoder) { + if let expires = self.visibilityExpires { + encoder.encodeInt32(expires, forKey: "expires") + } else { + encoder.encodeNil(forKey: "expires") + } + } + + public func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? PeersNearbyState, self == to { + return true + } else { + return false + } + } +} diff --git a/submodules/TelegramCore/Sources/PendingMessageManager.swift b/submodules/TelegramCore/Sources/PendingMessageManager.swift index 0be097728d..9cb6459e60 100644 --- a/submodules/TelegramCore/Sources/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/PendingMessageManager.swift @@ -75,7 +75,7 @@ private func reasonForError(_ error: String) -> PendingMessageFailureReason? { } private final class PeerPendingMessagesSummaryContext { - var messageDeliveredSubscribers = Bag<() -> Void>() + var messageDeliveredSubscribers = Bag<(MessageId.Namespace) -> Void>() var messageFailedSubscribers = Bag<(PendingMessageFailureReason) -> Void>() } @@ -239,7 +239,7 @@ public final class PendingMessageManager { for peerId in peerIdsWithDeliveredMessages { if let context = strongSelf.peerSummaryContexts[peerId] { for subscriber in context.messageDeliveredSubscribers.copyItems() { - subscriber() + subscriber(Namespaces.Message.Cloud) } } } @@ -355,6 +355,8 @@ public final class PendingMessageManager { strongSelf.collectUploadingInfo(messageContext: messageContext, message: message) } + var messagesToUpload: [(PendingMessageContext, Message, PendingMessageUploadedContentType, Signal)] = [] + var messagesToForward: [PeerIdAndNamespace: [(PendingMessageContext, Message, ForwardSourceInfoAttribute)]] = [:] for (messageContext, _) in strongSelf.messageContexts.values.compactMap({ messageContext -> (PendingMessageContext, Message)? in if case let .collectingInfo(message) = messageContext.state { return (messageContext, message) @@ -365,16 +367,58 @@ public final class PendingMessageManager { return lhs.1.index < rhs.1.index }) { if case let .collectingInfo(message) = messageContext.state { - let (contentUploadSignal, contentType) = messageContentToUpload(network: strongSelf.network, postbox: strongSelf.postbox, auxiliaryMethods: strongSelf.auxiliaryMethods, transformOutgoingMessageMedia: strongSelf.transformOutgoingMessageMedia, messageMediaPreuploadManager: strongSelf.messageMediaPreuploadManager, revalidationContext: strongSelf.revalidationContext, forceReupload: messageContext.forcedReuploadOnce, isGrouped: message.groupingKey != nil, message: message) - messageContext.contentType = contentType - - if strongSelf.canBeginUploadingMessage(id: message.id, type: contentType) { - strongSelf.beginUploadingMessage(messageContext: messageContext, id: message.id, groupId: message.groupingKey, uploadSignal: contentUploadSignal) - } else { - messageContext.state = .waitingForUploadToStart(groupId: message.groupingKey, upload: contentUploadSignal) + let contentToUpload = messageContentToUpload(network: strongSelf.network, postbox: strongSelf.postbox, auxiliaryMethods: strongSelf.auxiliaryMethods, transformOutgoingMessageMedia: strongSelf.transformOutgoingMessageMedia, messageMediaPreuploadManager: strongSelf.messageMediaPreuploadManager, revalidationContext: strongSelf.revalidationContext, forceReupload: messageContext.forcedReuploadOnce, isGrouped: message.groupingKey != nil, message: message) + messageContext.contentType = contentToUpload.type + switch contentToUpload { + case let .immediate(result, type): + var isForward = false + switch result { + case let .content(content): + switch content.content { + case let .forward(forwardInfo): + isForward = true + let peerIdAndNamespace = PeerIdAndNamespace(peerId: message.id.peerId, namespace: message.id.namespace) + if messagesToForward[peerIdAndNamespace] == nil { + messagesToForward[peerIdAndNamespace] = [] + } + messagesToForward[peerIdAndNamespace]!.append((messageContext, message, forwardInfo)) + default: + break + } + default: + break + } + if !isForward { + messagesToUpload.append((messageContext, message, type, .single(result))) + } + case let .signal(signal, type): + messagesToUpload.append((messageContext, message, type, signal)) } } } + + for (messageContext, message, type, contentUploadSignal) in messagesToUpload { + if strongSelf.canBeginUploadingMessage(id: message.id, type: type) { + strongSelf.beginUploadingMessage(messageContext: messageContext, id: message.id, groupId: message.groupingKey, uploadSignal: contentUploadSignal) + } else { + messageContext.state = .waitingForUploadToStart(groupId: message.groupingKey, upload: contentUploadSignal) + } + } + + for (_, messages) in messagesToForward { + for (context, _, _) in messages { + context.state = .sending(groupId: nil) + } + let sendMessage: Signal = strongSelf.sendGroupMessagesContent(network: strongSelf.network, postbox: strongSelf.postbox, stateManager: strongSelf.stateManager, group: messages.map { data in + let (_, message, forwardInfo) = data + return (message.id, PendingMessageUploadedContentAndReuploadInfo(content: .forward(forwardInfo), reuploadInfo: nil)) + }) + |> map { next -> PendingMessageResult in + return .progress(1.0) + } + messages[0].0.sendDisposable.set((sendMessage + |> deliverOn(strongSelf.queue)).start()) + } } })) } @@ -502,7 +546,8 @@ public final class PendingMessageManager { } } return .complete() - }).start(next: { [weak self] next in + } + |> deliverOn(queue)).start(next: { [weak self] next in if let strongSelf = self { assert(strongSelf.queue.isCurrent()) @@ -630,7 +675,9 @@ public final class PendingMessageManager { let sendMessageRequest: Signal if isForward { - flags |= (1 << 9) + if !messages.contains(where: { $0.0.groupingKey == nil }) { + flags |= (1 << 9) + } var forwardIds: [(MessageId, Int64)] = [] for (message, content) in messages { @@ -1044,13 +1091,28 @@ public final class PendingMessageManager { } private func applySentMessage(postbox: Postbox, stateManager: AccountStateManager, message: Message, result: Api.Updates) -> Signal { + var apiMessage: Api.Message? + for resultMessage in result.messages { + if let id = resultMessage.id(namespace: Namespaces.Message.allScheduled.contains(message.id.namespace) ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) { + if id.peerId == message.id.peerId { + apiMessage = resultMessage + break + } + } + } + + var namespace = Namespaces.Message.Cloud + if let apiMessage = apiMessage, let id = apiMessage.id(namespace: message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) { + namespace = id.namespace + } + return applyUpdateMessage(postbox: postbox, stateManager: stateManager, message: message, result: result) |> afterDisposed { [weak self] in if let strongSelf = self { strongSelf.queue.async { if let context = strongSelf.peerSummaryContexts[message.id.peerId] { for subscriber in context.messageDeliveredSubscribers.copyItems() { - subscriber() + subscriber(namespace) } } } @@ -1059,13 +1121,18 @@ public final class PendingMessageManager { } private func applySentGroupMessages(postbox: Postbox, stateManager: AccountStateManager, messages: [Message], result: Api.Updates) -> Signal { + var namespace = Namespaces.Message.Cloud + if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { + namespace = Namespaces.Message.ScheduledCloud + } + return applyUpdateGroupMessages(postbox: postbox, stateManager: stateManager, messages: messages, result: result) |> afterDisposed { [weak self] in if let strongSelf = self { strongSelf.queue.async { - if let peerId = messages.first?.id.peerId, let context = strongSelf.peerSummaryContexts[peerId] { + if let message = messages.first, let context = strongSelf.peerSummaryContexts[message.id.peerId] { for subscriber in context.messageDeliveredSubscribers.copyItems() { - subscriber() + subscriber(namespace) } } } @@ -1073,7 +1140,7 @@ public final class PendingMessageManager { } } - public func deliveredMessageEvents(peerId: PeerId) -> Signal { + public func deliveredMessageEvents(peerId: PeerId) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() @@ -1086,8 +1153,8 @@ public final class PendingMessageManager { self.peerSummaryContexts[peerId] = summaryContext } - let index = summaryContext.messageDeliveredSubscribers.add({ - subscriber.putNext(true) + let index = summaryContext.messageDeliveredSubscribers.add({ namespace in + subscriber.putNext(namespace) }) disposable.set(ActionDisposable { diff --git a/submodules/TelegramCore/Sources/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessageUploadedContent.swift index 5c3cd4a7f5..e808ed503f 100644 --- a/submodules/TelegramCore/Sources/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessageUploadedContent.swift @@ -38,11 +38,25 @@ enum PendingMessageUploadError { case generic } -func messageContentToUpload(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, message: Message) -> (Signal, PendingMessageUploadedContentType) { +enum MessageContentToUpload { + case signal(Signal, PendingMessageUploadedContentType) + case immediate(PendingMessageUploadedContentResult, PendingMessageUploadedContentType) + + var type: PendingMessageUploadedContentType { + switch self { + case let .signal(_, type): + return type + case let .immediate(_, type): + return type + } + } +} + +func messageContentToUpload(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, message: Message) -> MessageContentToUpload { return messageContentToUpload(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, peerId: message.id.peerId, messageId: message.id, attributes: message.attributes, text: message.text, media: message.media) } -func messageContentToUpload(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, peerId: PeerId, messageId: MessageId?, attributes: [MessageAttribute], text: String, media: [Media]) -> (Signal, PendingMessageUploadedContentType) { +func messageContentToUpload(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, peerId: PeerId, messageId: MessageId?, attributes: [MessageAttribute], text: String, media: [Media]) -> MessageContentToUpload { var contextResult: OutgoingChatContextResultMessageAttribute? var autoremoveAttribute: AutoremoveTimeoutMessageAttribute? for attribute in attributes { @@ -65,15 +79,15 @@ func messageContentToUpload(network: Network, postbox: Postbox, auxiliaryMethods } if let media = media.first as? TelegramMediaAction, media.action == .historyScreenshot { - return (.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .messageScreenshot, reuploadInfo: nil))), .none) + return .immediate(.content(PendingMessageUploadedContentAndReuploadInfo(content: .messageScreenshot, reuploadInfo: nil)), .none) } else if let forwardInfo = forwardInfo { - return (.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .forward(forwardInfo), reuploadInfo: nil))), .text) + return .immediate(.content(PendingMessageUploadedContentAndReuploadInfo(content: .forward(forwardInfo), reuploadInfo: nil)), .text) } else if let contextResult = contextResult { - return (.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .chatContextResult(contextResult), reuploadInfo: nil))), .text) + return .immediate(.content(PendingMessageUploadedContentAndReuploadInfo(content: .chatContextResult(contextResult), reuploadInfo: nil)), .text) } else if let media = media.first, let mediaResult = mediaContentToUpload(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, peerId: peerId, media: media, text: text, autoremoveAttribute: autoremoveAttribute, messageId: messageId, attributes: attributes) { - return (mediaResult, .media) + return .signal(mediaResult, .media) } else { - return (.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .text(text), reuploadInfo: nil))), .text) + return .signal(.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .text(text), reuploadInfo: nil))), .text) } } @@ -140,7 +154,28 @@ func mediaContentToUpload(network: Network, postbox: Postbox, auxiliaryMethods: if peerId.namespace == Namespaces.Peer.SecretChat { return .fail(.generic) } - let inputPoll = Api.InputMedia.inputMediaPoll(poll: Api.Poll.poll(id: 0, flags: 0, question: poll.text, answers: poll.options.map({ $0.apiOption }))) + var pollFlags: Int32 = 0 + switch poll.kind { + case let .poll(multipleAnswers): + if multipleAnswers { + pollFlags |= 1 << 2 + } + case .quiz: + pollFlags |= 1 << 3 + } + switch poll.publicity { + case .anonymous: + break + case .public: + pollFlags |= 1 << 1 + } + var pollMediaFlags: Int32 = 0 + var correctAnswers: [Buffer]? + if let correctAnswersValue = poll.correctAnswers { + pollMediaFlags |= 1 << 0 + correctAnswers = correctAnswersValue.map { Buffer(data: $0) } + } + let inputPoll = Api.InputMedia.inputMediaPoll(flags: pollMediaFlags, poll: Api.Poll.poll(id: 0, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption })), correctAnswers: correctAnswers) return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputPoll, text), reuploadInfo: nil))) } else { return nil @@ -350,6 +385,22 @@ private func uploadedMediaImageContent(network: Network, postbox: Postbox, trans flags |= 1 << 1 ttlSeconds = autoremoveAttribute.timeout } + var stickers: [Api.InputDocument]? + for attribute in attributes { + if let attribute = attribute as? EmbeddedMediaStickersMessageAttribute { + var stickersValue: [Api.InputDocument] = [] + for file in attribute.files { + if let resource = file.resource as? CloudDocumentMediaResource, let fileReference = resource.fileReference { + stickersValue.append(Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference))) + } + } + if !stickersValue.isEmpty { + stickers = stickersValue + flags |= 1 << 0 + } + break + } + } return postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) } @@ -357,10 +408,10 @@ private func uploadedMediaImageContent(network: Network, postbox: Postbox, trans |> mapToSignal { inputPeer -> Signal in if let inputPeer = inputPeer { if autoremoveAttribute != nil { - return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaUploadedPhoto(flags: flags, file: file, stickers: nil, ttlSeconds: ttlSeconds), text), reuploadInfo: nil))) + return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaUploadedPhoto(flags: flags, file: file, stickers: stickers, ttlSeconds: ttlSeconds), text), reuploadInfo: nil))) } - return network.request(Api.functions.messages.uploadMedia(peer: inputPeer, media: Api.InputMedia.inputMediaUploadedPhoto(flags: flags, file: file, stickers: nil, ttlSeconds: ttlSeconds))) + return network.request(Api.functions.messages.uploadMedia(peer: inputPeer, media: Api.InputMedia.inputMediaUploadedPhoto(flags: flags, file: file, stickers: stickers, ttlSeconds: ttlSeconds))) |> mapError { _ -> PendingMessageUploadError in return .generic } |> mapToSignal { result -> Signal in switch result { diff --git a/submodules/TelegramCore/Sources/PendingUpdateMessageManager.swift b/submodules/TelegramCore/Sources/PendingUpdateMessageManager.swift new file mode 100644 index 0000000000..f4e6adbe11 --- /dev/null +++ b/submodules/TelegramCore/Sources/PendingUpdateMessageManager.swift @@ -0,0 +1,178 @@ +import Foundation +import SwiftSignalKit +import SyncCore +import Postbox + +private final class PendingUpdateMessageContext { + var value: ChatUpdatingMessageMedia + let disposable: Disposable + + init(value: ChatUpdatingMessageMedia, disposable: Disposable) { + self.value = value + self.disposable = disposable + } +} + +private final class PendingUpdateMessageManagerImpl { + let queue: Queue + let postbox: Postbox + let network: Network + let stateManager: AccountStateManager + let messageMediaPreuploadManager: MessageMediaPreuploadManager + let mediaReferenceRevalidationContext: MediaReferenceRevalidationContext + + var transformOutgoingMessageMedia: TransformOutgoingMessageMedia? + + private var updatingMessageMediaValue: [MessageId: ChatUpdatingMessageMedia] = [:] { + didSet { + if self.updatingMessageMediaValue != oldValue { + self.updatingMessageMediaPromise.set(.single(self.updatingMessageMediaValue)) + } + } + } + private let updatingMessageMediaPromise = Promise<[MessageId: ChatUpdatingMessageMedia]>() + var updatingMessageMedia: Signal<[MessageId: ChatUpdatingMessageMedia], NoError> { + return self.updatingMessageMediaPromise.get() + } + + private var contexts: [MessageId: PendingUpdateMessageContext] = [:] + + private let errorsPipe = ValuePipe<(MessageId, RequestEditMessageError)>() + var errors: Signal<(MessageId, RequestEditMessageError), NoError> { + return self.errorsPipe.signal() + } + + init(queue: Queue, postbox: Postbox, network: Network, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext) { + self.queue = queue + self.postbox = postbox + self.network = network + self.stateManager = stateManager + self.messageMediaPreuploadManager = messageMediaPreuploadManager + self.mediaReferenceRevalidationContext = mediaReferenceRevalidationContext + + self.updatingMessageMediaPromise.set(.single(self.updatingMessageMediaValue)) + } + + deinit { + for (_, context) in self.contexts { + context.disposable.dispose() + } + } + + private func updateValues() { + self.updatingMessageMediaValue = self.contexts.mapValues { context in + return context.value + } + } + + func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, disableUrlPreview: Bool) { + if let context = self.contexts[messageId] { + self.contexts.removeValue(forKey: messageId) + context.disposable.dispose() + } + + let disposable = MetaDisposable() + let context = PendingUpdateMessageContext(value: ChatUpdatingMessageMedia(text: text, entities: entities, disableUrlPreview: disableUrlPreview, media: media, progress: 0.0), disposable: disposable) + self.contexts[messageId] = context + + let queue = self.queue + disposable.set((requestEditMessage(postbox: self.postbox, network: self.network, stateManager: self.stateManager, transformOutgoingMessageMedia: self.transformOutgoingMessageMedia, messageMediaPreuploadManager: self.messageMediaPreuploadManager, mediaReferenceRevalidationContext: self.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, disableUrlPreview: disableUrlPreview, scheduleTime: nil) + |> deliverOn(self.queue)).start(next: { [weak self, weak context] value in + queue.async { + guard let strongSelf = self, let initialContext = context else { + return + } + if let context = strongSelf.contexts[messageId], context === initialContext { + switch value { + case .done: + strongSelf.contexts.removeValue(forKey: messageId) + context.disposable.dispose() + strongSelf.updateValues() + case let .progress(progress): + context.value = context.value.withProgress(progress) + strongSelf.updateValues() + } + } + } + }, error: { [weak self, weak context] error in + queue.async { + guard let strongSelf = self, let initialContext = context else { + return + } + if let context = strongSelf.contexts[messageId], context === initialContext { + strongSelf.contexts.removeValue(forKey: messageId) + context.disposable.dispose() + strongSelf.updateValues() + } + + strongSelf.errorsPipe.putNext((messageId, error)) + } + })) + } + + func cancel(messageId: MessageId) { + if let context = self.contexts[messageId] { + self.contexts.removeValue(forKey: messageId) + context.disposable.dispose() + + self.updateValues() + } + } +} + +public final class PendingUpdateMessageManager { + private let queue = Queue() + private let impl: QueueLocalObject + + var transformOutgoingMessageMedia: TransformOutgoingMessageMedia? { + didSet { + let transformOutgoingMessageMedia = self.transformOutgoingMessageMedia + self.impl.with { impl in + impl.transformOutgoingMessageMedia = transformOutgoingMessageMedia + } + } + } + + public var updatingMessageMedia: Signal<[MessageId: ChatUpdatingMessageMedia], NoError> { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.updatingMessageMedia.start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } + + public var errors: Signal<(MessageId, RequestEditMessageError), NoError> { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.errors.start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } + + init(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext) { + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return PendingUpdateMessageManagerImpl(queue: queue, postbox: postbox, network: network, stateManager: stateManager, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext) + }) + } + + public func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute? = nil, disableUrlPreview: Bool = false) { + self.impl.with { impl in + impl.add(messageId: messageId, text: text, media: media, entities: entities, disableUrlPreview: disableUrlPreview) + } + } + + public func cancel(messageId: MessageId) { + self.impl.with { impl in + impl.cancel(messageId: messageId) + } + } +} diff --git a/submodules/TelegramCore/Sources/Polls.swift b/submodules/TelegramCore/Sources/Polls.swift index 207cd11cfb..803323a618 100644 --- a/submodules/TelegramCore/Sources/Polls.swift +++ b/submodules/TelegramCore/Sources/Polls.swift @@ -10,22 +10,72 @@ public enum RequestMessageSelectPollOptionError { case generic } -public func requestMessageSelectPollOption(account: Account, messageId: MessageId, opaqueIdentifier: Data?) -> Signal { +public func requestMessageSelectPollOption(account: Account, messageId: MessageId, opaqueIdentifiers: [Data]) -> Signal { return account.postbox.loadedPeerWithId(messageId.peerId) |> take(1) |> castError(RequestMessageSelectPollOptionError.self) |> mapToSignal { peer in if let inputPeer = apiInputPeer(peer) { - return account.network.request(Api.functions.messages.sendVote(peer: inputPeer, msgId: messageId.id, options: opaqueIdentifier.flatMap({ [Buffer(data: $0)] }) ?? [])) + return account.network.request(Api.functions.messages.sendVote(peer: inputPeer, msgId: messageId.id, options: opaqueIdentifiers.map { Buffer(data: $0) })) |> mapError { _ -> RequestMessageSelectPollOptionError in return .generic } - |> mapToSignal { result -> Signal in - account.stateManager.addUpdates(result) - return .complete() + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> TelegramMediaPoll? in + var resultPoll: TelegramMediaPoll? + switch result { + case let .updates(updates, _, _, _, _): + for update in updates { + switch update { + case let .updateMessagePoll(_, id, poll, results): + let pollId = MediaId(namespace: Namespaces.Media.CloudPoll, id: id) + resultPoll = transaction.getMedia(pollId) as? TelegramMediaPoll + if let poll = poll { + switch poll { + case let .poll(id, flags, question, answers): + let publicity: TelegramMediaPollPublicity + if (flags & (1 << 1)) != 0 { + publicity = .public + } else { + publicity = .anonymous + } + let kind: TelegramMediaPollKind + if (flags & (1 << 3)) != 0 { + kind = .quiz + } else { + kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0) + } + resultPoll = TelegramMediaPoll(pollId: pollId, publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0) + default: + break + } + } + + let resultsMin: Bool + switch results { + case let .pollResults(pollResults): + resultsMin = (pollResults.flags & (1 << 0)) != 0 + } + resultPoll = resultPoll?.withUpdatedResults(TelegramMediaPollResults(apiResults: results), min: resultsMin) + + if let resultPoll = resultPoll { + updateMessageMedia(transaction: transaction, id: pollId, media: resultPoll) + } + default: + break + } + } + break + default: + break + } + account.stateManager.addUpdates(result) + return resultPoll + } + |> castError(RequestMessageSelectPollOptionError.self) } } else { - return .complete() + return .single(nil) } } } @@ -51,7 +101,32 @@ public func requestClosePoll(postbox: Postbox, network: Network, stateManager: A } var flags: Int32 = 0 flags |= 1 << 14 - return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(poll: .poll(id: poll.pollId.id, flags: 1 << 0, question: poll.text, answers: poll.options.map({ $0.apiOption }))), replyMarkup: nil, entities: nil, scheduleDate: nil)) + + var pollFlags: Int32 = 0 + switch poll.kind { + case let .poll(multipleAnswers): + if multipleAnswers { + pollFlags |= 1 << 2 + } + case .quiz: + pollFlags |= 1 << 3 + } + switch poll.publicity { + case .anonymous: + break + case .public: + pollFlags |= 1 << 1 + } + var pollMediaFlags: Int32 = 0 + var correctAnswers: [Buffer]? + if let correctAnswersValue = poll.correctAnswers { + pollMediaFlags |= 1 << 0 + correctAnswers = correctAnswersValue.map { Buffer(data: $0) } + } + + pollFlags |= 1 << 0 + + return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(flags: pollMediaFlags, poll: .poll(id: poll.pollId.id, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption })), correctAnswers: correctAnswers), replyMarkup: nil, entities: nil, scheduleDate: nil)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -64,3 +139,280 @@ public func requestClosePoll(postbox: Postbox, network: Network, stateManager: A } } } + +private let cachedPollResultsCollectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 20, highWaterItemCount: 40) + +final class CachedPollOptionResult: PostboxCoding { + let peerIds: [PeerId] + let count: Int32 + + public static func key(pollId: MediaId, optionOpaqueIdentifier: Data) -> ValueBoxKey { + let key = ValueBoxKey(length: 4 + 8 + optionOpaqueIdentifier.count) + key.setInt32(0, value: pollId.namespace) + key.setInt64(4, value: pollId.id) + key.setData(4 + 8, value: optionOpaqueIdentifier) + return key + } + + public init(peerIds: [PeerId], count: Int32) { + self.peerIds = peerIds + self.count = count + } + + public init(decoder: PostboxDecoder) { + self.peerIds = decoder.decodeInt64ArrayForKey("peerIds").map(PeerId.init) + self.count = decoder.decodeInt32ForKey("count", orElse: 0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt64Array(self.peerIds.map { $0.toInt64() }, forKey: "peerIds") + encoder.encodeInt32(self.count, forKey: "count") + } +} + +private final class PollResultsOptionContext { + private let queue: Queue + private let account: Account + private let pollId: MediaId + private let messageId: MessageId + private let opaqueIdentifier: Data + private let disposable = MetaDisposable() + private var isLoadingMore: Bool = false + private var hasLoadedOnce: Bool = false + private var canLoadMore: Bool = true + private var nextOffset: String? + private var results: [RenderedPeer] = [] + private var count: Int + private var populateCache: Bool = true + + let state = Promise() + + init(queue: Queue, account: Account, pollId: MediaId, messageId: MessageId, opaqueIdentifier: Data, count: Int) { + self.queue = queue + self.account = account + self.pollId = pollId + self.messageId = messageId + self.opaqueIdentifier = opaqueIdentifier + self.count = count + + self.isLoadingMore = true + self.disposable.set((account.postbox.transaction { transaction -> (peers: [RenderedPeer], canLoadMore: Bool)? in + let cachedResult = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPollResults, key: CachedPollOptionResult.key(pollId: pollId, optionOpaqueIdentifier: opaqueIdentifier))) as? CachedPollOptionResult + if let cachedResult = cachedResult, Int(cachedResult.count) == count { + var result: [RenderedPeer] = [] + for peerId in cachedResult.peerIds { + if let peer = transaction.getPeer(peerId) { + result.append(RenderedPeer(peer: peer)) + } else { + return nil + } + } + return (result, Int(cachedResult.count) > result.count) + } else { + return nil + } + } + |> deliverOn(self.queue)).start(next: { [weak self] cachedPeersAndCanLoadMore in + guard let strongSelf = self else { + return + } + strongSelf.isLoadingMore = false + if let (cachedPeers, canLoadMore) = cachedPeersAndCanLoadMore { + strongSelf.results = cachedPeers + strongSelf.hasLoadedOnce = true + strongSelf.canLoadMore = canLoadMore + } + strongSelf.loadMore() + })) + } + + deinit { + self.disposable.dispose() + } + + func loadMore() { + if self.isLoadingMore { + return + } + self.isLoadingMore = true + let pollId = self.pollId + let messageId = self.messageId + let opaqueIdentifier = self.opaqueIdentifier + let account = self.account + let nextOffset = self.nextOffset + let populateCache = self.populateCache + self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal<([RenderedPeer], Int, String?), NoError> in + if let inputPeer = inputPeer { + let signal = account.network.request(Api.functions.messages.getPollVotes(flags: 1 << 0, peer: inputPeer, id: messageId.id, option: Buffer(data: opaqueIdentifier), offset: nextOffset, limit: nextOffset == nil ? 15 : 50)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<([RenderedPeer], Int, String?), NoError> in + return account.postbox.transaction { transaction -> ([RenderedPeer], Int, String?) in + guard let result = result else { + return ([], 0, nil) + } + switch result { + case let .votesList(_, count, votes, users, nextOffset): + var peers: [Peer] = [] + for apiUser in users { + peers.append(TelegramUser(user: apiUser)) + } + updatePeers(transaction: transaction, peers: peers, update: { _, updated in + return updated + }) + var resultPeers: [RenderedPeer] = [] + for vote in votes { + let peerId: PeerId + switch vote { + case let .messageUserVote(userId, _, _): + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + case let .messageUserVoteInputOption(userId, _): + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + case let .messageUserVoteMultiple(userId, _, _): + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + } + if let peer = transaction.getPeer(peerId) { + resultPeers.append(RenderedPeer(peer: peer)) + } + } + if populateCache { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPollResults, key: CachedPollOptionResult.key(pollId: pollId, optionOpaqueIdentifier: opaqueIdentifier)), entry: CachedPollOptionResult(peerIds: resultPeers.map { $0.peerId }, count: count), collectionSpec: cachedPollResultsCollectionSpec) + } + return (resultPeers, Int(count), nextOffset) + } + } + } + #if DEBUG + //return signal |> delay(4.0, queue: .concurrentDefaultQueue()) + #endif + return signal + } else { + return .single(([], 0, nil)) + } + } + |> deliverOn(self.queue)).start(next: { [weak self] peers, updatedCount, nextOffset in + guard let strongSelf = self else { + return + } + if strongSelf.populateCache { + strongSelf.populateCache = false + strongSelf.results.removeAll() + } + var existingIds = Set(strongSelf.results.map { $0.peerId }) + for peer in peers { + if !existingIds.contains(peer.peerId) { + strongSelf.results.append(peer) + existingIds.insert(peer.peerId) + } + } + strongSelf.isLoadingMore = false + strongSelf.hasLoadedOnce = true + strongSelf.canLoadMore = nextOffset != nil + strongSelf.nextOffset = nextOffset + if strongSelf.canLoadMore { + strongSelf.count = max(updatedCount, strongSelf.results.count) + } else { + strongSelf.count = strongSelf.results.count + } + strongSelf.updateState() + })) + self.updateState() + } + + func updateState() { + self.state.set(.single(PollResultsOptionState(peers: self.results, isLoadingMore: self.isLoadingMore, hasLoadedOnce: self.hasLoadedOnce, canLoadMore: self.canLoadMore, count: self.count))) + } +} + +public struct PollResultsOptionState: Equatable { + public var peers: [RenderedPeer] + public var isLoadingMore: Bool + public var hasLoadedOnce: Bool + public var canLoadMore: Bool + public var count: Int +} + +public struct PollResultsState: Equatable { + public var options: [Data: PollResultsOptionState] +} + +private final class PollResultsContextImpl { + private let queue: Queue + + private var optionContexts: [Data: PollResultsOptionContext] = [:] + + let state = Promise() + + init(queue: Queue, account: Account, messageId: MessageId, poll: TelegramMediaPoll) { + self.queue = queue + + for option in poll.options { + var count = 0 + if let voters = poll.results.voters { + for voter in voters { + if voter.opaqueIdentifier == option.opaqueIdentifier { + count = Int(voter.count) + } + } + } + self.optionContexts[option.opaqueIdentifier] = PollResultsOptionContext(queue: self.queue, account: account, pollId: poll.pollId, messageId: messageId, opaqueIdentifier: option.opaqueIdentifier, count: count) + } + + self.state.set(combineLatest(queue: self.queue, self.optionContexts.map { (opaqueIdentifier, context) -> Signal<(Data, PollResultsOptionState), NoError> in + return context.state.get() + |> map { state -> (Data, PollResultsOptionState) in + return (opaqueIdentifier, state) + } + }) + |> map { states -> PollResultsState in + var options: [Data: PollResultsOptionState] = [:] + for (opaqueIdentifier, state) in states { + options[opaqueIdentifier] = state + } + return PollResultsState(options: options) + }) + + for (_, context) in self.optionContexts { + context.loadMore() + } + } + + func loadMore(optionOpaqueIdentifier: Data) { + self.optionContexts[optionOpaqueIdentifier]?.loadMore() + } +} + +public final class PollResultsContext { + private let queue: Queue = Queue() + private let impl: QueueLocalObject + + public var state: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.state.get().start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } + + public init(account: Account, messageId: MessageId, poll: TelegramMediaPoll) { + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return PollResultsContextImpl(queue: queue, account: account, messageId: messageId, poll: poll) + }) + } + + public func loadMore(optionOpaqueIdentifier: Data) { + self.impl.with { impl in + impl.loadMore(optionOpaqueIdentifier: optionOpaqueIdentifier) + } + } +} diff --git a/submodules/TelegramCore/Sources/ProcessRemovedMedia.swift b/submodules/TelegramCore/Sources/ProcessRemovedMedia.swift deleted file mode 100644 index 725c1fb04b..0000000000 --- a/submodules/TelegramCore/Sources/ProcessRemovedMedia.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation -import Postbox - -import SyncCore - -func processRemovedMedia(_ mediaBox: MediaBox, _ media: Media) { - if let image = media as? TelegramMediaImage { - let _ = mediaBox.removeCachedResources(Set(image.representations.map({ WrappedMediaResourceId($0.resource.id) }))).start() - } else if let file = media as? TelegramMediaFile { - let _ = mediaBox.removeCachedResources(Set(file.previewRepresentations.map({ WrappedMediaResourceId($0.resource.id) }))).start() - let _ = mediaBox.removeCachedResources(Set([WrappedMediaResourceId(file.resource.id)])).start() - } -} diff --git a/submodules/TelegramCore/Sources/ProcessSecretChatIncomingDecryptedOperations.swift b/submodules/TelegramCore/Sources/ProcessSecretChatIncomingDecryptedOperations.swift index 65442005c6..6a5b779752 100644 --- a/submodules/TelegramCore/Sources/ProcessSecretChatIncomingDecryptedOperations.swift +++ b/submodules/TelegramCore/Sources/ProcessSecretChatIncomingDecryptedOperations.swift @@ -712,7 +712,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 resources.append((resource, thumb.makeData())) } representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size))) - let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: file.id), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil) + let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: file.id), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) parsedMedia.append(image) } case let .decryptedMessageMediaAudio(duration, mimeType, size, key, iv): @@ -910,7 +910,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 resources.append((resource, thumb.makeData())) } representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size))) - let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: file.id), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil) + let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: file.id), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) parsedMedia.append(image) } case let .decryptedMessageMediaAudio(duration, mimeType, size, key, iv): @@ -1144,7 +1144,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 resources.append((resource, thumb.makeData())) } representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size))) - let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: file.id), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil) + let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: file.id), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) parsedMedia.append(image) } case let .decryptedMessageMediaAudio(duration, mimeType, size, key, iv): diff --git a/submodules/TelegramCore/Sources/ProxySettings.swift b/submodules/TelegramCore/Sources/ProxySettings.swift index 82253e7fb4..933f79dc14 100644 --- a/submodules/TelegramCore/Sources/ProxySettings.swift +++ b/submodules/TelegramCore/Sources/ProxySettings.swift @@ -4,9 +4,9 @@ import SwiftSignalKit import MtProtoKit import SyncCore -public func updateProxySettingsInteractively(accountManager: AccountManager, _ f: @escaping (ProxySettings) -> ProxySettings) -> Signal { - return accountManager.transaction { transaction -> Void in - updateProxySettingsInteractively(transaction: transaction, f) +public func updateProxySettingsInteractively(accountManager: AccountManager, _ f: @escaping (ProxySettings) -> ProxySettings) -> Signal { + return accountManager.transaction { transaction -> Bool in + return updateProxySettingsInteractively(transaction: transaction, f) } } @@ -21,10 +21,13 @@ extension ProxyServerSettings { } } -public func updateProxySettingsInteractively(transaction: AccountManagerModifier, _ f: @escaping (ProxySettings) -> ProxySettings) { +public func updateProxySettingsInteractively(transaction: AccountManagerModifier, _ f: @escaping (ProxySettings) -> ProxySettings) -> Bool { + var hasChanges = false transaction.updateSharedData(SharedDataKeys.proxySettings, { current in let previous = (current as? ProxySettings) ?? ProxySettings.defaultSettings let updated = f(previous) + hasChanges = previous != updated return updated }) + return hasChanges } diff --git a/submodules/TelegramCore/Sources/RateCall.swift b/submodules/TelegramCore/Sources/RateCall.swift index 340236437a..dbc28f7cf7 100644 --- a/submodules/TelegramCore/Sources/RateCall.swift +++ b/submodules/TelegramCore/Sources/RateCall.swift @@ -14,8 +14,8 @@ public func rateCall(account: Account, callId: CallId, starsCount: Int32, commen |> map { _ in } } -public func saveCallDebugLog(account: Account, callId: CallId, log: String) -> Signal { - return account.network.request(Api.functions.phone.saveCallDebug(peer: Api.InputPhoneCall.inputPhoneCall(id: callId.id, accessHash: callId.accessHash), debug: .dataJSON(data: log))) +public func saveCallDebugLog(network: Network, callId: CallId, log: String) -> Signal { + return network.request(Api.functions.phone.saveCallDebug(peer: Api.InputPhoneCall.inputPhoneCall(id: callId.id, accessHash: callId.accessHash), debug: .dataJSON(data: log))) |> retryRequest |> map { _ in } } diff --git a/submodules/TelegramCore/Sources/ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/ReactionsMessageAttribute.swift index 625486d429..5bdfdf62c3 100644 --- a/submodules/TelegramCore/Sources/ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ReactionsMessageAttribute.swift @@ -4,8 +4,8 @@ import TelegramApi import SyncCore -extension ReactionsMessageAttribute { - /*func withUpdatedResults(_ reactions: Api.MessageReactions) -> ReactionsMessageAttribute { +/*extension ReactionsMessageAttribute { + func withUpdatedResults(_ reactions: Api.MessageReactions) -> ReactionsMessageAttribute { switch reactions { case let .messageReactions(flags, results): let min = (flags & (1 << 0)) != 0 @@ -33,8 +33,8 @@ extension ReactionsMessageAttribute { } return ReactionsMessageAttribute(reactions: reactions) } - }*/ -} + } +}*/ public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsMessageAttribute? { var current: ReactionsMessageAttribute? diff --git a/submodules/TelegramCore/Sources/RecentPeers.swift b/submodules/TelegramCore/Sources/RecentPeers.swift index eb44d97c28..fd4d853cb6 100644 --- a/submodules/TelegramCore/Sources/RecentPeers.swift +++ b/submodules/TelegramCore/Sources/RecentPeers.swift @@ -231,3 +231,20 @@ public func recentlyUsedInlineBots(postbox: Postbox) -> Signal<[(Peer, Double)], } } +public func removeRecentlyUsedInlineBot(account: Account, peerId: PeerId) -> Signal { + return account.postbox.transaction { transaction -> Signal in + transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentInlineBots, itemId: RecentPeerItemId(peerId).rawValue) + + if let peer = transaction.getPeer(peerId), let apiPeer = apiInputPeer(peer) { + return account.network.request(Api.functions.contacts.resetTopPeerRating(category: .topPeerCategoryBotsInline, peer: apiPeer)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } else { + return .complete() + } + } |> switchToLatest +} diff --git a/submodules/TelegramCore/Sources/ReplyMarkupMessageAttribute.swift b/submodules/TelegramCore/Sources/ReplyMarkupMessageAttribute.swift index cf364c8429..587ee1be42 100644 --- a/submodules/TelegramCore/Sources/ReplyMarkupMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ReplyMarkupMessageAttribute.swift @@ -30,6 +30,15 @@ extension ReplyMarkupButton { self.init(title: text, titleWhenForwarded: fwdText, action: .urlAuth(url: url, buttonId: buttonId)) case let .inputKeyboardButtonUrlAuth(_, text, fwdText, url, _): self.init(title: text, titleWhenForwarded: fwdText, action: .urlAuth(url: url, buttonId: 0)) + case let .keyboardButtonRequestPoll(_, quiz, text): + var isQuiz: Bool? = quiz.flatMap { quiz in + if case .boolTrue = quiz { + return true + } else { + return false + } + } + self.init(title: text, titleWhenForwarded: nil, action: .setupPoll(isQuiz: isQuiz)) } } } diff --git a/submodules/TelegramCore/Sources/RequestEditMessage.swift b/submodules/TelegramCore/Sources/RequestEditMessage.swift index 94d775b546..0050505c1e 100644 --- a/submodules/TelegramCore/Sources/RequestEditMessage.swift +++ b/submodules/TelegramCore/Sources/RequestEditMessage.swift @@ -27,10 +27,14 @@ public enum RequestEditMessageError { } public func requestEditMessage(account: Account, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute? = nil, disableUrlPreview: Bool = false, scheduleTime: Int32? = nil) -> Signal { - return requestEditMessageInternal(account: account, messageId: messageId, text: text, media: media, entities: entities, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: false) + return requestEditMessage(postbox: account.postbox, network: account.network, stateManager: account.stateManager, transformOutgoingMessageMedia: account.transformOutgoingMessageMedia, messageMediaPreuploadManager: account.messageMediaPreuploadManager, mediaReferenceRevalidationContext: account.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime) +} + +func requestEditMessage(postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, disableUrlPreview: Bool, scheduleTime: Int32?) -> Signal { + return requestEditMessageInternal(postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: false) |> `catch` { error -> Signal in if case .invalidReference = error { - return requestEditMessageInternal(account: account, messageId: messageId, text: text, media: media, entities: entities, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: true) + return requestEditMessageInternal(postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: true) } else { return .fail(error) } @@ -45,34 +49,34 @@ public func requestEditMessage(account: Account, messageId: MessageId, text: Str } } -private func requestEditMessageInternal(account: Account, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, disableUrlPreview: Bool, scheduleTime: Int32?, forceReupload: Bool) -> Signal { +private func requestEditMessageInternal(postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, disableUrlPreview: Bool, scheduleTime: Int32?, forceReupload: Bool) -> Signal { let uploadedMedia: Signal switch media { - case .keep: - uploadedMedia = .single(.progress(0.0)) - |> then(.single(nil)) - case let .update(media): - let generateUploadSignal: (Bool) -> Signal? = { forceReupload in - let augmentedMedia = augmentMediaWithReference(media) - return mediaContentToUpload(network: account.network, postbox: account.postbox, auxiliaryMethods: account.auxiliaryMethods, transformOutgoingMessageMedia: account.transformOutgoingMessageMedia, messageMediaPreuploadManager: account.messageMediaPreuploadManager, revalidationContext: account.mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveAttribute: nil, messageId: nil, attributes: []) - } - if let uploadSignal = generateUploadSignal(forceReupload) { - uploadedMedia = .single(.progress(0.027)) - |> then(uploadSignal) - |> map { result -> PendingMessageUploadedContentResult? in - switch result { - case let .progress(value): - return .progress(max(value, 0.027)) - case let .content(content): - return .content(content) - } + case .keep: + uploadedMedia = .single(.progress(0.0)) + |> then(.single(nil)) + case let .update(media): + let generateUploadSignal: (Bool) -> Signal? = { forceReupload in + let augmentedMedia = augmentMediaWithReference(media) + return mediaContentToUpload(network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveAttribute: nil, messageId: nil, attributes: []) + } + if let uploadSignal = generateUploadSignal(forceReupload) { + uploadedMedia = .single(.progress(0.027)) + |> then(uploadSignal) + |> map { result -> PendingMessageUploadedContentResult? in + switch result { + case let .progress(value): + return .progress(max(value, 0.027)) + case let .content(content): + return .content(content) } - |> `catch` { _ -> Signal in - return .single(nil) - } - } else { - uploadedMedia = .single(nil) } + |> `catch` { _ -> Signal in + return .single(nil) + } + } else { + uploadedMedia = .single(nil) + } } return uploadedMedia |> mapError { _ -> RequestEditMessageInternalError in return .error(.generic) } @@ -86,7 +90,7 @@ private func requestEditMessageInternal(account: Account, messageId: MessageId, pendingMediaContent = content.content } } - return account.postbox.transaction { transaction -> (Peer?, Message?, SimpleDictionary) in + return postbox.transaction { transaction -> (Peer?, Message?, SimpleDictionary) in guard let message = transaction.getMessage(messageId) else { return (nil, nil, SimpleDictionary()) } @@ -155,7 +159,7 @@ private func requestEditMessageInternal(account: Account, messageId: MessageId, flags |= Int32(1 << 15) } - return account.network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: text, media: inputMedia, replyMarkup: nil, entities: apiEntities, scheduleDate: effectiveScheduleTime)) + return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: text, media: inputMedia, replyMarkup: nil, entities: apiEntities, scheduleDate: effectiveScheduleTime)) |> map { result -> Api.Updates? in return result } @@ -176,16 +180,58 @@ private func requestEditMessageInternal(account: Account, messageId: MessageId, } |> mapToSignal { result -> Signal in if let result = result { - return account.postbox.transaction { transaction -> RequestEditMessageResult in + return postbox.transaction { transaction -> RequestEditMessageResult in var toMedia: Media? if let message = result.messages.first.flatMap({ StoreMessage(apiMessage: $0) }) { toMedia = message.media.first } if case let .update(fromMedia) = media, let toMedia = toMedia { - applyMediaResourceChanges(from: fromMedia.media, to: toMedia, postbox: account.postbox) + applyMediaResourceChanges(from: fromMedia.media, to: toMedia, postbox: postbox, force: true) } - account.stateManager.addUpdates(result) + + switch result { + case let .updates(updates, users, chats, _, _): + for update in updates { + switch update { + case .updateEditMessage(let message, _, _), .updateNewMessage(let message, _, _), .updateEditChannelMessage(let message, _, _), .updateNewChannelMessage(let message, _, _): + var peers: [Peer] = [] + for chat in chats { + if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { + peers.append(groupOrChannel) + } + } + for user in users { + let telegramUser = TelegramUser(user: user) + peers.append(telegramUser) + } + + updatePeers(transaction: transaction, peers: peers, update: { _, updated in updated }) + + if let message = StoreMessage(apiMessage: message), case let .Id(id) = message.id { + transaction.updateMessage(id, update: { previousMessage in + var updatedFlags = message.flags + var updatedLocalTags = message.localTags + if previousMessage.localTags.contains(.OutgoingLiveLocation) { + updatedLocalTags.insert(.OutgoingLiveLocation) + } + if previousMessage.flags.contains(.Incoming) { + updatedFlags.insert(.Incoming) + } else { + updatedFlags.remove(.Incoming) + } + return .update(message.withUpdatedLocalTags(updatedLocalTags).withUpdatedFlags(updatedFlags)) + }) + } + default: + break + } + } + default: + break + } + + stateManager.addUpdates(result) return .done(true) } diff --git a/submodules/TelegramCore/Sources/ScheduledMessages.swift b/submodules/TelegramCore/Sources/ScheduledMessages.swift index d40720f44c..f8500bb722 100644 --- a/submodules/TelegramCore/Sources/ScheduledMessages.swift +++ b/submodules/TelegramCore/Sources/ScheduledMessages.swift @@ -104,9 +104,13 @@ func managedApplyPendingScheduledMessagesActions(postbox: Postbox, network: Netw }) |> then( postbox.transaction { transaction -> Void in + var resourceIds: [WrappedMediaResourceId] = [] transaction.deleteMessages([entry.id], forEachMedia: { media in - processRemovedMedia(postbox.mediaBox, media) + addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) }) + if !resourceIds.isEmpty { + let _ = postbox.mediaBox.removeCachedResources(Set(resourceIds)).start() + } } |> ignoreValues ) diff --git a/submodules/TelegramCore/Sources/SearchStickers.swift b/submodules/TelegramCore/Sources/SearchStickers.swift index c17bc64c94..a0cfcf01d5 100644 --- a/submodules/TelegramCore/Sources/SearchStickers.swift +++ b/submodules/TelegramCore/Sources/SearchStickers.swift @@ -222,7 +222,7 @@ public func searchStickers(account: Account, query: String, scope: SearchSticker } public struct FoundStickerSets { - public let infos: [(ItemCollectionId, ItemCollectionInfo, ItemCollectionItem?, Bool)] + public var infos: [(ItemCollectionId, ItemCollectionInfo, ItemCollectionItem?, Bool)] public let entries: [ItemCollectionViewEntry] public init(infos: [(ItemCollectionId, ItemCollectionInfo, ItemCollectionItem?, Bool)] = [], entries: [ItemCollectionViewEntry] = []) { self.infos = infos diff --git a/submodules/TelegramCore/Sources/Serialization.swift b/submodules/TelegramCore/Sources/Serialization.swift index 9d1c029feb..15b20dd3d8 100644 --- a/submodules/TelegramCore/Sources/Serialization.swift +++ b/submodules/TelegramCore/Sources/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 106 + return 110 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/SlowMode.swift b/submodules/TelegramCore/Sources/SlowMode.swift index 4bb00f44e5..7134b47d08 100644 --- a/submodules/TelegramCore/Sources/SlowMode.swift +++ b/submodules/TelegramCore/Sources/SlowMode.swift @@ -6,6 +6,7 @@ import SyncCore public enum UpdateChannelSlowModeError { case generic + case tooManyChannels } public func updateChannelSlowModeInteractively(postbox: Postbox, network: Network, accountStateManager: AccountStateManager, peerId: PeerId, timeout: Int32?) -> Signal { diff --git a/submodules/TelegramCore/Sources/StandaloneUploadedMedia.swift b/submodules/TelegramCore/Sources/StandaloneUploadedMedia.swift index 05a73c353d..03837a49a5 100644 --- a/submodules/TelegramCore/Sources/StandaloneUploadedMedia.swift +++ b/submodules/TelegramCore/Sources/StandaloneUploadedMedia.swift @@ -102,7 +102,7 @@ public func standaloneUploadedImage(account: Account, peerId: PeerId, text: Stri |> mapToSignal { result -> Signal in switch result { case let .encryptedFile(id, accessHash, size, dcId, _): - return .single(.result(.media(.standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()), representations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: Int32(data.count), datacenterId: Int(dcId), key: key))], immediateThumbnailData: nil, reference: nil, partialReference: nil))))) + return .single(.result(.media(.standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()), representations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: Int32(data.count), datacenterId: Int(dcId), key: key))], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []))))) case .encryptedFileEmpty: return .fail(.generic) } diff --git a/submodules/TelegramCore/Sources/StickerPack.swift b/submodules/TelegramCore/Sources/StickerPack.swift index c653fb9913..2dd169dba3 100644 --- a/submodules/TelegramCore/Sources/StickerPack.swift +++ b/submodules/TelegramCore/Sources/StickerPack.swift @@ -1,8 +1,9 @@ import Foundation import Postbox import TelegramApi - +import SwiftSignalKit import SyncCore +import MtProtoKit func telegramStickerPachThumbnailRepresentationFromApiSize(datacenterId: Int32, size: Api.PhotoSize) -> TelegramMediaImageRepresentation? { switch size { @@ -49,3 +50,54 @@ extension StickerPackCollectionInfo { } } } + +public func stickerPacksAttachedToMedia(account: Account, media: AnyMediaReference) -> Signal<[StickerPackReference], NoError> { + let inputMedia: Api.InputStickeredMedia + let resourceReference: MediaResourceReference + if let imageReference = media.concrete(TelegramMediaImage.self), let reference = imageReference.media.reference, case let .cloud(imageId, accessHash, fileReference) = reference, let representation = largestImageRepresentation(imageReference.media.representations) { + inputMedia = .inputStickeredMediaPhoto(id: Api.InputPhoto.inputPhoto(id: imageId, accessHash: accessHash, fileReference: Buffer(data: fileReference ?? Data()))) + resourceReference = imageReference.resourceReference(representation.resource) + } else if let fileReference = media.concrete(TelegramMediaFile.self), let resource = fileReference.media.resource as? CloudDocumentMediaResource { + inputMedia = .inputStickeredMediaDocument(id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data()))) + resourceReference = fileReference.resourceReference(fileReference.media.resource) + } else { + return .single([]) + } + return account.network.request(Api.functions.messages.getAttachedStickers(media: inputMedia)) + |> `catch` { _ -> Signal<[Api.StickerSetCovered], MTRpcError> in + return revalidateMediaResourceReference(postbox: account.postbox, network: account.network, revalidationContext: account.mediaReferenceRevalidationContext, info: TelegramCloudMediaResourceFetchInfo(reference: resourceReference, preferBackgroundReferenceRevalidation: false, continueInBackground: false), resource: resourceReference.resource) + |> mapError { _ -> MTRpcError in + return MTRpcError(errorCode: 500, errorDescription: "Internal") + } + |> mapToSignal { reference -> Signal<[Api.StickerSetCovered], MTRpcError> in + let inputMedia: Api.InputStickeredMedia + if let resource = reference.updatedResource as? TelegramCloudMediaResourceWithFileReference, let updatedReference = resource.fileReference { + if let imageReference = media.concrete(TelegramMediaImage.self), let reference = imageReference.media.reference, case let .cloud(imageId, accessHash, fileReference) = reference, let representation = largestImageRepresentation(imageReference.media.representations) { + inputMedia = .inputStickeredMediaPhoto(id: Api.InputPhoto.inputPhoto(id: imageId, accessHash: accessHash, fileReference: Buffer(data: updatedReference ?? Data()))) + } else if let fileReference = media.concrete(TelegramMediaFile.self), let resource = fileReference.media.resource as? CloudDocumentMediaResource { + inputMedia = .inputStickeredMediaDocument(id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: updatedReference ?? Data()))) + } else { + return .single([]) + } + return account.network.request(Api.functions.messages.getAttachedStickers(media: inputMedia)) + } else { + return .single([]) + } + } + |> `catch` { _ -> Signal<[Api.StickerSetCovered], MTRpcError> in + return .single([]) + } + } + |> map { result -> [StickerPackReference] in + return result.map { pack in + switch pack { + case let .stickerSetCovered(set, _), let .stickerSetMultiCovered(set, _): + let info = StickerPackCollectionInfo(apiSet: set, namespace: Namespaces.ItemCollection.CloudStickerPacks) + return .id(id: info.id.id, accessHash: info.accessHash) + } + } + } + |> `catch` { _ -> Signal<[StickerPackReference], NoError> in + return .single([]) + } +} diff --git a/submodules/TelegramCore/Sources/StickerPackInteractiveOperations.swift b/submodules/TelegramCore/Sources/StickerPackInteractiveOperations.swift index 28fa1e1a6d..4605f32262 100644 --- a/submodules/TelegramCore/Sources/StickerPackInteractiveOperations.swift +++ b/submodules/TelegramCore/Sources/StickerPackInteractiveOperations.swift @@ -4,7 +4,7 @@ import SwiftSignalKit import SyncCore -public func addStickerPackInteractively(postbox: Postbox, info: StickerPackCollectionInfo, items: [ItemCollectionItem]) -> Signal { +public func addStickerPackInteractively(postbox: Postbox, info: StickerPackCollectionInfo, items: [ItemCollectionItem], positionInList: Int? = nil) -> Signal { return postbox.transaction { transaction -> Void in let namespace: SynchronizeInstalledStickerPacksOperationNamespace? switch info.id.namespace { @@ -23,7 +23,11 @@ public func addStickerPackInteractively(postbox: Postbox, info: StickerPackColle updatedInfos.remove(at: index) updatedInfos.insert(currentInfo, at: 0) } else { - updatedInfos.insert(info, at: 0) + if let positionInList = positionInList, positionInList <= updatedInfos.count { + updatedInfos.insert(info, at: positionInList) + } else { + updatedInfos.insert(info, at: 0) + } transaction.replaceItemCollectionItems(collectionId: info.id, items: items) } transaction.replaceItemCollectionInfos(namespace: info.id.namespace, itemCollectionInfos: updatedInfos.map { ($0.id, $0) }) @@ -36,8 +40,8 @@ public enum RemoveStickerPackOption { case archive } -public func removeStickerPackInteractively(postbox: Postbox, id: ItemCollectionId, option: RemoveStickerPackOption) -> Signal { - return postbox.transaction { transaction -> Void in +public func removeStickerPackInteractively(postbox: Postbox, id: ItemCollectionId, option: RemoveStickerPackOption) -> Signal<(Int, [ItemCollectionItem])?, NoError> { + return postbox.transaction { transaction -> (Int, [ItemCollectionItem])? in let namespace: SynchronizeInstalledStickerPacksOperationNamespace? switch id.namespace { case Namespaces.ItemCollection.CloudStickerPacks: @@ -55,8 +59,14 @@ public func removeStickerPackInteractively(postbox: Postbox, id: ItemCollectionI case .archive: content = .archive([id]) } + let index = transaction.getItemCollectionsInfos(namespace: id.namespace).index(where: { $0.0 == id }) + let items = transaction.getItemCollectionItems(collectionId: id) + addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: namespace, content: content) transaction.removeItemCollection(collectionId: id) + return index.flatMap { ($0, items) } + } else { + return nil } } } diff --git a/submodules/TelegramCore/Sources/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/StoreMessage_Telegram.swift index c4f24f6dec..44751ce910 100644 --- a/submodules/TelegramCore/Sources/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/StoreMessage_Telegram.swift @@ -328,7 +328,19 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI case let .messageMediaPoll(poll, results): switch poll { case let .poll(id, flags, question, answers): - return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0), nil) + let publicity: TelegramMediaPollPublicity + if (flags & (1 << 1)) != 0 { + publicity = .public + } else { + publicity = .anonymous + } + let kind: TelegramMediaPollKind + if (flags & (1 << 3)) != 0 { + kind = .quiz + } else { + kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0) + } + return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0), nil) } } } @@ -374,6 +386,8 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Strikethrough)) case let .messageEntityBlockquote(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .BlockQuote)) + case let .messageEntityBankCard(offset, length): + result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .BankCard)) } } return result diff --git a/submodules/TelegramCore/Sources/TelegramChannel.swift b/submodules/TelegramCore/Sources/TelegramChannel.swift index 89702678e0..e3c12f4686 100644 --- a/submodules/TelegramCore/Sources/TelegramChannel.swift +++ b/submodules/TelegramCore/Sources/TelegramChannel.swift @@ -28,7 +28,7 @@ public extension TelegramChannel { return false } } else { - if let adminRights = self.adminRights, adminRights.flags.contains(.canPostMessages) { + if let _ = self.adminRights { return true } if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendMessages) { diff --git a/submodules/TelegramCore/Sources/TelegramMediaImage.swift b/submodules/TelegramCore/Sources/TelegramMediaImage.swift index db04708bb2..3ec9628b2a 100644 --- a/submodules/TelegramCore/Sources/TelegramMediaImage.swift +++ b/submodules/TelegramCore/Sources/TelegramMediaImage.swift @@ -32,9 +32,14 @@ func telegramMediaImageRepresentationsFromApiSizes(datacenterId: Int32, photoId: func telegramMediaImageFromApiPhoto(_ photo: Api.Photo) -> TelegramMediaImage? { switch photo { - case let .photo(_, id, accessHash, fileReference, _, sizes, dcId): + case let .photo(flags, id, accessHash, fileReference, _, sizes, dcId): let (immediateThumbnailData, representations) = telegramMediaImageRepresentationsFromApiSizes(datacenterId: dcId, photoId: id, accessHash: accessHash, fileReference: fileReference.makeData(), sizes: sizes) - return TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudImage, id: id), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: .cloud(imageId: id, accessHash: accessHash, fileReference: fileReference.makeData()), partialReference: nil) + var imageFlags: TelegramMediaImageFlags = [] + let hasStickers = (flags & (1 << 0)) != 0 + if hasStickers { + imageFlags.insert(.hasStickers) + } + return TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudImage, id: id), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: .cloud(imageId: id, accessHash: accessHash, fileReference: fileReference.makeData()), partialReference: nil, flags: imageFlags) case .photoEmpty: return nil } diff --git a/submodules/TelegramCore/Sources/TelegramMediaPoll.swift b/submodules/TelegramCore/Sources/TelegramMediaPoll.swift index ab4e9fc689..0a80671435 100644 --- a/submodules/TelegramCore/Sources/TelegramMediaPoll.swift +++ b/submodules/TelegramCore/Sources/TelegramMediaPoll.swift @@ -21,7 +21,7 @@ extension TelegramMediaPollOptionVoters { init(apiVoters: Api.PollAnswerVoters) { switch apiVoters { case let .pollAnswerVoters(flags, option, voters): - self.init(selected: (flags & (1 << 0)) != 0, opaqueIdentifier: option.makeData(), count: voters) + self.init(selected: (flags & (1 << 0)) != 0, opaqueIdentifier: option.makeData(), count: voters, isCorrect: (flags & (1 << 1)) != 0) } } } @@ -29,8 +29,10 @@ extension TelegramMediaPollOptionVoters { extension TelegramMediaPollResults { init(apiResults: Api.PollResults) { switch apiResults { - case let .pollResults(_, results, totalVoters): - self.init(voters: results.flatMap({ $0.map(TelegramMediaPollOptionVoters.init(apiVoters:)) }), totalVoters: totalVoters) + case let .pollResults(_, results, totalVoters, recentVoters): + self.init(voters: results.flatMap({ $0.map(TelegramMediaPollOptionVoters.init(apiVoters:)) }), totalVoters: totalVoters, recentVoters: recentVoters.flatMap { recentVoters in + return recentVoters.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: $0) } + } ?? []) } } } diff --git a/submodules/TelegramCore/Sources/TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/TelegramMediaWebpage.swift index 371b2dde24..3c50ba95b5 100644 --- a/submodules/TelegramCore/Sources/TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/TelegramMediaWebpage.swift @@ -4,13 +4,24 @@ import TelegramApi import SyncCore +func telegramMediaWebpageAttributeFromApiWebpageAttribute(_ attribute: Api.WebPageAttribute) -> TelegramMediaWebpageAttribute? { + switch attribute { + case let .webPageAttributeTheme(flags, documents, settings): + var files: [TelegramMediaFile] = [] + if let documents = documents { + files = documents.compactMap { telegramMediaFileFromApiDocument($0) } + } + return .theme(TelegraMediaWebpageThemeAttribute(files: files, settings: settings.flatMap { TelegramThemeSettings(apiThemeSettings: $0) })) + } +} + func telegramMediaWebpageFromApiWebpage(_ webpage: Api.WebPage, url: String?) -> TelegramMediaWebpage? { switch webpage { case .webPageNotModified: return nil case let .webPagePending(id, date): return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Pending(date, url)) - case let .webPage(_, id, url, displayUrl, hash, type, siteName, title, description, photo, embedUrl, embedType, embedWidth, embedHeight, duration, author, document, documents, cachedPage): + case let .webPage(_, id, url, displayUrl, hash, type, siteName, title, description, photo, embedUrl, embedType, embedWidth, embedHeight, duration, author, document, cachedPage, attributes): var embedSize: PixelDimensions? if let embedWidth = embedWidth, let embedHeight = embedHeight { embedSize = PixelDimensions(width: embedWidth, height: embedHeight) @@ -27,15 +38,15 @@ func telegramMediaWebpageFromApiWebpage(_ webpage: Api.WebPage, url: String?) -> if let document = document { file = telegramMediaFileFromApiDocument(document) } - var files: [TelegramMediaFile]? - if let documents = documents { - files = documents.compactMap(telegramMediaFileFromApiDocument) + var webpageAttributes: [TelegramMediaWebpageAttribute] = [] + if let attributes = attributes { + webpageAttributes = attributes.compactMap(telegramMediaWebpageAttributeFromApiWebpageAttribute) } var instantPage: InstantPage? if let cachedPage = cachedPage { instantPage = InstantPage(apiPage: cachedPage) } - return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Loaded(TelegramMediaWebpageLoadedContent(url: url, displayUrl: displayUrl, hash: hash, type: type, websiteName: siteName, title: title, text: description, embedUrl: embedUrl, embedType: embedType, embedSize: embedSize, duration: webpageDuration, author: author, image: image, file: file, files: files, instantPage: instantPage))) + return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Loaded(TelegramMediaWebpageLoadedContent(url: url, displayUrl: displayUrl, hash: hash, type: type, websiteName: siteName, title: title, text: description, embedUrl: embedUrl, embedType: embedType, embedSize: embedSize, duration: webpageDuration, author: author, image: image, file: file, attributes: webpageAttributes, instantPage: instantPage))) case .webPageEmpty: return nil } diff --git a/submodules/TelegramCore/Sources/TextEntitiesMessageAttribute.swift b/submodules/TelegramCore/Sources/TextEntitiesMessageAttribute.swift index c898bed30f..1702002184 100644 --- a/submodules/TelegramCore/Sources/TextEntitiesMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/TextEntitiesMessageAttribute.swift @@ -45,6 +45,8 @@ func apiEntitiesFromMessageTextEntities(_ entities: [MessageTextEntity], associa apiEntities.append(.messageEntityBlockquote(offset: offset, length: length)) case .Underline: apiEntities.append(.messageEntityUnderline(offset: offset, length: length)) + case .BankCard: + apiEntities.append(.messageEntityBankCard(offset: offset, length: length)) case .Custom: break } diff --git a/submodules/TelegramCore/Sources/Theme.swift b/submodules/TelegramCore/Sources/Theme.swift index 43e65aee1e..7a421768a8 100644 --- a/submodules/TelegramCore/Sources/Theme.swift +++ b/submodules/TelegramCore/Sources/Theme.swift @@ -8,10 +8,81 @@ import SyncCore extension TelegramTheme { convenience init?(apiTheme: Api.Theme) { switch apiTheme { - case let .theme(flags, id, accessHash, slug, title, document, installCount): - self.init(id: id, accessHash: accessHash, slug: slug, title: title, file: document.flatMap(telegramMediaFileFromApiDocument), isCreator: (flags & 1 << 0) != 0, isDefault: (flags & 1 << 1) != 0, installCount: installCount) + case let .theme(flags, id, accessHash, slug, title, document, settings, installCount): + self.init(id: id, accessHash: accessHash, slug: slug, title: title, file: document.flatMap(telegramMediaFileFromApiDocument), settings: settings.flatMap(TelegramThemeSettings.init(apiThemeSettings:)), isCreator: (flags & 1 << 0) != 0, isDefault: (flags & 1 << 1) != 0, installCount: installCount) default: return nil } } } + +extension TelegramBaseTheme { + init(apiBaseTheme: Api.BaseTheme) { + switch apiBaseTheme { + case .baseThemeClassic: + self = .classic + case .baseThemeDay: + self = .day + case .baseThemeNight: + self = .night + case .baseThemeTinted: + self = .tinted + case .baseThemeArctic: + self = .day + } + } + + var apiBaseTheme: Api.BaseTheme { + switch self { + case .classic: + return .baseThemeClassic + case .day: + return .baseThemeDay + case .night: + return .baseThemeNight + case .tinted: + return .baseThemeTinted + } + } +} + +extension TelegramThemeSettings { + convenience init?(apiThemeSettings: Api.ThemeSettings) { + switch apiThemeSettings { + case let .themeSettings(flags, baseTheme, accentColor, messageTopColor, messageBottomColor, wallpaper): + var messageColors: (UInt32, UInt32)? + if let messageTopColor = messageTopColor, let messageBottomColor = messageBottomColor { + messageColors = (UInt32(bitPattern: messageTopColor), UInt32(bitPattern: messageBottomColor)) + } + self.init(baseTheme: TelegramBaseTheme(apiBaseTheme: baseTheme) ?? .classic, accentColor: UInt32(bitPattern: accentColor), messageColors: messageColors, wallpaper: wallpaper.flatMap(TelegramWallpaper.init(apiWallpaper:))) + default: + return nil + } + } + + var apiInputThemeSettings: Api.InputThemeSettings { + var flags: Int32 = 0 + if let _ = self.messageColors { + flags |= 1 << 0 + } + + var inputWallpaper: Api.InputWallPaper? + var inputWallpaperSettings: Api.WallPaperSettings? + if let wallpaper = self.wallpaper, let inputWallpaperAndSettings = wallpaper.apiInputWallpaperAndSettings { + inputWallpaper = inputWallpaperAndSettings.0 + inputWallpaperSettings = inputWallpaperAndSettings.1 + flags |= 1 << 1 + } + + var messageTopColor: Int32? + var messageBottomColor: Int32? + if let color = self.messageColors?.0 { + messageTopColor = Int32(bitPattern: color) + } + if let color = self.messageColors?.1 { + messageBottomColor = Int32(bitPattern: color) + } + + return .inputThemeSettings(flags: flags, baseTheme: self.baseTheme.apiBaseTheme, accentColor: Int32(bitPattern: self.accentColor), messageTopColor: messageTopColor, messageBottomColor: messageBottomColor, wallpaper: inputWallpaper, wallpaperSettings: inputWallpaperSettings) + } +} diff --git a/submodules/TelegramCore/Sources/Themes.swift b/submodules/TelegramCore/Sources/Themes.swift index 8082a5b0ca..5916c8308f 100644 --- a/submodules/TelegramCore/Sources/Themes.swift +++ b/submodules/TelegramCore/Sources/Themes.swift @@ -13,7 +13,7 @@ let telegramThemeFormat = "ios" let telegramThemeFileExtension = "tgios-theme" #endif -public func telegramThemes(postbox: Postbox, network: Network, accountManager: AccountManager, forceUpdate: Bool = false) -> Signal<[TelegramTheme], NoError> { +public func telegramThemes(postbox: Postbox, network: Network, accountManager: AccountManager?, forceUpdate: Bool = false) -> Signal<[TelegramTheme], NoError> { let fetch: ([TelegramTheme]?, Int32?) -> Signal<[TelegramTheme], NoError> = { current, hash in network.request(Api.functions.account.getThemes(format: telegramThemeFormat, hash: hash ?? 0)) |> retryRequest @@ -31,18 +31,20 @@ public func telegramThemes(postbox: Postbox, network: Network, accountManager: A } } |> mapToSignal { items, hash -> Signal<[TelegramTheme], NoError> in - let _ = accountManager.transaction { transaction in - transaction.updateSharedData(SharedDataKeys.themeSettings, { current in - var updated = current as? ThemeSettings ?? ThemeSettings(currentTheme: nil) - for theme in items { - if theme.id == updated.currentTheme?.id { - updated = ThemeSettings(currentTheme: theme) - break + if let accountManager = accountManager { + let _ = accountManager.transaction { transaction in + transaction.updateSharedData(SharedDataKeys.themeSettings, { current in + var updated = current as? ThemeSettings ?? ThemeSettings(currentTheme: nil) + for theme in items { + if theme.id == updated.currentTheme?.id { + updated = ThemeSettings(currentTheme: theme) + break + } } - } - return updated - }) - }.start() + return updated + }) + }.start() + } return postbox.transaction { transaction -> [TelegramTheme] in var entries: [OrderedItemListEntry] = [] @@ -114,10 +116,11 @@ public enum ThemeUpdatedResult { } private func checkThemeUpdated(network: Network, theme: TelegramTheme) -> Signal { - guard let file = theme.file, let fileId = file.id?.id else { + let id = theme.settings != nil ? 0 : theme.file?.id?.id + guard let documentId = id else { return .fail(.generic) } - return network.request(Api.functions.account.getTheme(format: telegramThemeFormat, theme: .inputTheme(id: theme.id, accessHash: theme.accessHash), documentId: fileId)) + return network.request(Api.functions.account.getTheme(format: telegramThemeFormat, theme: .inputTheme(id: theme.id, accessHash: theme.accessHash), documentId: documentId)) |> mapError { _ -> GetThemeError in return .generic } |> map { theme -> ThemeUpdatedResult in if let theme = TelegramTheme(apiTheme: theme) { @@ -279,53 +282,96 @@ public enum CreateThemeResult { case progress(Float) } -public func createTheme(account: Account, title: String, resource: MediaResource, thumbnailData: Data? = nil) -> Signal { - return uploadTheme(account: account, resource: resource, thumbnailData: thumbnailData) - |> mapError { _ in return CreateThemeError.generic } - |> mapToSignal { result -> Signal in - switch result { - case let .complete(file): - if let resource = file.resource as? CloudDocumentMediaResource { - return account.network.request(Api.functions.account.createTheme(slug: "", title: title, document: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)))) - |> mapError { error in - if error.errorDescription == "THEME_SLUG_INVALID" { - return .slugInvalid - } else if error.errorDescription == "THEME_SLUG_OCCUPIED" { - return .slugOccupied - } - return .generic - } - |> mapToSignal { apiTheme -> Signal in - if let theme = TelegramTheme(apiTheme: apiTheme) { - return account.postbox.transaction { transaction -> CreateThemeResult in - let entries = transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudThemes) - var items = entries.map { $0.contents as! TelegramTheme } - items.insert(theme, at: 0) - var updatedEntries: [OrderedItemListEntry] = [] - for item in items { - var intValue = Int32(updatedEntries.count) - let id = MemoryBuffer(data: Data(bytes: &intValue, count: 4)) - updatedEntries.append(OrderedItemListEntry(id: id, contents: item)) - } - transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.CloudThemes, items: updatedEntries) - return .result(theme) +public func createTheme(account: Account, title: String, resource: MediaResource? = nil, thumbnailData: Data? = nil, settings: TelegramThemeSettings?) -> Signal { + var flags: Int32 = 0 + + var inputSettings: Api.InputThemeSettings? + if let _ = resource { + flags |= 1 << 2 + } + if let settings = settings { + flags |= 1 << 3 + inputSettings = settings.apiInputThemeSettings + } + + if let resource = resource { + return uploadTheme(account: account, resource: resource, thumbnailData: thumbnailData) + |> mapError { _ in return CreateThemeError.generic } + |> mapToSignal { result -> Signal in + switch result { + case let .complete(file): + if let resource = file.resource as? CloudDocumentMediaResource { + return account.network.request(Api.functions.account.createTheme(flags: flags, slug: "", title: title, document: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), settings: inputSettings)) + |> mapError { error in + if error.errorDescription == "THEME_SLUG_INVALID" { + return .slugInvalid + } else if error.errorDescription == "THEME_SLUG_OCCUPIED" { + return .slugOccupied + } + return .generic + } + |> mapToSignal { apiTheme -> Signal in + if let theme = TelegramTheme(apiTheme: apiTheme) { + return account.postbox.transaction { transaction -> CreateThemeResult in + let entries = transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudThemes) + var items = entries.map { $0.contents as! TelegramTheme } + items.insert(theme, at: 0) + var updatedEntries: [OrderedItemListEntry] = [] + for item in items { + var intValue = Int32(updatedEntries.count) + let id = MemoryBuffer(data: Data(bytes: &intValue, count: 4)) + updatedEntries.append(OrderedItemListEntry(id: id, contents: item)) + } + transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.CloudThemes, items: updatedEntries) + return .result(theme) + } + |> castError(CreateThemeError.self) + } else { + return .fail(.generic) } - |> castError(CreateThemeError.self) - } else { - return .fail(.generic) } } + else { + return .fail(.generic) + } + case let .progress(progress): + return .single(.progress(progress)) + } + } + } else { + return account.network.request(Api.functions.account.createTheme(flags: flags, slug: "", title: title, document: .inputDocumentEmpty, settings: inputSettings)) + |> mapError { error in + if error.errorDescription == "THEME_SLUG_INVALID" { + return .slugInvalid + } else if error.errorDescription == "THEME_SLUG_OCCUPIED" { + return .slugOccupied } - else { + return .generic + } + |> mapToSignal { apiTheme -> Signal in + if let theme = TelegramTheme(apiTheme: apiTheme) { + return account.postbox.transaction { transaction -> CreateThemeResult in + let entries = transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudThemes) + var items = entries.map { $0.contents as! TelegramTheme } + items.insert(theme, at: 0) + var updatedEntries: [OrderedItemListEntry] = [] + for item in items { + var intValue = Int32(updatedEntries.count) + let id = MemoryBuffer(data: Data(bytes: &intValue, count: 4)) + updatedEntries.append(OrderedItemListEntry(id: id, contents: item)) + } + transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.CloudThemes, items: updatedEntries) + return .result(theme) + } + |> castError(CreateThemeError.self) + } else { return .fail(.generic) } - case let .progress(progress): - return .single(.progress(progress)) } } } -public func updateTheme(account: Account, accountManager: AccountManager, theme: TelegramTheme, title: String?, slug: String?, resource: MediaResource?, thumbnailData: Data? = nil) -> Signal { +public func updateTheme(account: Account, accountManager: AccountManager, theme: TelegramTheme, title: String?, slug: String?, resource: MediaResource?, thumbnailData: Data? = nil, settings: TelegramThemeSettings?) -> Signal { guard title != nil || slug != nil || resource != nil else { return .complete() } @@ -339,6 +385,11 @@ public func updateTheme(account: Account, accountManager: AccountManager, theme: if let _ = resource { flags |= 1 << 2 } + var inputSettings: Api.InputThemeSettings? + if let settings = settings { + flags |= 1 << 3 + inputSettings = settings.apiInputThemeSettings + } let uploadSignal: Signal if let resource = resource { uploadSignal = uploadTheme(account: account, resource: resource, thumbnailData: thumbnailData) @@ -367,7 +418,7 @@ public func updateTheme(account: Account, accountManager: AccountManager, theme: inputDocument = nil } - return account.network.request(Api.functions.account.updateTheme(flags: flags, format: telegramThemeFormat, theme: .inputTheme(id: theme.id, accessHash: theme.accessHash), slug: slug, title: title, document: inputDocument)) + return account.network.request(Api.functions.account.updateTheme(flags: flags, format: telegramThemeFormat, theme: .inputTheme(id: theme.id, accessHash: theme.accessHash), slug: slug, title: title, document: inputDocument, settings: inputSettings)) |> mapError { error in if error.errorDescription == "THEME_SLUG_INVALID" { return .slugInvalid @@ -499,6 +550,9 @@ private func areThemesEqual(_ lhs: TelegramTheme, _ rhs: TelegramTheme) -> Bool if lhs.file?.id != rhs.file?.id { return false } + if lhs.settings != rhs.settings { + return false + } return true } diff --git a/submodules/TelegramCore/Sources/UnauthorizedAccountStateManager.swift b/submodules/TelegramCore/Sources/UnauthorizedAccountStateManager.swift new file mode 100644 index 0000000000..19a87d91f3 --- /dev/null +++ b/submodules/TelegramCore/Sources/UnauthorizedAccountStateManager.swift @@ -0,0 +1,86 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit +import SyncCore + +private final class UnauthorizedUpdateMessageService: NSObject, MTMessageService { + let pipe: ValuePipe<[Api.Update]> = ValuePipe() + var mtProto: MTProto? + + override init() { + super.init() + } + + func mtProtoWillAdd(_ mtProto: MTProto!) { + self.mtProto = mtProto + } + + func mtProtoDidChangeSession(_ mtProto: MTProto!) { + } + + func mtProtoServerDidChangeSession(_ mtProto: MTProto!, firstValidMessageId: Int64, otherValidMessageIds: [Any]!) { + } + + func putNext(_ updates: [Api.Update]) { + self.pipe.putNext(updates) + } + + func mtProto(_ mtProto: MTProto!, receivedMessage message: MTIncomingMessage!) { + if let updates = (message.body as? BoxedMessage)?.body as? Api.Updates { + self.addUpdates(updates) + } + } + + func addUpdates(_ updates: Api.Updates) { + switch updates { + case let .updates(updates, _, _, _, _): + self.putNext(updates) + case let .updatesCombined(updates, _, _, _, _, _): + self.putNext(updates) + case let .updateShort(update, _): + self.putNext([update]) + case .updateShortChatMessage, .updateShortMessage, .updatesTooLong, .updateShortSentMessage: + break + } + } +} + + +final class UnauthorizedAccountStateManager { + private let queue = Queue() + private let network: Network + private var updateService: UnauthorizedUpdateMessageService? + private let updateServiceDisposable = MetaDisposable() + private let updateLoginToken: () -> Void + + init(network: Network, updateLoginToken: @escaping () -> Void) { + self.network = network + self.updateLoginToken = updateLoginToken + } + + deinit { + self.updateServiceDisposable.dispose() + } + + func reset() { + self.queue.async { + if self.updateService == nil { + self.updateService = UnauthorizedUpdateMessageService() + let updateLoginToken = self.updateLoginToken + self.updateServiceDisposable.set(self.updateService!.pipe.signal().start(next: { updates in + for update in updates { + switch update { + case .updateLoginToken: + updateLoginToken() + default: + break + } + } + })) + self.network.mtProto.add(self.updateService) + } + } + } +} diff --git a/submodules/TelegramCore/Sources/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/UpdateCachedPeerData.swift index 6ba93d2ea0..d0f10bfb51 100644 --- a/submodules/TelegramCore/Sources/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/UpdateCachedPeerData.swift @@ -2,149 +2,179 @@ import Foundation import Postbox import TelegramApi import SwiftSignalKit - import SyncCore -func fetchAndUpdateSupplementalCachedPeerData(peerId rawPeerId: PeerId, network: Network, postbox: Postbox) -> Signal { - return postbox.transaction { transaction -> Signal in - guard let rawPeer = transaction.getPeer(rawPeerId) else { +func fetchAndUpdateSupplementalCachedPeerData(peerId rawPeerId: PeerId, network: Network, postbox: Postbox) -> Signal { + return postbox.combinedView(keys: [.basicPeer(rawPeerId)]) + |> mapToSignal { views -> Signal in + guard let view = views.views[.basicPeer(rawPeerId)] as? BasicPeerView else { return .complete() } - - let peer: Peer - if let secretChat = rawPeer as? TelegramSecretChat { - guard let user = transaction.getPeer(secretChat.regularPeerId) else { - return .complete() - } - peer = user - } else { - peer = rawPeer + guard let peer = view.peer else { + return .complete() } + return .single(peer) + } + |> take(1) + |> mapToSignal { _ -> Signal in + return postbox.transaction { transaction -> Signal in + guard let rawPeer = transaction.getPeer(rawPeerId) else { + return .single(false) + } - let cachedData = transaction.getPeerCachedData(peerId: peer.id) - - if let cachedData = cachedData as? CachedUserData { - if cachedData.peerStatusSettings != nil { - return .complete() + let peer: Peer + if let secretChat = rawPeer as? TelegramSecretChat { + guard let user = transaction.getPeer(secretChat.regularPeerId) else { + return .single(false) + } + peer = user + } else { + peer = rawPeer } - } else if let cachedData = cachedData as? CachedGroupData { - if cachedData.peerStatusSettings != nil { - return .complete() + + let cachedData = transaction.getPeerCachedData(peerId: peer.id) + + if let cachedData = cachedData as? CachedUserData { + if cachedData.peerStatusSettings != nil { + return .single(true) + } + } else if let cachedData = cachedData as? CachedGroupData { + if cachedData.peerStatusSettings != nil { + return .single(true) + } + } else if let cachedData = cachedData as? CachedChannelData { + if cachedData.peerStatusSettings != nil { + return .single(true) + } + } else if let cachedData = cachedData as? CachedSecretChatData { + if cachedData.peerStatusSettings != nil { + return .single(true) + } } - } else if let cachedData = cachedData as? CachedChannelData { - if cachedData.peerStatusSettings != nil { - return .complete() - } - } else if let cachedData = cachedData as? CachedSecretChatData { - if cachedData.peerStatusSettings != nil { - return .complete() - } - } - - if peer.id.namespace == Namespaces.Peer.SecretChat { - return postbox.transaction { transaction -> Void in - var peerStatusSettings: PeerStatusSettings - if let peer = transaction.getPeer(peer.id), let associatedPeerId = peer.associatedPeerId, !transaction.isPeerContact(peerId: associatedPeerId) { - if let peer = peer as? TelegramSecretChat, case .creator = peer.role { + + if peer.id.namespace == Namespaces.Peer.SecretChat { + return postbox.transaction { transaction -> Bool in + var peerStatusSettings: PeerStatusSettings + if let peer = transaction.getPeer(peer.id), let associatedPeerId = peer.associatedPeerId, !transaction.isPeerContact(peerId: associatedPeerId) { + if let peer = peer as? TelegramSecretChat, case .creator = peer.role { + peerStatusSettings = PeerStatusSettings() + peerStatusSettings = [] + } else { + peerStatusSettings = PeerStatusSettings() + peerStatusSettings.insert(.canReport) + } + } else { peerStatusSettings = PeerStatusSettings() peerStatusSettings = [] - } else { - peerStatusSettings = PeerStatusSettings() - peerStatusSettings.insert(.canReport) } - } else { - peerStatusSettings = PeerStatusSettings() - peerStatusSettings = [] - } - - transaction.updatePeerCachedData(peerIds: [peer.id], update: { peerId, current in - if let current = current as? CachedSecretChatData { - return current.withUpdatedPeerStatusSettings(peerStatusSettings) - } else { - return CachedSecretChatData(peerStatusSettings: peerStatusSettings) - } - }) - } - } else if let inputPeer = apiInputPeer(peer) { - return network.request(Api.functions.messages.getPeerSettings(peer: inputPeer)) - |> retryRequest - |> mapToSignal { peerSettings -> Signal in - let peerStatusSettings = PeerStatusSettings(apiSettings: peerSettings) - - return postbox.transaction { transaction -> Void in - transaction.updatePeerCachedData(peerIds: Set([peer.id]), update: { _, current in - switch peer.id.namespace { - case Namespaces.Peer.CloudUser: - let previous: CachedUserData - if let current = current as? CachedUserData { - previous = current - } else { - previous = CachedUserData() - } - return previous.withUpdatedPeerStatusSettings(peerStatusSettings) - case Namespaces.Peer.CloudGroup: - let previous: CachedGroupData - if let current = current as? CachedGroupData { - previous = current - } else { - previous = CachedGroupData() - } - return previous.withUpdatedPeerStatusSettings(peerStatusSettings) - case Namespaces.Peer.CloudChannel: - let previous: CachedChannelData - if let current = current as? CachedChannelData { - previous = current - } else { - previous = CachedChannelData() - } - return previous.withUpdatedPeerStatusSettings(peerStatusSettings) - default: - break + + transaction.updatePeerCachedData(peerIds: [peer.id], update: { peerId, current in + if let current = current as? CachedSecretChatData { + return current.withUpdatedPeerStatusSettings(peerStatusSettings) + } else { + return CachedSecretChatData(peerStatusSettings: peerStatusSettings) } - return current }) + + return true } + } else if let inputPeer = apiInputPeer(peer) { + return network.request(Api.functions.messages.getPeerSettings(peer: inputPeer)) + |> retryRequest + |> mapToSignal { peerSettings -> Signal in + let peerStatusSettings = PeerStatusSettings(apiSettings: peerSettings) + + return postbox.transaction { transaction -> Bool in + transaction.updatePeerCachedData(peerIds: Set([peer.id]), update: { _, current in + switch peer.id.namespace { + case Namespaces.Peer.CloudUser: + let previous: CachedUserData + if let current = current as? CachedUserData { + previous = current + } else { + previous = CachedUserData() + } + return previous.withUpdatedPeerStatusSettings(peerStatusSettings) + case Namespaces.Peer.CloudGroup: + let previous: CachedGroupData + if let current = current as? CachedGroupData { + previous = current + } else { + previous = CachedGroupData() + } + return previous.withUpdatedPeerStatusSettings(peerStatusSettings) + case Namespaces.Peer.CloudChannel: + let previous: CachedChannelData + if let current = current as? CachedChannelData { + previous = current + } else { + previous = CachedChannelData() + } + return previous.withUpdatedPeerStatusSettings(peerStatusSettings) + default: + break + } + return current + }) + return true + } + } + } else { + return .single(false) } - } else { - return .complete() } + |> switchToLatest } - |> switchToLatest } -func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerId, network: Network, postbox: Postbox) -> Signal { - return postbox.transaction { transaction -> (Api.InputUser?, Peer?, PeerId) in - guard let rawPeer = transaction.getPeer(rawPeerId) else { +func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerId, network: Network, postbox: Postbox) -> Signal { + return postbox.combinedView(keys: [.basicPeer(rawPeerId)]) + |> mapToSignal { views -> Signal in + if accountPeerId == rawPeerId { + return .single(true) + } + guard let view = views.views[.basicPeer(rawPeerId)] as? BasicPeerView else { + return .complete() + } + guard let peer = view.peer else { + return .complete() + } + return .single(true) + } + |> take(1) + |> mapToSignal { _ -> Signal in + return postbox.transaction { transaction -> (Api.InputUser?, Peer?, PeerId) in + guard let rawPeer = transaction.getPeer(rawPeerId) else { + if rawPeerId == accountPeerId { + return (.inputUserSelf, transaction.getPeer(rawPeerId), rawPeerId) + } else { + return (nil, nil, rawPeerId) + } + } + + let peer: Peer + if let secretChat = rawPeer as? TelegramSecretChat { + guard let user = transaction.getPeer(secretChat.regularPeerId) else { + return (nil, nil, rawPeerId) + } + peer = user + } else { + peer = rawPeer + } + if rawPeerId == accountPeerId { return (.inputUserSelf, transaction.getPeer(rawPeerId), rawPeerId) } else { - return (nil, nil, rawPeerId) + return (apiInputUser(peer), peer, peer.id) } } - - let peer: Peer - if let secretChat = rawPeer as? TelegramSecretChat { - guard let user = transaction.getPeer(secretChat.regularPeerId) else { - return (nil, nil, rawPeerId) - } - peer = user - } else { - peer = rawPeer - } - - if rawPeerId == accountPeerId { - return (.inputUserSelf, transaction.getPeer(rawPeerId), rawPeerId) - } else { - return (apiInputUser(peer), peer, peer.id) - } - } - |> mapToSignal { inputUser, maybePeer, peerId -> Signal in - if let inputUser = inputUser { - return network.request(Api.functions.users.getFullUser(id: inputUser)) - |> retryRequest - |> mapToSignal { result -> Signal in - return postbox.transaction { transaction -> Void in - switch result { + |> mapToSignal { inputUser, maybePeer, peerId -> Signal in + if let inputUser = inputUser { + return network.request(Api.functions.users.getFullUser(id: inputUser)) + |> retryRequest + |> mapToSignal { result -> Signal in + return postbox.transaction { transaction -> Bool in + switch result { case let .userFull(userFull): let telegramUser = TelegramUser(user: userFull.user) updatePeers(transaction: transaction, peers: [telegramUser], update: { _, updated -> Peer in @@ -154,292 +184,301 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI if let presence = TelegramUserPresence(apiUser: userFull.user) { updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: [telegramUser.id: presence]) } - } - transaction.updatePeerCachedData(peerIds: [peerId], update: { peerId, current in - let previous: CachedUserData - if let current = current as? CachedUserData { - previous = current - } else { - previous = CachedUserData() } - switch result { - case let .userFull(userFull): - let botInfo = userFull.botInfo.flatMap(BotInfo.init(apiBotInfo:)) - let isBlocked = (userFull.flags & (1 << 0)) != 0 - let callsAvailable = (userFull.flags & (1 << 4)) != 0 - let callsPrivate = (userFull.flags & (1 << 5)) != 0 - let canPinMessages = (userFull.flags & (1 << 7)) != 0 - let pinnedMessageId = userFull.pinnedMsgId.flatMap({ MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }) - - let peerStatusSettings = PeerStatusSettings(apiSettings: userFull.settings) - - var hasScheduledMessages = false - if (userFull.flags & 1 << 12) != 0 { - hasScheduledMessages = true - } - - return previous.withUpdatedAbout(userFull.about).withUpdatedBotInfo(botInfo).withUpdatedCommonGroupCount(userFull.commonChatsCount).withUpdatedIsBlocked(isBlocked).withUpdatedCallsAvailable(callsAvailable).withUpdatedCallsPrivate(callsPrivate).withUpdatedCanPinMessages(canPinMessages).withUpdatedPeerStatusSettings(peerStatusSettings).withUpdatedPinnedMessageId(pinnedMessageId).withUpdatedHasScheduledMessages(hasScheduledMessages) - } - }) - } - } - } else if peerId.namespace == Namespaces.Peer.CloudGroup { - return network.request(Api.functions.messages.getFullChat(chatId: peerId.id)) - |> retryRequest - |> mapToSignal { result -> Signal in - return postbox.transaction { transaction -> Void in - switch result { - case let .chatFull(fullChat, chats, users): - switch fullChat { - case let .chatFull(chatFull): - transaction.updateCurrentPeerNotificationSettings([peerId: TelegramPeerNotificationSettings(apiSettings: chatFull.notifySettings)]) - case .channelFull: - break + transaction.updatePeerCachedData(peerIds: [peerId], update: { peerId, current in + let previous: CachedUserData + if let current = current as? CachedUserData { + previous = current + } else { + previous = CachedUserData() } - - switch fullChat { - case let .chatFull(chatFull): - var botInfos: [CachedPeerBotInfo] = [] - for botInfo in chatFull.botInfo ?? [] { - switch botInfo { - case let .botInfo(userId, _, _): - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) - let parsedBotInfo = BotInfo(apiBotInfo: botInfo) - botInfos.append(CachedPeerBotInfo(peerId: peerId, botInfo: parsedBotInfo)) - } - } - let participants = CachedGroupParticipants(apiParticipants: chatFull.participants) - let exportedInvitation = ExportedInvitation(apiExportedInvite: chatFull.exportedInvite) - let pinnedMessageId = chatFull.pinnedMsgId.flatMap({ MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }) - - var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] - for chat in chats { - if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { - peers.append(groupOrChannel) - } - } - for user in users { - let telegramUser = TelegramUser(user: user) - peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } - } + switch result { + case let .userFull(userFull): + let botInfo = userFull.botInfo.flatMap(BotInfo.init(apiBotInfo:)) + let isBlocked = (userFull.flags & (1 << 0)) != 0 + let callsAvailable = (userFull.flags & (1 << 4)) != 0 + let callsPrivate = (userFull.flags & (1 << 5)) != 0 + let canPinMessages = (userFull.flags & (1 << 7)) != 0 + let pinnedMessageId = userFull.pinnedMsgId.flatMap({ MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }) - updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in - return updated - }) - - updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences) - - var flags = CachedGroupFlags() - if (chatFull.flags & 1 << 7) != 0 { - flags.insert(.canChangeUsername) - } + let peerStatusSettings = PeerStatusSettings(apiSettings: userFull.settings) var hasScheduledMessages = false - if (chatFull.flags & 1 << 8) != 0 { + if (userFull.flags & 1 << 12) != 0 { hasScheduledMessages = true } - - transaction.updatePeerCachedData(peerIds: [peerId], update: { _, current in - let previous: CachedGroupData - if let current = current as? CachedGroupData { - previous = current - } else { - previous = CachedGroupData() - } - - return previous.withUpdatedParticipants(participants) - .withUpdatedExportedInvitation(exportedInvitation) - .withUpdatedBotInfos(botInfos) - .withUpdatedPinnedMessageId(pinnedMessageId) - .withUpdatedAbout(chatFull.about) - .withUpdatedFlags(flags) - .withUpdatedHasScheduledMessages(hasScheduledMessages) - }) - case .channelFull: - break + + return previous.withUpdatedAbout(userFull.about).withUpdatedBotInfo(botInfo).withUpdatedCommonGroupCount(userFull.commonChatsCount).withUpdatedIsBlocked(isBlocked).withUpdatedCallsAvailable(callsAvailable).withUpdatedCallsPrivate(callsPrivate).withUpdatedCanPinMessages(canPinMessages).withUpdatedPeerStatusSettings(peerStatusSettings).withUpdatedPinnedMessageId(pinnedMessageId).withUpdatedHasScheduledMessages(hasScheduledMessages) } + }) + return true } } - } - } else if let inputChannel = maybePeer.flatMap(apiInputChannel) { - return network.request(Api.functions.channels.getFullChannel(channel: inputChannel)) - |> map(Optional.init) - |> `catch` { error -> Signal in - if error.errorDescription == "CHANNEL_PRIVATE" { - return .single(nil) - } - return .complete() - } - |> mapToSignal { result -> Signal in - return postbox.transaction { transaction -> Void in - if let result = result { + } else if peerId.namespace == Namespaces.Peer.CloudGroup { + return network.request(Api.functions.messages.getFullChat(chatId: peerId.id)) + |> retryRequest + |> mapToSignal { result -> Signal in + return postbox.transaction { transaction -> Bool in switch result { - case let .chatFull(fullChat, chats, users): - switch fullChat { - case let .channelFull(channelFull): - transaction.updateCurrentPeerNotificationSettings([peerId: TelegramPeerNotificationSettings(apiSettings: channelFull.notifySettings)]) - case .chatFull: - break + case let .chatFull(fullChat, chats, users): + switch fullChat { + case let .chatFull(chatFull): + transaction.updateCurrentPeerNotificationSettings([peerId: TelegramPeerNotificationSettings(apiSettings: chatFull.notifySettings)]) + case .channelFull: + break + } + + switch fullChat { + case let .chatFull(chatFull): + var botInfos: [CachedPeerBotInfo] = [] + for botInfo in chatFull.botInfo ?? [] { + switch botInfo { + case let .botInfo(userId, _, _): + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + let parsedBotInfo = BotInfo(apiBotInfo: botInfo) + botInfos.append(CachedPeerBotInfo(peerId: peerId, botInfo: parsedBotInfo)) + } + } + let participants = CachedGroupParticipants(apiParticipants: chatFull.participants) + let exportedInvitation = ExportedInvitation(apiExportedInvite: chatFull.exportedInvite) + let pinnedMessageId = chatFull.pinnedMsgId.flatMap({ MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }) + + var peers: [Peer] = [] + var peerPresences: [PeerId: PeerPresence] = [:] + for chat in chats { + if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { + peers.append(groupOrChannel) + } + } + for user in users { + let telegramUser = TelegramUser(user: user) + peers.append(telegramUser) + if let presence = TelegramUserPresence(apiUser: user) { + peerPresences[telegramUser.id] = presence + } } - switch fullChat { - case let .channelFull(flags, _, about, participantsCount, adminsCount, kickedCount, bannedCount, _, _, _, _, _, _, apiExportedInvite, apiBotInfos, migratedFromChatId, migratedFromMaxId, pinnedMsgId, stickerSet, minAvailableMsgId, folderId, linkedChatId, location, slowmodeSeconds, slowmodeNextSendDate, pts): - var channelFlags = CachedChannelFlags() - if (flags & (1 << 3)) != 0 { - channelFlags.insert(.canDisplayParticipants) - } - if (flags & (1 << 6)) != 0 { - channelFlags.insert(.canChangeUsername) - } - if (flags & (1 << 10)) == 0 { - channelFlags.insert(.preHistoryEnabled) - } - if (flags & (1 << 12)) != 0 { - channelFlags.insert(.canViewStats) - } - if (flags & (1 << 7)) != 0 { - channelFlags.insert(.canSetStickerSet) - } - if (flags & (1 << 16)) != 0 { - channelFlags.insert(.canChangePeerGeoLocation) - } - - let linkedDiscussionPeerId: PeerId? - if let linkedChatId = linkedChatId, linkedChatId != 0 { - linkedDiscussionPeerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: linkedChatId) - } else { - linkedDiscussionPeerId = nil - } - - let peerGeoLocation: PeerGeoLocation? - if let location = location { - peerGeoLocation = PeerGeoLocation(apiLocation: location) - } else { - peerGeoLocation = nil - } - - var botInfos: [CachedPeerBotInfo] = [] - for botInfo in apiBotInfos { - switch botInfo { - case let .botInfo(userId, _, _): - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) - let parsedBotInfo = BotInfo(apiBotInfo: botInfo) - botInfos.append(CachedPeerBotInfo(peerId: peerId, botInfo: parsedBotInfo)) - } - } - - var pinnedMessageId: MessageId? - if let pinnedMsgId = pinnedMsgId { - pinnedMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: pinnedMsgId) - } - - var minAvailableMessageId: MessageId? - if let minAvailableMsgId = minAvailableMsgId { - minAvailableMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: minAvailableMsgId) - - if let pinnedMsgId = pinnedMsgId, pinnedMsgId < minAvailableMsgId { - pinnedMessageId = nil - } - } - - var migrationReference: ChannelMigrationReference? - if let migratedFromChatId = migratedFromChatId, let migratedFromMaxId = migratedFromMaxId { - migrationReference = ChannelMigrationReference(maxMessageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudGroup, id: migratedFromChatId), namespace: Namespaces.Message.Cloud, id: migratedFromMaxId)) - } - - var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] - for chat in chats { - if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { - peers.append(groupOrChannel) - } - } - for user in users { - let telegramUser = TelegramUser(user: user) - peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } - } - - updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in - return updated - }) - - updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences) - - let stickerPack: StickerPackCollectionInfo? = stickerSet.flatMap { apiSet -> StickerPackCollectionInfo in - let namespace: ItemCollectionId.Namespace - switch apiSet { - case let .stickerSet(flags, _, _, _, _, _, _, _, _, _): - if (flags & (1 << 3)) != 0 { - namespace = Namespaces.ItemCollection.CloudMaskPacks - } else { - namespace = Namespaces.ItemCollection.CloudStickerPacks - } - } - - return StickerPackCollectionInfo(apiSet: apiSet, namespace: namespace) - } - - var hasScheduledMessages = false - if (flags & (1 << 19)) != 0 { - hasScheduledMessages = true - } - - var minAvailableMessageIdUpdated = false - transaction.updatePeerCachedData(peerIds: [peerId], update: { _, current in - var previous: CachedChannelData - if let current = current as? CachedChannelData { - previous = current - } else { - previous = CachedChannelData() - } - - previous = previous.withUpdatedIsNotAccessible(false) - - minAvailableMessageIdUpdated = previous.minAvailableMessageId != minAvailableMessageId - - return previous.withUpdatedFlags(channelFlags) - .withUpdatedAbout(about) - .withUpdatedParticipantsSummary(CachedChannelParticipantsSummary(memberCount: participantsCount, adminCount: adminsCount, bannedCount: bannedCount, kickedCount: kickedCount)) - .withUpdatedExportedInvitation(ExportedInvitation(apiExportedInvite: apiExportedInvite)) - .withUpdatedBotInfos(botInfos) - .withUpdatedPinnedMessageId(pinnedMessageId) - .withUpdatedStickerPack(stickerPack) - .withUpdatedMinAvailableMessageId(minAvailableMessageId) - .withUpdatedMigrationReference(migrationReference) - .withUpdatedLinkedDiscussionPeerId(linkedDiscussionPeerId) - .withUpdatedPeerGeoLocation(peerGeoLocation) - .withUpdatedSlowModeTimeout(slowmodeSeconds) - .withUpdatedSlowModeValidUntilTimestamp(slowmodeNextSendDate) - .withUpdatedHasScheduledMessages(hasScheduledMessages) - }) - - if let minAvailableMessageId = minAvailableMessageId, minAvailableMessageIdUpdated { - transaction.deleteMessagesInRange(peerId: peerId, namespace: minAvailableMessageId.namespace, minId: 1, maxId: minAvailableMessageId.id, forEachMedia: { media in - processRemovedMedia(postbox.mediaBox, media) - }) - } - case .chatFull: - break + updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in + return updated + }) + + updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences) + + var flags = CachedGroupFlags() + if (chatFull.flags & 1 << 7) != 0 { + flags.insert(.canChangeUsername) } + + var hasScheduledMessages = false + if (chatFull.flags & 1 << 8) != 0 { + hasScheduledMessages = true + } + + transaction.updatePeerCachedData(peerIds: [peerId], update: { _, current in + let previous: CachedGroupData + if let current = current as? CachedGroupData { + previous = current + } else { + previous = CachedGroupData() + } + + return previous.withUpdatedParticipants(participants) + .withUpdatedExportedInvitation(exportedInvitation) + .withUpdatedBotInfos(botInfos) + .withUpdatedPinnedMessageId(pinnedMessageId) + .withUpdatedAbout(chatFull.about) + .withUpdatedFlags(flags) + .withUpdatedHasScheduledMessages(hasScheduledMessages) + }) + case .channelFull: + break + } } - } else { - transaction.updatePeerCachedData(peerIds: [peerId], update: { _, _ in - var updated = CachedChannelData() - updated = updated.withUpdatedIsNotAccessible(true) - return updated - }) + return true } } + } else if let inputChannel = maybePeer.flatMap(apiInputChannel) { + return network.request(Api.functions.channels.getFullChannel(channel: inputChannel)) + |> map(Optional.init) + |> `catch` { error -> Signal in + if error.errorDescription == "CHANNEL_PRIVATE" { + return .single(nil) + } + return .single(nil) + } + |> mapToSignal { result -> Signal in + return postbox.transaction { transaction -> Bool in + if let result = result { + switch result { + case let .chatFull(fullChat, chats, users): + switch fullChat { + case let .channelFull(channelFull): + transaction.updateCurrentPeerNotificationSettings([peerId: TelegramPeerNotificationSettings(apiSettings: channelFull.notifySettings)]) + case .chatFull: + break + } + + switch fullChat { + case let .channelFull(flags, _, about, participantsCount, adminsCount, kickedCount, bannedCount, _, _, _, _, _, _, apiExportedInvite, apiBotInfos, migratedFromChatId, migratedFromMaxId, pinnedMsgId, stickerSet, minAvailableMsgId, folderId, linkedChatId, location, slowmodeSeconds, slowmodeNextSendDate, pts): + var channelFlags = CachedChannelFlags() + if (flags & (1 << 3)) != 0 { + channelFlags.insert(.canDisplayParticipants) + } + if (flags & (1 << 6)) != 0 { + channelFlags.insert(.canChangeUsername) + } + if (flags & (1 << 10)) == 0 { + channelFlags.insert(.preHistoryEnabled) + } + if (flags & (1 << 12)) != 0 { + channelFlags.insert(.canViewStats) + } + if (flags & (1 << 7)) != 0 { + channelFlags.insert(.canSetStickerSet) + } + if (flags & (1 << 16)) != 0 { + channelFlags.insert(.canChangePeerGeoLocation) + } + + let linkedDiscussionPeerId: PeerId? + if let linkedChatId = linkedChatId, linkedChatId != 0 { + linkedDiscussionPeerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: linkedChatId) + } else { + linkedDiscussionPeerId = nil + } + + let peerGeoLocation: PeerGeoLocation? + if let location = location { + peerGeoLocation = PeerGeoLocation(apiLocation: location) + } else { + peerGeoLocation = nil + } + + var botInfos: [CachedPeerBotInfo] = [] + for botInfo in apiBotInfos { + switch botInfo { + case let .botInfo(userId, _, _): + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + let parsedBotInfo = BotInfo(apiBotInfo: botInfo) + botInfos.append(CachedPeerBotInfo(peerId: peerId, botInfo: parsedBotInfo)) + } + } + + var pinnedMessageId: MessageId? + if let pinnedMsgId = pinnedMsgId { + pinnedMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: pinnedMsgId) + } + + var minAvailableMessageId: MessageId? + if let minAvailableMsgId = minAvailableMsgId { + minAvailableMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: minAvailableMsgId) + + if let pinnedMsgId = pinnedMsgId, pinnedMsgId < minAvailableMsgId { + pinnedMessageId = nil + } + } + + var migrationReference: ChannelMigrationReference? + if let migratedFromChatId = migratedFromChatId, let migratedFromMaxId = migratedFromMaxId { + migrationReference = ChannelMigrationReference(maxMessageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudGroup, id: migratedFromChatId), namespace: Namespaces.Message.Cloud, id: migratedFromMaxId)) + } + + var peers: [Peer] = [] + var peerPresences: [PeerId: PeerPresence] = [:] + for chat in chats { + if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { + peers.append(groupOrChannel) + } + } + for user in users { + let telegramUser = TelegramUser(user: user) + peers.append(telegramUser) + if let presence = TelegramUserPresence(apiUser: user) { + peerPresences[telegramUser.id] = presence + } + } + + updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in + return updated + }) + + updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences) + + let stickerPack: StickerPackCollectionInfo? = stickerSet.flatMap { apiSet -> StickerPackCollectionInfo in + let namespace: ItemCollectionId.Namespace + switch apiSet { + case let .stickerSet(flags, _, _, _, _, _, _, _, _, _): + if (flags & (1 << 3)) != 0 { + namespace = Namespaces.ItemCollection.CloudMaskPacks + } else { + namespace = Namespaces.ItemCollection.CloudStickerPacks + } + } + + return StickerPackCollectionInfo(apiSet: apiSet, namespace: namespace) + } + + var hasScheduledMessages = false + if (flags & (1 << 19)) != 0 { + hasScheduledMessages = true + } + + var minAvailableMessageIdUpdated = false + transaction.updatePeerCachedData(peerIds: [peerId], update: { _, current in + var previous: CachedChannelData + if let current = current as? CachedChannelData { + previous = current + } else { + previous = CachedChannelData() + } + + previous = previous.withUpdatedIsNotAccessible(false) + + minAvailableMessageIdUpdated = previous.minAvailableMessageId != minAvailableMessageId + + return previous.withUpdatedFlags(channelFlags) + .withUpdatedAbout(about) + .withUpdatedParticipantsSummary(CachedChannelParticipantsSummary(memberCount: participantsCount, adminCount: adminsCount, bannedCount: bannedCount, kickedCount: kickedCount)) + .withUpdatedExportedInvitation(ExportedInvitation(apiExportedInvite: apiExportedInvite)) + .withUpdatedBotInfos(botInfos) + .withUpdatedPinnedMessageId(pinnedMessageId) + .withUpdatedStickerPack(stickerPack) + .withUpdatedMinAvailableMessageId(minAvailableMessageId) + .withUpdatedMigrationReference(migrationReference) + .withUpdatedLinkedDiscussionPeerId(linkedDiscussionPeerId) + .withUpdatedPeerGeoLocation(peerGeoLocation) + .withUpdatedSlowModeTimeout(slowmodeSeconds) + .withUpdatedSlowModeValidUntilTimestamp(slowmodeNextSendDate) + .withUpdatedHasScheduledMessages(hasScheduledMessages) +// .withUpdatedStatsDatacenterId(statsDc ?? 0) + }) + + if let minAvailableMessageId = minAvailableMessageId, minAvailableMessageIdUpdated { + var resourceIds: [WrappedMediaResourceId] = [] + transaction.deleteMessagesInRange(peerId: peerId, namespace: minAvailableMessageId.namespace, minId: 1, maxId: minAvailableMessageId.id, forEachMedia: { media in + addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) + }) + if !resourceIds.isEmpty { + let _ = postbox.mediaBox.removeCachedResources(Set(resourceIds)).start() + } + } + case .chatFull: + break + } + } + } else { + transaction.updatePeerCachedData(peerIds: [peerId], update: { _, _ in + var updated = CachedChannelData() + updated = updated.withUpdatedIsNotAccessible(true) + return updated + }) + } + return true + } + } + } else { + return .single(false) } - } else { - return .complete() } } } diff --git a/submodules/TelegramCore/Sources/Wallpaper.swift b/submodules/TelegramCore/Sources/Wallpaper.swift index ff7b895163..4d41fa0368 100644 --- a/submodules/TelegramCore/Sources/Wallpaper.swift +++ b/submodules/TelegramCore/Sources/Wallpaper.swift @@ -8,21 +8,30 @@ import SyncCore extension WallpaperSettings { init(apiWallpaperSettings: Api.WallPaperSettings) { switch apiWallpaperSettings { - case let .wallPaperSettings(flags, backgroundColor, intensity): - self = WallpaperSettings(blur: (flags & 1 << 1) != 0, motion: (flags & 1 << 2) != 0, color: backgroundColor, intensity: intensity) + case let .wallPaperSettings(flags, backgroundColor, secondBackgroundColor, intensity, rotation): + self = WallpaperSettings(blur: (flags & 1 << 1) != 0, motion: (flags & 1 << 2) != 0, color: backgroundColor.flatMap { UInt32(bitPattern: $0) }, bottomColor: secondBackgroundColor.flatMap { UInt32(bitPattern: $0) }, intensity: intensity, rotation: rotation) } } } func apiWallpaperSettings(_ wallpaperSettings: WallpaperSettings) -> Api.WallPaperSettings { var flags: Int32 = 0 + if let _ = wallpaperSettings.color { + flags |= (1 << 0) + } if wallpaperSettings.blur { flags |= (1 << 1) } if wallpaperSettings.motion { flags |= (1 << 2) } - return .wallPaperSettings(flags: flags, backgroundColor: wallpaperSettings.color, intensity: wallpaperSettings.intensity) + if let _ = wallpaperSettings.intensity { + flags |= (1 << 3) + } + if let _ = wallpaperSettings.bottomColor { + flags |= (1 << 4) + } + return .wallPaperSettings(flags: flags, backgroundColor: wallpaperSettings.color.flatMap { Int32(bitPattern: $0) }, secondBackgroundColor: wallpaperSettings.bottomColor.flatMap { Int32(bitPattern: $0) }, intensity: wallpaperSettings.intensity, rotation: wallpaperSettings.rotation ?? 0) } extension TelegramWallpaper { @@ -38,9 +47,37 @@ extension TelegramWallpaper { } self = .file(id: id, accessHash: accessHash, isCreator: (flags & 1 << 0) != 0, isDefault: (flags & 1 << 1) != 0, isPattern: (flags & 1 << 3) != 0, isDark: (flags & 1 << 4) != 0, slug: slug, file: file, settings: wallpaperSettings) } else { - assertionFailure() + //assertionFailure() self = .color(0xffffff) } + case let .wallPaperNoFile(flags, settings): + if let settings = settings, case let .wallPaperSettings(flags, backgroundColor, secondBackgroundColor, intensity, rotation) = settings { + if let color = backgroundColor, let bottomColor = secondBackgroundColor { + self = .gradient(UInt32(bitPattern: color), UInt32(bitPattern: bottomColor), WallpaperSettings(rotation: rotation)) + } else if let color = backgroundColor { + self = .color(UInt32(bitPattern: color)) + } else { + self = .color(0xffffff) + } + } else { + self = .color(0xffffff) + } + + } + } + + var apiInputWallpaperAndSettings: (Api.InputWallPaper?, Api.WallPaperSettings)? { + switch self { + case .builtin: + return nil + case let .file(file): + return (.inputWallPaperSlug(slug: file.slug), apiWallpaperSettings(file.settings)) + case let .color(color): + return (.inputWallPaperNoFile, apiWallpaperSettings(WallpaperSettings(color: color))) + case let .gradient(topColor, bottomColor, settings): + return (.inputWallPaperNoFile, apiWallpaperSettings(WallpaperSettings(color: topColor, bottomColor: bottomColor, rotation: settings.rotation))) + default: + return nil } } } diff --git a/submodules/TelegramCore/Sources/Wallpapers.swift b/submodules/TelegramCore/Sources/Wallpapers.swift index 061aa7d4ac..039f88385e 100644 --- a/submodules/TelegramCore/Sources/Wallpapers.swift +++ b/submodules/TelegramCore/Sources/Wallpapers.swift @@ -132,8 +132,8 @@ public enum GetWallpaperError { case generic } -public func getWallpaper(account: Account, slug: String) -> Signal { - return account.network.request(Api.functions.account.getWallPaper(wallpaper: .inputWallPaperSlug(slug: slug))) +public func getWallpaper(network: Network, slug: String) -> Signal { + return network.request(Api.functions.account.getWallPaper(wallpaper: .inputWallPaperSlug(slug: slug))) |> mapError { _ -> GetWallpaperError in return .generic } |> map { wallpaper -> TelegramWallpaper in return TelegramWallpaper(apiWallpaper: wallpaper) diff --git a/submodules/AppIntents/BUCK b/submodules/TelegramIntents/BUCK similarity index 95% rename from submodules/AppIntents/BUCK rename to submodules/TelegramIntents/BUCK index 2bc4100875..0c6544dc69 100644 --- a/submodules/AppIntents/BUCK +++ b/submodules/TelegramIntents/BUCK @@ -1,7 +1,7 @@ load("//Config:buck_rule_macros.bzl", "static_library") static_library( - name = "AppIntents", + name = "TelegramIntents", srcs = glob([ "Sources/**/*.swift", ]), diff --git a/submodules/TelegramIntents/Info.plist b/submodules/TelegramIntents/Info.plist new file mode 100644 index 0000000000..e1fe4cfb7b --- /dev/null +++ b/submodules/TelegramIntents/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/submodules/TelegramIntents/Sources/TelegramIntents.swift b/submodules/TelegramIntents/Sources/TelegramIntents.swift new file mode 100644 index 0000000000..a06109c8d2 --- /dev/null +++ b/submodules/TelegramIntents/Sources/TelegramIntents.swift @@ -0,0 +1,206 @@ +import Foundation +import UIKit +import Intents +import Display +import Postbox +import TelegramCore +import SyncCore +import SwiftSignalKit +import TelegramUIPreferences +import TelegramPresentationData +import AvatarNode +import AccountContext + +private let savedMessagesAvatar: UIImage = { + return generateImage(CGSize(width: 60.0, height: 60.0)) { size, context in + var locations: [CGFloat] = [1.0, 0.0] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: [UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor] as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + + let factor = size.width / 60.0 + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: factor, y: -factor) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + if let savedMessagesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/SavedMessagesIcon"), color: .white) { + context.draw(savedMessagesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - savedMessagesIcon.size.width) / 2.0), y: floor((size.height - savedMessagesIcon.size.height) / 2.0)), size: savedMessagesIcon.size)) + } + }! +}() + +public enum SendMessageIntentContext { + case chat + case share +} + +public enum SendMessageIntentSubject: CaseIterable { + case contact + case savedMessages + case privateChat + case group + + func toString() -> String { + switch self { + case .contact: + return "contact" + case .savedMessages: + return "savedMessages" + case .privateChat: + return "privateChat" + case .group: + return "group" + } + } +} + +public func donateSendMessageIntent(account: Account, sharedContext: SharedAccountContext, intentContext: SendMessageIntentContext, peerIds: [PeerId]) { + if #available(iOSApplicationExtension 13.2, iOS 13.2, *) { + let _ = (sharedContext.accountManager.transaction { transaction -> Bool in + if case .none = transaction.getAccessChallengeData() { + return true + } else { + return false + } + } + |> mapToSignal { unlocked -> Signal<[(Peer, SendMessageIntentSubject, UIImage?)], NoError> in + if unlocked { + return sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.intentsSettings]) + |> mapToSignal { sharedData -> Signal<[(Peer, SendMessageIntentSubject)], NoError> in + let settings = (sharedData.entries[ApplicationSpecificSharedDataKeys.intentsSettings] as? IntentsSettings) ?? IntentsSettings.defaultSettings + if let accountId = settings.account, accountId != account.peerId { + return .single([]) + } + if case .chat = intentContext, settings.onlyShared { + return .single([]) + } + return account.postbox.transaction { transaction -> [(Peer, SendMessageIntentSubject)] in + var peers: [(Peer, SendMessageIntentSubject)] = [] + for peerId in peerIds { + if peerId.namespace != Namespaces.Peer.SecretChat, let peer = transaction.getPeer(peerId) { + var subject: SendMessageIntentSubject? + let chatListIndex = transaction.getPeerChatListIndex(peerId) + if chatListIndex?.0 == Namespaces.PeerGroup.archive { + continue + } + if peerId.namespace == Namespaces.Peer.CloudUser { + if peerId == account.peerId { + if !settings.savedMessages { + continue + } + subject = .savedMessages + } else if transaction.isPeerContact(peerId: peerId) { + if !settings.contacts { + continue + } + subject = .contact + } else { + if !settings.privateChats { + continue + } + subject = .privateChat + } + } else if peerId.namespace == Namespaces.Peer.CloudGroup { + if !settings.groups { + continue + } + subject = .group + } else if let peer = peer as? TelegramChannel { + if case .group = peer.info { + if !settings.groups { + continue + } + subject = .group + } else { + continue + } + } else { + continue + } + + if let subject = subject { + peers.append((peer, subject)) + } + } + } + return peers + } + } + |> mapToSignal { peers -> Signal<[(Peer, SendMessageIntentSubject, UIImage?)], NoError> in + var signals: [Signal<(Peer, SendMessageIntentSubject, UIImage?), NoError>] = [] + for (peer, subject) in peers { + if peer.id == account.peerId { + signals.append(.single((peer, subject, savedMessagesAvatar))) + } else { + let peerAndAvatar = (peerAvatarImage(account: account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: peer.smallProfileImage, round: false) ?? .single(nil)) + |> map { imageVersions -> (Peer, SendMessageIntentSubject, UIImage?) in + let avatarImage = imageVersions?.0 + return (peer, subject, avatarImage) + } + signals.append(peerAndAvatar) + } + } + return combineLatest(signals) + } + } else { + return .single([]) + } + } + |> deliverOnMainQueue).start(next: { peers in + let presentationData = sharedContext.currentPresentationData.with { $0 } + + for (peer, subject, avatarImage) in peers { + let recipientHandle = INPersonHandle(value: "tg\(peer.id.toInt64())", type: .unknown) + let displayTitle: String + var nameComponents = PersonNameComponents() + + if let peer = peer as? TelegramUser { + if peer.botInfo != nil || peer.flags.contains(.isSupport) { + continue + } + + if peer.id == account.peerId { + displayTitle = presentationData.strings.DialogList_SavedMessages + nameComponents.givenName = displayTitle + } else { + displayTitle = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + nameComponents.givenName = peer.firstName + nameComponents.familyName = peer.lastName + } + } else { + displayTitle = peer.compactDisplayTitle + nameComponents.givenName = displayTitle + } + + let recipient = INPerson(personHandle: recipientHandle, nameComponents: nameComponents, displayName: displayTitle, image: nil, contactIdentifier: nil, customIdentifier: "tg\(peer.id.toInt64())") + + let intent = INSendMessageIntent(recipients: [recipient], content: nil, speakableGroupName: INSpeakableString(spokenPhrase: displayTitle), conversationIdentifier: "tg\(peer.id.toInt64())", serviceName: nil, sender: nil) + if let avatarImage = avatarImage, let avatarImageData = avatarImage.jpegData(compressionQuality: 0.8) { + intent.setImage(INImage(imageData: avatarImageData), forParameterNamed: \.groupName) + } + let interaction = INInteraction(intent: intent, response: nil) + interaction.direction = .outgoing + interaction.groupIdentifier = "sendMessage_\(peer.id.toInt64())" + interaction.donate { error in + if let error = error { + print(error) + } + } + } + }) + } +} + +public func deleteSendMessageIntents(peerId: PeerId) { + if #available(iOS 10.0, *) { + INInteraction.delete(with: "sendMessage_\(peerId.toInt64())") + } +} + +public func deleteAllSendMessageIntents() { + if #available(iOS 10.0, *) { + INInteraction.deleteAll() + } +} diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 62b7251ee8..f74f864b5c 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -129,9 +129,12 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case volumeButtonToUnmuteTip = 9 case archiveChatTips = 10 case archiveIntroDismissed = 11 - case callsTabTip = 12 case cellularDataPermissionWarning = 13 case chatMessageSearchResultsTip = 14 + case chatMessageOptionsTip = 15 + case chatTextSelectionTip = 16 + case themeChangeTip = 17 + case callsTabTip = 18 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -233,6 +236,18 @@ private struct ApplicationSpecificNoticeKeys { static func chatMessageSearchResultsTip() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.chatMessageSearchResultsTip.key) } + + static func chatMessageOptionsTip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.chatMessageOptionsTip.key) + } + + static func chatTextSelectionTip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.chatTextSelectionTip.key) + } + + static func themeChangeTip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.themeChangeTip.key) + } } public struct ApplicationSpecificNotice { @@ -489,7 +504,7 @@ public struct ApplicationSpecificNotice { public static func setVolumeButtonToUnmute(accountManager: AccountManager) { let _ = accountManager.transaction { transaction -> Void in transaction.setNotice(ApplicationSpecificNoticeKeys.volumeButtonToUnmuteTip(), ApplicationSpecificBoolNotice()) - }.start() + }.start() } public static func getCallsTabTip(accountManager: AccountManager) -> Signal { @@ -546,6 +561,66 @@ public struct ApplicationSpecificNotice { } } + public static func getChatMessageOptionsTip(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.chatMessageOptionsTip()) as? ApplicationSpecificCounterNotice { + return value.value + } else { + return 0 + } + } + } + + public static func incrementChatMessageOptionsTip(accountManager: AccountManager, count: Int32 = 1) -> Signal { + return accountManager.transaction { transaction -> Void in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.chatMessageOptionsTip()) as? ApplicationSpecificCounterNotice { + currentValue = value.value + } + currentValue += count + + transaction.setNotice(ApplicationSpecificNoticeKeys.chatMessageOptionsTip(), ApplicationSpecificCounterNotice(value: currentValue)) + } + } + + public static func getChatTextSelectionTips(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.chatTextSelectionTip()) as? ApplicationSpecificCounterNotice { + return value.value + } else { + return 0 + } + } + } + + public static func incrementChatTextSelectionTips(accountManager: AccountManager, count: Int32 = 1) -> Signal { + return accountManager.transaction { transaction -> Void in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.chatTextSelectionTip()) as? ApplicationSpecificCounterNotice { + currentValue = value.value + } + currentValue += count + + transaction.setNotice(ApplicationSpecificNoticeKeys.chatTextSelectionTip(), ApplicationSpecificCounterNotice(value: currentValue)) + } + } + + public static func getThemeChangeTip(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Bool in + if let _ = transaction.getNotice(ApplicationSpecificNoticeKeys.themeChangeTip()) as? ApplicationSpecificBoolNotice { + return true + } else { + return false + } + } + } + + public static func markThemeChangeTipAsSeen(accountManager: AccountManager) { + let _ = accountManager.transaction { transaction -> Void in + transaction.setNotice(ApplicationSpecificNoticeKeys.themeChangeTip(), ApplicationSpecificBoolNotice()) + }.start() + } + public static func reset(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Void in } diff --git a/submodules/TelegramPermissionsUI/Sources/PermissionController.swift b/submodules/TelegramPermissionsUI/Sources/PermissionController.swift index 6964197836..e43f4b9f96 100644 --- a/submodules/TelegramPermissionsUI/Sources/PermissionController.swift +++ b/submodules/TelegramPermissionsUI/Sources/PermissionController.swift @@ -15,6 +15,8 @@ public final class PermissionController: ViewController { private var state: PermissionControllerContent? private var splashScreen = false + private var locationManager: LocationManager? + private var controllerNode: PermissionControllerNode { return self.displayNode as! PermissionControllerNode } @@ -185,11 +187,15 @@ public final class PermissionController: ViewController { case let .nearbyLocation(status): self.title = self.presentationData.strings.Permissions_PeopleNearbyTitle_v0 + if self.locationManager == nil { + self.locationManager = LocationManager() + } + self.allow = { [weak self] in if let strongSelf = self { switch status { case .requestable: - DeviceAccess.authorizeAccess(to: .location(.tracking), presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, { [weak self] result in + DeviceAccess.authorizeAccess(to: .location(.tracking), locationManager: strongSelf.locationManager, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, { [weak self] result in self?.proceed?(result) }) case .denied, .unreachable: diff --git a/submodules/TelegramPresentationData/Sources/ChatControllerBackgroundNode.swift b/submodules/TelegramPresentationData/Sources/ChatControllerBackgroundNode.swift index 5375ddab1b..f72ccd56f1 100644 --- a/submodules/TelegramPresentationData/Sources/ChatControllerBackgroundNode.swift +++ b/submodules/TelegramPresentationData/Sources/ChatControllerBackgroundNode.swift @@ -11,9 +11,9 @@ import AppBundle private var backgroundImageForWallpaper: (TelegramWallpaper, Bool, UIImage)? -public func chatControllerBackgroundImage(theme: PresentationTheme, wallpaper initialWallpaper: TelegramWallpaper, mediaBox: MediaBox, composed: Bool = true, knockoutMode: Bool) -> UIImage? { +public func chatControllerBackgroundImage(theme: PresentationTheme?, wallpaper initialWallpaper: TelegramWallpaper, mediaBox: MediaBox, composed: Bool = true, knockoutMode: Bool, cached: Bool = true) -> UIImage? { var wallpaper = initialWallpaper - if knockoutMode { + if knockoutMode, let theme = theme { switch theme.name { case let .builtin(name): switch name { @@ -28,9 +28,10 @@ public func chatControllerBackgroundImage(theme: PresentationTheme, wallpaper in } var backgroundImage: UIImage? - if composed && wallpaper == backgroundImageForWallpaper?.0, (wallpaper.settings?.blur ?? false) == backgroundImageForWallpaper?.1 { + if cached && composed && wallpaper == backgroundImageForWallpaper?.0, (wallpaper.settings?.blur ?? false) == backgroundImageForWallpaper?.1 { backgroundImage = backgroundImageForWallpaper?.2 } else { + var succeed = true switch wallpaper { case .builtin: if let filePath = getAppBundle().path(forResource: "ChatWallpaperBuiltin0", ofType: "jpg") { @@ -38,9 +39,23 @@ public func chatControllerBackgroundImage(theme: PresentationTheme, wallpaper in } case let .color(color): backgroundImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in - context.setFillColor(UIColor(rgb: UInt32(bitPattern: color)).cgColor) + context.setFillColor(UIColor(argb: color).withAlphaComponent(1.0).cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) }) + case let .gradient(topColor, bottomColor, settings): + backgroundImage = generateImage(CGSize(width: 640.0, height: 1280.0), rotatedContext: { size, context in + let gradientColors = [UIColor(argb: topColor).cgColor, UIColor(argb: bottomColor).cgColor] as CFArray + + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + context.translateBy(x: 320.0, y: 640.0) + context.rotate(by: CGFloat(settings.rotation ?? 0) * CGFloat.pi / 180.0) + context.translateBy(x: -320.0, y: -640.0) + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + }) case let .image(representations, settings): if let largest = largestImageRepresentation(representations) { if settings.blur && composed { @@ -53,13 +68,14 @@ public func chatControllerBackgroundImage(theme: PresentationTheme, wallpaper in backgroundImage = image } if backgroundImage == nil, let path = mediaBox.completedResourcePath(largest.resource) { + succeed = false backgroundImage = UIImage(contentsOfFile: path)?.precomposed() } } case let .file(file): - if file.isPattern, let color = file.settings.color, let intensity = file.settings.intensity { + if wallpaper.isPattern, let color = file.settings.color, let intensity = file.settings.intensity { var image: UIImage? - let _ = mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPatternWallpaperRepresentation(color: color, intensity: intensity), complete: true, fetch: true, attemptSynchronously: true).start(next: { data in + let _ = mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPatternWallpaperRepresentation(color: color, bottomColor: file.settings.bottomColor, intensity: intensity, rotation: file.settings.rotation), complete: true, fetch: true, attemptSynchronously: true).start(next: { data in if data.complete { image = UIImage(contentsOfFile: data.path)?.precomposed() } @@ -76,13 +92,175 @@ public func chatControllerBackgroundImage(theme: PresentationTheme, wallpaper in backgroundImage = image } if backgroundImage == nil, let path = mediaBox.completedResourcePath(file.file.resource) { + succeed = false backgroundImage = UIImage(contentsOfFile: path)?.precomposed() } } } - if let backgroundImage = backgroundImage, composed { + if let backgroundImage = backgroundImage, composed && succeed { backgroundImageForWallpaper = (wallpaper, (wallpaper.settings?.blur ?? false), backgroundImage) } } return backgroundImage } + +private var signalBackgroundImageForWallpaper: (TelegramWallpaper, Bool, UIImage)? + +public func chatControllerBackgroundImageSignal(wallpaper: TelegramWallpaper, mediaBox: MediaBox, accountMediaBox: MediaBox) -> Signal<(UIImage?, Bool)?, NoError> { + var backgroundImage: UIImage? + if wallpaper == signalBackgroundImageForWallpaper?.0, (wallpaper.settings?.blur ?? false) == signalBackgroundImageForWallpaper?.1, let image = signalBackgroundImageForWallpaper?.2 { + return .single((image, true)) + } else { + func cacheWallpaper(_ image: UIImage?) { + if let image = image { + Queue.mainQueue().async { + signalBackgroundImageForWallpaper = (wallpaper, (wallpaper.settings?.blur ?? false), image) + } + } + } + + switch wallpaper { + case .builtin: + if let filePath = getAppBundle().path(forResource: "ChatWallpaperBuiltin0", ofType: "jpg") { + return .single((UIImage(contentsOfFile: filePath)?.precomposed(), true)) + |> afterNext { image in + cacheWallpaper(image?.0) + } + } + case let .color(color): + return .single((generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in + context.setFillColor(UIColor(argb: color).withAlphaComponent(1.0).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + }), true)) + |> afterNext { image in + cacheWallpaper(image?.0) + } + case let .gradient(topColor, bottomColor, settings): + return .single((generateImage(CGSize(width: 640.0, height: 1280.0), rotatedContext: { size, context in + let gradientColors = [UIColor(argb: topColor).cgColor, UIColor(argb: bottomColor).cgColor] as CFArray + + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + context.translateBy(x: 320.0, y: 640.0) + context.rotate(by: CGFloat(settings.rotation ?? 0) * CGFloat.pi / 180.0) + context.translateBy(x: -320.0, y: -640.0) + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + }), true)) + |> afterNext { image in + cacheWallpaper(image?.0) + } + case let .image(representations, settings): + if let largest = largestImageRepresentation(representations) { + if settings.blur { + return mediaBox.cachedResourceRepresentation(largest.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true, attemptSynchronously: true) + |> map { data -> (UIImage?, Bool)? in + if data.complete { + return (UIImage(contentsOfFile: data.path)?.precomposed(), true) + } else { + return nil + } + } + |> afterNext { image in + cacheWallpaper(image?.0) + } + } else if let path = mediaBox.completedResourcePath(largest.resource) { + return .single((UIImage(contentsOfFile: path)?.precomposed(), true)) + |> afterNext { image in + cacheWallpaper(image?.0) + } + } + } + case let .file(file): + if wallpaper.isPattern, let color = file.settings.color, let intensity = file.settings.intensity { + let representation = CachedPatternWallpaperRepresentation(color: color, bottomColor: file.settings.bottomColor, intensity: intensity, rotation: file.settings.rotation) + + let effectiveMediaBox: MediaBox + if FileManager.default.fileExists(atPath: mediaBox.cachedRepresentationCompletePath(file.file.resource.id, representation: representation)) { + effectiveMediaBox = mediaBox + } else { + effectiveMediaBox = accountMediaBox + } + + return effectiveMediaBox.cachedResourceRepresentation(file.file.resource, representation: representation, complete: true, fetch: true, attemptSynchronously: true) + |> take(1) + |> mapToSignal { data -> Signal<(UIImage?, Bool)?, NoError> in + if data.complete { + return .single((UIImage(contentsOfFile: data.path)?.precomposed(), true)) + } else { + let interimWallpaper: TelegramWallpaper + if let secondColor = file.settings.bottomColor { + interimWallpaper = .gradient(color, secondColor, file.settings) + } else { + interimWallpaper = .color(color) + } + + let settings = file.settings + let interrimImage = generateImage(CGSize(width: 640.0, height: 1280.0), rotatedContext: { size, context in + if let color = settings.color { + let gradientColors = [UIColor(argb: color).cgColor, UIColor(argb: settings.bottomColor ?? color).cgColor] as CFArray + + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + context.translateBy(x: 320.0, y: 640.0) + context.rotate(by: CGFloat(settings.rotation ?? 0) * CGFloat.pi / 180.0) + context.translateBy(x: -320.0, y: -640.0) + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + } + }) + + return .single((interrimImage, false)) |> then(effectiveMediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPatternWallpaperRepresentation(color: color, bottomColor: file.settings.bottomColor, intensity: intensity, rotation: file.settings.rotation), complete: true, fetch: true, attemptSynchronously: false) + |> map { data -> (UIImage?, Bool)? in + return (UIImage(contentsOfFile: data.path)?.precomposed(), true) + }) + } + } + |> afterNext { image in + cacheWallpaper(image?.0) + } + } else { + if file.settings.blur { + let representation = CachedBlurredWallpaperRepresentation() + + let effectiveMediaBox: MediaBox + if FileManager.default.fileExists(atPath: mediaBox.cachedRepresentationCompletePath(file.file.resource.id, representation: representation)) { + effectiveMediaBox = mediaBox + } else { + effectiveMediaBox = accountMediaBox + } + + return effectiveMediaBox.cachedResourceRepresentation(file.file.resource, representation: representation, complete: true, fetch: true, attemptSynchronously: true) + |> map { data -> (UIImage?, Bool)? in + if data.complete { + return (UIImage(contentsOfFile: data.path)?.precomposed(), true) + } else { + return nil + } + } + |> afterNext { image in + cacheWallpaper(image?.0) + } + } else { + var path: String? + if let maybePath = mediaBox.completedResourcePath(file.file.resource) { + path = maybePath + } else if let maybePath = accountMediaBox.completedResourcePath(file.file.resource) { + path = maybePath + } + if let path = path { + return .single((UIImage(contentsOfFile: path)?.precomposed(), true)) + |> afterNext { image in + cacheWallpaper(image?.0) + } + } + } + } + } + } + return .complete() +} diff --git a/submodules/TelegramPresentationData/Sources/ChatMessageBubbleImages.swift b/submodules/TelegramPresentationData/Sources/ChatMessageBubbleImages.swift index 2ccebc9c4f..ef0d6dc6b7 100644 --- a/submodules/TelegramPresentationData/Sources/ChatMessageBubbleImages.swift +++ b/submodules/TelegramPresentationData/Sources/ChatMessageBubbleImages.swift @@ -26,91 +26,332 @@ public func messageSingleBubbleLikeImage(fillColor: UIColor, strokeColor: UIColo })!.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0)) } -public func messageBubbleImage(incoming: Bool, fillColor: UIColor, strokeColor: UIColor, neighbors: MessageBubbleImageNeighbors, theme: PresentationThemeChat, wallpaper: TelegramWallpaper, knockout knockoutValue: Bool, mask: Bool = false) -> UIImage { - let diameter: CGFloat = 36.0 - let corner: CGFloat = 7.0 +private let minRadiusForFullTailCorner: CGFloat = 14.0 + +func mediaBubbleCornerImage(incoming: Bool, radius: CGFloat, inset: CGFloat) -> UIImage { + let imageSize = CGSize(width: radius + 7.0, height: 8.0) + let fixedMainDiameter: CGFloat = 33.0 + + let formContext = DrawingContext(size: imageSize) + formContext.withFlippedContext { context in + context.clear(CGRect(origin: CGPoint(), size: imageSize)) + context.translateBy(x: imageSize.width / 2.0, y: imageSize.height / 2.0) + context.scaleBy(x: incoming ? -1.0 : 1.0, y: -1.0) + context.translateBy(x: -imageSize.width / 2.0, y: -imageSize.height / 2.0) + + context.setFillColor(UIColor.black.cgColor) + + let bottomEllipse = CGRect(origin: CGPoint(x: 24.0, y: 16.0), size: CGSize(width: 27.0, height: 17.0)).insetBy(dx: inset, dy: inset).offsetBy(dx: inset, dy: inset) + let topEllipse = CGRect(origin: CGPoint(x: 33.0, y: 14.0), size: CGSize(width: 23.0, height: 21.0)).insetBy(dx: -inset, dy: -inset).offsetBy(dx: inset, dy: inset) + + context.translateBy(x: -fixedMainDiameter + imageSize.width - 6.0, y: -fixedMainDiameter + imageSize.height) + + let topLeftRadius: CGFloat = 2.0 + let topRightRadius: CGFloat = 2.0 + let bottomLeftRadius: CGFloat = 2.0 + let bottomRightRadius: CGFloat = radius + + context.move(to: CGPoint(x: 0.0, y: topLeftRadius)) + context.addArc(tangent1End: CGPoint(x: 0.0, y: 0.0), tangent2End: CGPoint(x: topLeftRadius, y: 0.0), radius: topLeftRadius) + context.addLine(to: CGPoint(x: fixedMainDiameter - topRightRadius, y: 0.0)) + context.addArc(tangent1End: CGPoint(x: fixedMainDiameter, y: 0.0), tangent2End: CGPoint(x: fixedMainDiameter, y: topRightRadius), radius: topRightRadius) + context.addLine(to: CGPoint(x: fixedMainDiameter, y: fixedMainDiameter - bottomRightRadius)) + context.addArc(tangent1End: CGPoint(x: fixedMainDiameter, y: fixedMainDiameter), tangent2End: CGPoint(x: fixedMainDiameter - bottomRightRadius, y: fixedMainDiameter), radius: bottomRightRadius) + context.addLine(to: CGPoint(x: bottomLeftRadius, y: fixedMainDiameter)) + context.addArc(tangent1End: CGPoint(x: 0.0, y: fixedMainDiameter), tangent2End: CGPoint(x: 0.0, y: fixedMainDiameter - bottomLeftRadius), radius: bottomLeftRadius) + context.addLine(to: CGPoint(x: 0.0, y: topLeftRadius)) + context.fillPath() + + if radius >= minRadiusForFullTailCorner { + context.move(to: CGPoint(x: bottomEllipse.minX, y: bottomEllipse.midY)) + context.addQuadCurve(to: CGPoint(x: bottomEllipse.midX, y: bottomEllipse.maxY), control: CGPoint(x: bottomEllipse.minX, y: bottomEllipse.maxY)) + context.addQuadCurve(to: CGPoint(x: bottomEllipse.maxX, y: bottomEllipse.midY), control: CGPoint(x: bottomEllipse.maxX, y: bottomEllipse.maxY)) + context.fillPath() + } else { + context.fill(CGRect(origin: CGPoint(x: bottomEllipse.minX - 5.0, y: bottomEllipse.midY), size: CGSize(width: bottomEllipse.width + 5.0, height: bottomEllipse.height / 2.0))) + } + context.fill(CGRect(origin: CGPoint(x: fixedMainDiameter / 2.0, y: floor(fixedMainDiameter / 2.0)), size: CGSize(width: fixedMainDiameter / 2.0, height: ceil(bottomEllipse.midY) - floor(fixedMainDiameter / 2.0)))) + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fillEllipse(in: topEllipse) + } + + return formContext.generateImage()! +} + +public func messageBubbleImage(maxCornerRadius: CGFloat, minCornerRadius: CGFloat, incoming: Bool, fillColor: UIColor, strokeColor: UIColor, neighbors: MessageBubbleImageNeighbors, theme: PresentationThemeChat, wallpaper: TelegramWallpaper, knockout knockoutValue: Bool, mask: Bool = false, extendedEdges: Bool = false, onlyOutline: Bool = false, onlyShadow: Bool = false) -> UIImage { + let topLeftRadius: CGFloat + let topRightRadius: CGFloat + let bottomLeftRadius: CGFloat + let bottomRightRadius: CGFloat + let drawTail: Bool + + switch neighbors { + case .none: + topLeftRadius = maxCornerRadius + topRightRadius = maxCornerRadius + bottomLeftRadius = maxCornerRadius + bottomRightRadius = maxCornerRadius + drawTail = true + case .both: + topLeftRadius = maxCornerRadius + topRightRadius = minCornerRadius + bottomLeftRadius = maxCornerRadius + bottomRightRadius = minCornerRadius + drawTail = false + case .bottom: + topLeftRadius = maxCornerRadius + topRightRadius = minCornerRadius + bottomLeftRadius = maxCornerRadius + bottomRightRadius = maxCornerRadius + drawTail = true + case .side: + topLeftRadius = maxCornerRadius + topRightRadius = maxCornerRadius + bottomLeftRadius = minCornerRadius + bottomRightRadius = minCornerRadius + drawTail = false + case let .top(side): + topLeftRadius = maxCornerRadius + topRightRadius = maxCornerRadius + bottomLeftRadius = side ? minCornerRadius : maxCornerRadius + bottomRightRadius = minCornerRadius + drawTail = false + } + + let fixedMainDiameter: CGFloat = 33.0 + let innerSize = CGSize(width: fixedMainDiameter + 6.0, height: fixedMainDiameter) + let strokeInset: CGFloat = 1.0 + let sourceRawSize = CGSize(width: innerSize.width + strokeInset * 2.0, height: innerSize.height + strokeInset * 2.0) + let additionalInset: CGFloat = onlyShadow ? 10.0 : 1.0 + let imageSize = CGSize(width: sourceRawSize.width + additionalInset * 2.0, height: sourceRawSize.height + additionalInset * 2.0) + let outgoingStretchPoint: (x: Int, y: Int) = (Int(additionalInset + strokeInset + round(fixedMainDiameter / 2.0)) - 1, Int(additionalInset + strokeInset + round(fixedMainDiameter / 2.0))) + let incomingStretchPoint: (x: Int, y: Int) = (Int(sourceRawSize.width) - outgoingStretchPoint.x + Int(additionalInset), outgoingStretchPoint.y) + let knockout = knockoutValue && !mask - return generateImage(CGSize(width: 42.0, height: diameter), contextGenerator: { size, context in - var drawWithClearColor = false + let rawSize = imageSize + + let bottomEllipse = CGRect(origin: CGPoint(x: 24.0, y: 16.0), size: CGSize(width: 27.0, height: 17.0)) + let topEllipse = CGRect(origin: CGPoint(x: 33.0, y: 14.0), size: CGSize(width: 23.0, height: 21.0)) + + let formContext = DrawingContext(size: imageSize) + formContext.withFlippedContext { context in + context.clear(CGRect(origin: CGPoint(), size: rawSize)) + context.translateBy(x: additionalInset + strokeInset, y: additionalInset + strokeInset) - if knockout, case let .color(color) = wallpaper { - drawWithClearColor = !mask - context.setFillColor(UIColor(rgb: UInt32(color)).cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.black.cgColor) + + context.move(to: CGPoint(x: 0.0, y: topLeftRadius)) + context.addArc(tangent1End: CGPoint(x: 0.0, y: 0.0), tangent2End: CGPoint(x: topLeftRadius, y: 0.0), radius: topLeftRadius) + context.addLine(to: CGPoint(x: fixedMainDiameter - topRightRadius, y: 0.0)) + context.addArc(tangent1End: CGPoint(x: fixedMainDiameter, y: 0.0), tangent2End: CGPoint(x: fixedMainDiameter, y: topRightRadius), radius: topRightRadius) + context.addLine(to: CGPoint(x: fixedMainDiameter, y: fixedMainDiameter - bottomRightRadius)) + context.addArc(tangent1End: CGPoint(x: fixedMainDiameter, y: fixedMainDiameter), tangent2End: CGPoint(x: fixedMainDiameter - bottomRightRadius, y: fixedMainDiameter), radius: bottomRightRadius) + context.addLine(to: CGPoint(x: bottomLeftRadius, y: fixedMainDiameter)) + context.addArc(tangent1End: CGPoint(x: 0.0, y: fixedMainDiameter), tangent2End: CGPoint(x: 0.0, y: fixedMainDiameter - bottomLeftRadius), radius: bottomLeftRadius) + context.addLine(to: CGPoint(x: 0.0, y: topLeftRadius)) + context.fillPath() + + if drawTail { + if maxCornerRadius >= minRadiusForFullTailCorner { + context.move(to: CGPoint(x: bottomEllipse.minX, y: bottomEllipse.midY)) + context.addQuadCurve(to: CGPoint(x: bottomEllipse.midX, y: bottomEllipse.maxY), control: CGPoint(x: bottomEllipse.minX, y: bottomEllipse.maxY)) + context.addQuadCurve(to: CGPoint(x: bottomEllipse.maxX, y: bottomEllipse.midY), control: CGPoint(x: bottomEllipse.maxX, y: bottomEllipse.maxY)) + context.fillPath() + } else { + context.fill(CGRect(origin: CGPoint(x: bottomEllipse.minX - 2.0, y: bottomEllipse.midY), size: CGSize(width: bottomEllipse.width + 2.0, height: bottomEllipse.height / 2.0))) + } + context.fill(CGRect(origin: CGPoint(x: fixedMainDiameter / 2.0, y: floor(fixedMainDiameter / 2.0)), size: CGSize(width: fixedMainDiameter / 2.0, height: ceil(bottomEllipse.midY) - floor(fixedMainDiameter / 2.0)))) + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fillEllipse(in: topEllipse) + } + } + let formImage = formContext.generateImage()! + + let outlineContext = DrawingContext(size: imageSize) + outlineContext.withFlippedContext { context in + context.clear(CGRect(origin: CGPoint(), size: rawSize)) + context.translateBy(x: additionalInset + strokeInset, y: additionalInset + strokeInset) + + context.setStrokeColor(UIColor.black.cgColor) + let borderWidth: CGFloat + let borderOffset: CGFloat + + let innerExtension: CGFloat + if knockout && !mask { + innerExtension = 0.25 } else { - context.clear(CGRect(origin: CGPoint(), size: size)) + innerExtension = 0.25 } - let additionalOffset: CGFloat - switch neighbors { - case .none, .bottom: - additionalOffset = 0.0 - case .both, .side, .top: - additionalOffset = 6.0 + if abs(UIScreenPixel - 0.5) < CGFloat.ulpOfOne { + borderWidth = UIScreenPixel + innerExtension + borderOffset = -innerExtension / 2.0 + UIScreenPixel / 2.0 + } else { + borderWidth = UIScreenPixel * 2.0 + innerExtension + borderOffset = -innerExtension / 2.0 + UIScreenPixel * 2.0 / 2.0 } + context.setLineWidth(borderWidth) - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: incoming ? 1.0 : -1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0 + 0.5 + additionalOffset, y: -size.height / 2.0 + 0.5) + context.move(to: CGPoint(x: -borderOffset, y: topLeftRadius + borderOffset)) + context.addArc(tangent1End: CGPoint(x: -borderOffset, y: -borderOffset), tangent2End: CGPoint(x: topLeftRadius + borderOffset, y: -borderOffset), radius: topLeftRadius + borderOffset * 2.0) + context.addLine(to: CGPoint(x: fixedMainDiameter - topRightRadius - borderOffset, y: -borderOffset)) + context.addArc(tangent1End: CGPoint(x: fixedMainDiameter + borderOffset, y: -borderOffset), tangent2End: CGPoint(x: fixedMainDiameter + borderOffset, y: topRightRadius + borderOffset), radius: topRightRadius + borderOffset * 2.0) + context.addLine(to: CGPoint(x: fixedMainDiameter + borderOffset, y: fixedMainDiameter - bottomRightRadius - borderOffset)) + context.addArc(tangent1End: CGPoint(x: fixedMainDiameter + borderOffset, y: fixedMainDiameter + borderOffset), tangent2End: CGPoint(x: fixedMainDiameter - bottomRightRadius - borderOffset, y: fixedMainDiameter + borderOffset), radius: bottomRightRadius + borderOffset * 2.0) + context.addLine(to: CGPoint(x: bottomLeftRadius + borderOffset, y: fixedMainDiameter + borderOffset)) + context.addArc(tangent1End: CGPoint(x: -borderOffset, y: fixedMainDiameter + borderOffset), tangent2End: CGPoint(x: -borderOffset, y: fixedMainDiameter - bottomLeftRadius - borderOffset), radius: bottomLeftRadius + borderOffset * 2.0) + context.closePath() + context.strokePath() - let lineWidth: CGFloat = 1.0 - - if drawWithClearColor { + if drawTail { + let outlineBottomEllipse = bottomEllipse.insetBy(dx: -borderOffset, dy: -borderOffset) + let outlineInnerTopEllipse = topEllipse.insetBy(dx: borderOffset, dy: borderOffset) + let outlineTopEllipse = topEllipse.insetBy(dx: -borderOffset, dy: -borderOffset) + context.setBlendMode(.copy) context.setFillColor(UIColor.clear.cgColor) - } else { - context.setFillColor(fillColor.cgColor) - context.setLineWidth(lineWidth) - context.setStrokeColor(strokeColor.cgColor) - } - - switch neighbors { - case .none: - if !drawWithClearColor { - let _ = try? drawSvgPath(context, path: "M6,17.5 C6,7.83289181 13.8350169,0 23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41102995e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") - context.strokePath() - } - let _ = try? drawSvgPath(context, path: "M6,17.5 C6,7.83289181 13.8350169,0 23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41102995e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") + + if maxCornerRadius >= minRadiusForFullTailCorner { + context.move(to: CGPoint(x: bottomEllipse.minX, y: bottomEllipse.midY)) + context.addQuadCurve(to: CGPoint(x: bottomEllipse.midX, y: bottomEllipse.maxY), control: CGPoint(x: bottomEllipse.minX, y: bottomEllipse.maxY)) + context.addQuadCurve(to: CGPoint(x: bottomEllipse.maxX, y: bottomEllipse.midY), control: CGPoint(x: bottomEllipse.maxX, y: bottomEllipse.maxY)) context.fillPath() - case .side: - if !drawWithClearColor { - context.strokeEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 35.0, height: 35.0))) - context.strokePath() - } - context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 35.0, height: 35.0))) - case let .top(side): - if side { - if !drawWithClearColor { - let _ = try? drawSvgPath(context, path: "M17.5,0 L17.5,0 C27.1649831,-1.7754286e-15 35,7.83501688 35,17.5 L35,29 C35,32.3137085 32.3137085,35 29,35 L6,35 C2.6862915,35 4.05812251e-16,32.3137085 0,29 L0,17.5 C-1.18361906e-15,7.83501688 7.83501688,1.7754286e-15 17.5,0 ") - context.strokePath() - } - let _ = try? drawSvgPath(context, path: "M17.5,0 L17.5,0 C27.1649831,-1.7754286e-15 35,7.83501688 35,17.5 L35,29 C35,32.3137085 32.3137085,35 29,35 L6,35 C2.6862915,35 4.05812251e-16,32.3137085 0,29 L0,17.5 C-1.18361906e-15,7.83501688 7.83501688,1.7754286e-15 17.5,0 ") - context.fillPath() + } else { + context.fill(CGRect(origin: CGPoint(x: bottomEllipse.minX - 2.0, y: floor(bottomEllipse.midY)), size: CGSize(width: bottomEllipse.width + 2.0, height: ceil(bottomEllipse.height / 2.0)))) + } + context.fill(CGRect(origin: CGPoint(x: floor(fixedMainDiameter / 2.0), y: fixedMainDiameter / 2.0), size: CGSize(width: fixedMainDiameter / 2.0 + borderWidth, height: ceil(bottomEllipse.midY) - floor(fixedMainDiameter / 2.0)))) + + context.setBlendMode(.normal) + context.move(to: CGPoint(x: fixedMainDiameter + borderOffset, y: fixedMainDiameter / 2.0)) + context.addLine(to: CGPoint(x: fixedMainDiameter + borderOffset, y: outlineBottomEllipse.midY)) + context.strokePath() + + let bubbleTailContext = DrawingContext(size: imageSize) + bubbleTailContext.withFlippedContext { context in + context.clear(CGRect(origin: CGPoint(), size: rawSize)) + context.translateBy(x: additionalInset + strokeInset, y: additionalInset + strokeInset) + + context.setStrokeColor(UIColor.black.cgColor) + context.setLineWidth(borderWidth) + + if maxCornerRadius >= minRadiusForFullTailCorner { + context.move(to: CGPoint(x: outlineBottomEllipse.minX, y: outlineBottomEllipse.midY)) + context.addQuadCurve(to: CGPoint(x: outlineBottomEllipse.midX, y: outlineBottomEllipse.maxY), control: CGPoint(x: outlineBottomEllipse.minX, y: outlineBottomEllipse.maxY)) + context.addQuadCurve(to: CGPoint(x: outlineBottomEllipse.maxX, y: outlineBottomEllipse.midY), control: CGPoint(x: outlineBottomEllipse.maxX, y: outlineBottomEllipse.maxY)) } else { - if !drawWithClearColor { - let _ = try? drawSvgPath(context, path: "M35,17.5 C35,7.83501688 27.1671082,0 17.5,0 L17.5,0 C7.83501688,0 0,7.83289181 0,17.5 L0,29.0031815 C0,32.3151329 2.6882755,35 5.99681848,35 L17.5,35 C27.1649831,35 35,27.1671082 35,17.5 L35,17.5 L35,17.5 ") - context.strokePath() + context.move(to: CGPoint(x: outlineBottomEllipse.minX - 2.0, y: outlineBottomEllipse.maxY)) + context.addLine(to: CGPoint(x: outlineBottomEllipse.minX, y: outlineBottomEllipse.maxY)) + context.addLine(to: CGPoint(x: outlineBottomEllipse.maxX, y: outlineBottomEllipse.maxY)) + } + context.strokePath() + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fillEllipse(in: outlineInnerTopEllipse) + + context.move(to: CGPoint(x: 0.0, y: topLeftRadius)) + context.addArc(tangent1End: CGPoint(x: 0.0, y: 0.0), tangent2End: CGPoint(x: topLeftRadius, y: 0.0), radius: topLeftRadius) + context.addLine(to: CGPoint(x: fixedMainDiameter - topRightRadius, y: 0.0)) + context.addArc(tangent1End: CGPoint(x: fixedMainDiameter, y: 0.0), tangent2End: CGPoint(x: fixedMainDiameter, y: topRightRadius), radius: topRightRadius) + context.addLine(to: CGPoint(x: fixedMainDiameter, y: fixedMainDiameter - bottomRightRadius)) + context.addArc(tangent1End: CGPoint(x: fixedMainDiameter, y: fixedMainDiameter), tangent2End: CGPoint(x: fixedMainDiameter - bottomRightRadius, y: fixedMainDiameter), radius: bottomRightRadius) + context.addLine(to: CGPoint(x: bottomLeftRadius, y: fixedMainDiameter)) + context.addArc(tangent1End: CGPoint(x: 0.0, y: fixedMainDiameter), tangent2End: CGPoint(x: 0.0, y: fixedMainDiameter - bottomLeftRadius), radius: bottomLeftRadius) + context.addLine(to: CGPoint(x: 0.0, y: topLeftRadius)) + context.fillPath() + + let bottomEllipseMask = generateImage(bottomEllipse.insetBy(dx: -1.0, dy: -1.0).size, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.black.cgColor) + if maxCornerRadius >= minRadiusForFullTailCorner { + context.fillEllipse(in: CGRect(origin: CGPoint(x: 1.0 - borderOffset, y: 1.0 - borderOffset), size: CGSize(width: outlineBottomEllipse.width, height: outlineBottomEllipse.height))) + } else { + context.fill(CGRect(origin: CGPoint(x: 1.0 - borderOffset, y: 1.0 - borderOffset), size: CGSize(width: outlineBottomEllipse.width, height: outlineBottomEllipse.height))) } - let _ = try? drawSvgPath(context, path: "M35,17.5 C35,7.83501688 27.1671082,0 17.5,0 L17.5,0 C7.83501688,0 0,7.83289181 0,17.5 L0,29.0031815 C0,32.3151329 2.6882755,35 5.99681848,35 L17.5,35 C27.1649831,35 35,27.1671082 35,17.5 L35,17.5 L35,17.5 ") - context.fillPath() - } - case .bottom: - if !drawWithClearColor { - let _ = try? drawSvgPath(context, path: "M6,17.5 L6,5.99681848 C6,2.6882755 8.68486709,0 11.9968185,0 L23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41103066e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") - context.strokePath() - } - let _ = try? drawSvgPath(context, path: "M6,17.5 L6,5.99681848 C6,2.6882755 8.68486709,0 11.9968185,0 L23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41103066e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") - context.fillPath() - case .both: - if !drawWithClearColor { - let _ = try? drawSvgPath(context, path: "M35,17.5 C35,7.83501688 27.1671082,0 17.5,0 L5.99681848,0 C2.68486709,0 0,2.6882755 0,5.99681848 L0,29.0031815 C0,32.3151329 2.6882755,35 5.99681848,35 L17.5,35 C27.1649831,35 35,27.1671082 35,17.5 L35,17.5 L35,17.5 ") - context.strokePath() - } - let _ = try? drawSvgPath(context, path: "M35,17.5 C35,7.83501688 27.1671082,0 17.5,0 L5.99681848,0 C2.68486709,0 0,2.6882755 0,5.99681848 L0,29.0031815 C0,32.3151329 2.6882755,35 5.99681848,35 L17.5,35 C27.1649831,35 35,27.1671082 35,17.5 L35,17.5 L35,17.5 ") - context.fillPath() + context.clear(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) + })! + + context.clip(to: bottomEllipse.insetBy(dx: -1.0, dy: -1.0), mask: bottomEllipseMask.cgImage!) + context.strokeEllipse(in: outlineInnerTopEllipse) + context.resetClip() + } + + context.translateBy(x: -(additionalInset + strokeInset), y: -(additionalInset + strokeInset)) + context.draw(bubbleTailContext.generateImage()!.cgImage!, in: CGRect(origin: CGPoint(), size: rawSize)) + context.translateBy(x: additionalInset + strokeInset, y: additionalInset + strokeInset) } - })!.stretchableImage(withLeftCapWidth: incoming ? Int(corner + diameter / 2.0) : Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0)) + } + let outlineImage = generateImage(outlineContext.size, contextGenerator: { size, context in + context.setBlendMode(.copy) + let image = outlineContext.generateImage()! + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) + })! + + let drawingContext = DrawingContext(size: imageSize) + drawingContext.withFlippedContext { context in + if onlyShadow { + context.clear(CGRect(origin: CGPoint(), size: rawSize)) + + let bubbleColors = incoming ? theme.message.incoming : theme.message.outgoing + + if let shadow = bubbleColors.bubble.withWallpaper.shadow { + context.translateBy(x: rawSize.width / 2.0, y: rawSize.height / 2.0) + context.scaleBy(x: incoming ? -1.0 : 1.0, y: -1.0) + context.translateBy(x: -rawSize.width / 2.0, y: -rawSize.height / 2.0) + + context.setShadow(offset: CGSize(width: 0.0, height: -shadow.verticalOffset), blur: shadow.radius, color: shadow.color.cgColor) + context.draw(formImage.cgImage!, in: CGRect(origin: CGPoint(), size: rawSize)) + + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.clip(to: CGRect(origin: CGPoint(), size: rawSize), mask: formImage.cgImage!) + context.fill(CGRect(origin: CGPoint(), size: rawSize)) + } + } else { + var drawWithClearColor = false + + if knockout { + drawWithClearColor = !mask + if case let .color(color) = wallpaper { + context.setFillColor(UIColor(rgb: UInt32(color)).cgColor) + context.fill(CGRect(origin: CGPoint(), size: rawSize)) + } else { + context.clear(CGRect(origin: CGPoint(), size: rawSize)) + } + } else { + context.clear(CGRect(origin: CGPoint(), size: rawSize)) + } + + if drawWithClearColor { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + } else { + context.setBlendMode(.normal) + context.setFillColor(fillColor.cgColor) + } + + context.saveGState() + + context.translateBy(x: rawSize.width / 2.0, y: rawSize.height / 2.0) + context.scaleBy(x: incoming ? -1.0 : 1.0, y: -1.0) + context.translateBy(x: -rawSize.width / 2.0, y: -rawSize.height / 2.0) + + if !onlyOutline { + context.clip(to: CGRect(origin: CGPoint(), size: rawSize), mask: formImage.cgImage!) + context.fill(CGRect(origin: CGPoint(), size: rawSize)) + } else { + context.setFillColor(strokeColor.cgColor) + context.clip(to: CGRect(origin: CGPoint(), size: rawSize), mask: outlineImage.cgImage!) + context.fill(CGRect(origin: CGPoint(), size: rawSize)) + } + + context.restoreGState() + } + } + + return drawingContext.generateImage()!.stretchableImage(withLeftCapWidth: incoming ? incomingStretchPoint.x : outgoingStretchPoint.x, topCapHeight: incoming ? incomingStretchPoint.y : outgoingStretchPoint.y) } public enum MessageBubbleActionButtonPosition { @@ -120,14 +361,14 @@ public enum MessageBubbleActionButtonPosition { case bottomSingle } -public func messageBubbleActionButtonImage(color: UIColor, strokeColor: UIColor, position: MessageBubbleActionButtonPosition) -> UIImage { - let largeRadius: CGFloat = 17.0 - let smallRadius: CGFloat = 6.0 +public func messageBubbleActionButtonImage(color: UIColor, strokeColor: UIColor, position: MessageBubbleActionButtonPosition, bubbleCorners: PresentationChatBubbleCorners) -> UIImage { + let largeRadius: CGFloat = bubbleCorners.mainRadius + let smallRadius: CGFloat = (bubbleCorners.mergeBubbleCorners && largeRadius >= 10.0) ? bubbleCorners.auxiliaryRadius : bubbleCorners.mainRadius let size: CGSize if case .middle = position { size = CGSize(width: smallRadius + smallRadius, height: smallRadius + smallRadius) } else { - size = CGSize(width: 35.0, height: 35.0) + size = CGSize(width: largeRadius + largeRadius, height: largeRadius + largeRadius) } return generateImage(size, contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift b/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift index 41f573a03f..51b824d8ad 100644 --- a/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift +++ b/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift @@ -1,6 +1,41 @@ import Foundation import UIKit import Display +import TelegramUIPreferences + +public extension PresentationFontSize { + init(systemFontSize: CGFloat) { + var closestIndex = 0 + let allSizes = PresentationFontSize.allCases + for i in 0 ..< allSizes.count { + if abs(allSizes[i].baseDisplaySize - systemFontSize) < abs(allSizes[closestIndex].baseDisplaySize - systemFontSize) { + closestIndex = i + } + } + self = allSizes[closestIndex] + } +} + +public extension PresentationFontSize { + var baseDisplaySize: CGFloat { + switch self { + case .extraSmall: + return 14.0 + case .small: + return 15.0 + case .medium: + return 16.0 + case .regular: + return 17.0 + case .large: + return 19.0 + case .extraLarge: + return 23.0 + case .extraLargeX2: + return 26.0 + } + } +} public extension TabBarControllerTheme { convenience init(rootControllerTheme: PresentationTheme) { @@ -33,22 +68,35 @@ public extension NavigationBarPresentationData { } public extension ActionSheetControllerTheme { - convenience init(presentationTheme: PresentationTheme) { + convenience init(presentationData: PresentationData) { + let presentationTheme = presentationData.theme + let actionSheet = presentationTheme.actionSheet - self.init(dimColor: actionSheet.dimColor, backgroundType: actionSheet.backgroundType == .light ? .light : .dark, itemBackgroundColor: actionSheet.itemBackgroundColor, itemHighlightedBackgroundColor: actionSheet.itemHighlightedBackgroundColor, standardActionTextColor: actionSheet.standardActionTextColor, destructiveActionTextColor: actionSheet.destructiveActionTextColor, disabledActionTextColor: actionSheet.disabledActionTextColor, primaryTextColor: actionSheet.primaryTextColor, secondaryTextColor: actionSheet.secondaryTextColor, controlAccentColor: actionSheet.controlAccentColor, controlColor: presentationTheme.list.disclosureArrowColor, switchFrameColor: presentationTheme.list.itemSwitchColors.frameColor, switchContentColor: presentationTheme.list.itemSwitchColors.contentColor, switchHandleColor: presentationTheme.list.itemSwitchColors.handleColor) + self.init(dimColor: actionSheet.dimColor, backgroundType: actionSheet.backgroundType == .light ? .light : .dark, itemBackgroundColor: actionSheet.itemBackgroundColor, itemHighlightedBackgroundColor: actionSheet.itemHighlightedBackgroundColor, standardActionTextColor: actionSheet.standardActionTextColor, destructiveActionTextColor: actionSheet.destructiveActionTextColor, disabledActionTextColor: actionSheet.disabledActionTextColor, primaryTextColor: actionSheet.primaryTextColor, secondaryTextColor: actionSheet.secondaryTextColor, controlAccentColor: actionSheet.controlAccentColor, controlColor: presentationTheme.list.disclosureArrowColor, switchFrameColor: presentationTheme.list.itemSwitchColors.frameColor, switchContentColor: presentationTheme.list.itemSwitchColors.contentColor, switchHandleColor: presentationTheme.list.itemSwitchColors.handleColor, baseFontSize: presentationData.listsFontSize.baseDisplaySize) + } + + convenience init(presentationTheme: PresentationTheme, fontSize: PresentationFontSize) { + let actionSheet = presentationTheme.actionSheet + self.init(dimColor: actionSheet.dimColor, backgroundType: actionSheet.backgroundType == .light ? .light : .dark, itemBackgroundColor: actionSheet.itemBackgroundColor, itemHighlightedBackgroundColor: actionSheet.itemHighlightedBackgroundColor, standardActionTextColor: actionSheet.standardActionTextColor, destructiveActionTextColor: actionSheet.destructiveActionTextColor, disabledActionTextColor: actionSheet.disabledActionTextColor, primaryTextColor: actionSheet.primaryTextColor, secondaryTextColor: actionSheet.secondaryTextColor, controlAccentColor: actionSheet.controlAccentColor, controlColor: presentationTheme.list.disclosureArrowColor, switchFrameColor: presentationTheme.list.itemSwitchColors.frameColor, switchContentColor: presentationTheme.list.itemSwitchColors.contentColor, switchHandleColor: presentationTheme.list.itemSwitchColors.handleColor, baseFontSize: fontSize.baseDisplaySize) } } public extension ActionSheetController { - convenience init(presentationTheme: PresentationTheme) { - self.init(theme: ActionSheetControllerTheme(presentationTheme: presentationTheme)) + convenience init(presentationData: PresentationData) { + self.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) } } public extension AlertControllerTheme { - convenience init(presentationTheme: PresentationTheme) { + convenience init(presentationTheme: PresentationTheme, fontSize: PresentationFontSize) { let actionSheet = presentationTheme.actionSheet - self.init(backgroundType: actionSheet.backgroundType == .light ? .light : .dark, backgroundColor: actionSheet.itemBackgroundColor, separatorColor: actionSheet.itemHighlightedBackgroundColor, highlightedItemColor: actionSheet.itemHighlightedBackgroundColor, primaryColor: actionSheet.primaryTextColor, secondaryColor: actionSheet.secondaryTextColor, accentColor: actionSheet.controlAccentColor, destructiveColor: actionSheet.destructiveActionTextColor, disabledColor: actionSheet.disabledActionTextColor) + self.init(backgroundType: actionSheet.backgroundType == .light ? .light : .dark, backgroundColor: actionSheet.itemBackgroundColor, separatorColor: actionSheet.itemHighlightedBackgroundColor, highlightedItemColor: actionSheet.itemHighlightedBackgroundColor, primaryColor: actionSheet.primaryTextColor, secondaryColor: actionSheet.secondaryTextColor, accentColor: actionSheet.controlAccentColor, destructiveColor: actionSheet.destructiveActionTextColor, disabledColor: actionSheet.disabledActionTextColor, baseFontSize: fontSize.baseDisplaySize) + } + + convenience init(presentationData: PresentationData) { + let presentationTheme = presentationData.theme + let actionSheet = presentationTheme.actionSheet + self.init(backgroundType: actionSheet.backgroundType == .light ? .light : .dark, backgroundColor: actionSheet.itemBackgroundColor, separatorColor: actionSheet.itemHighlightedBackgroundColor, highlightedItemColor: actionSheet.itemHighlightedBackgroundColor, primaryColor: actionSheet.primaryTextColor, secondaryColor: actionSheet.secondaryTextColor, accentColor: actionSheet.controlAccentColor, destructiveColor: actionSheet.destructiveActionTextColor, disabledColor: actionSheet.disabledActionTextColor, baseFontSize: presentationData.listsFontSize.baseDisplaySize) } } diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index 4c7361ef0c..0814168886 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -1,91 +1,258 @@ import Foundation import UIKit +import TelegramCore +import SyncCore import TelegramUIPreferences -private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: PresentationThemeBaseColor?, preview: Bool) -> PresentationTheme { - let destructiveColor: UIColor = UIColor(rgb: 0xeb5545) - let constructiveColor: UIColor = UIColor(rgb: 0x08a723) - let secretColor: UIColor = UIColor(rgb: 0x00b12c) +public let defaultDarkPresentationTheme = makeDefaultDarkPresentationTheme(preview: false) + +public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, editing: Bool, title: String?, accentColor: UIColor?, backgroundColors: (UIColor, UIColor?)?, bubbleColors: (UIColor, UIColor?)?, wallpaper forcedWallpaper: TelegramWallpaper? = nil) -> PresentationTheme { + if (theme.referenceTheme != .night) { + return theme + } + + var intro = theme.intro + var rootController = theme.rootController + var list = theme.list + var chatList = theme.chatList + var chat = theme.chat + var actionSheet = theme.actionSheet - let badgeFillColor: UIColor - let badgeTextColor: UIColor - let secondaryBadgeTextColor: UIColor - let outgoingBubbleFillColor: UIColor - let outgoingBubbleHighlightedFillColor: UIColor - let outgoingScamColor: UIColor - - let outgoingPrimaryTextColor: UIColor - let outgoingSecondaryTextColor: UIColor - let outgoingLinkTextColor: UIColor - let outgoingCheckColor: UIColor - - var accentColor = accentColor - - if accentColor.rgb == UIColor.white.rgb { - badgeFillColor = .white - badgeTextColor = .black - secondaryBadgeTextColor = .black - outgoingBubbleFillColor = UIColor(rgb: 0x313131) - outgoingBubbleHighlightedFillColor = UIColor(rgb: 0x464646) - outgoingScamColor = destructiveColor - - outgoingPrimaryTextColor = .white - outgoingSecondaryTextColor = UIColor(rgb: 0xffffff, alpha: 0.5) - outgoingLinkTextColor = .white - outgoingCheckColor = UIColor(rgb: 0xffffff, alpha: 0.5) - } else { - badgeFillColor = destructiveColor - badgeTextColor = .white - outgoingBubbleFillColor = accentColor - outgoingBubbleHighlightedFillColor = accentColor.withMultipliedBrightnessBy(1.421) - - let lightness = accentColor.lightness - if lightness > 0.7 { - outgoingScamColor = .black - - secondaryBadgeTextColor = .black - outgoingPrimaryTextColor = .black - outgoingSecondaryTextColor = UIColor(rgb: 0x000000, alpha: 0.5) - outgoingLinkTextColor = .black - outgoingCheckColor = UIColor(rgb: 0x000000, alpha: 0.5) + var bubbleColors = bubbleColors + var monochrome = false + if bubbleColors == nil, editing { + let accentColor = accentColor ?? UIColor(rgb: 0xffffff) + if accentColor.rgb == 0xffffff { + monochrome = true + bubbleColors = (UIColor(rgb: 0x313131), UIColor(rgb: 0x313131)) } else { - outgoingScamColor = .white - - secondaryBadgeTextColor = .white - outgoingPrimaryTextColor = .white - outgoingSecondaryTextColor = UIColor(rgb: 0xffffff, alpha: 0.5) - outgoingLinkTextColor = .white - outgoingCheckColor = UIColor(rgb: 0xffffff, alpha: 0.5) - - let hsv = accentColor.hsv - accentColor = UIColor(hue: hsv.0, saturation: hsv.1, brightness: max(hsv.2, 0.55), alpha: 1.0) + bubbleColors = (accentColor.withMultiplied(hue: 0.966, saturation: 0.61, brightness: 0.98), accentColor) } } + + var badgeFillColor: UIColor? + var badgeTextColor: UIColor? + var secondaryBadgeTextColor: UIColor? + + var accentColor = accentColor + if let initialAccentColor = accentColor { + if monochrome { + badgeFillColor = UIColor(rgb: 0xffffff) + badgeTextColor = UIColor(rgb: 0x000000) + secondaryBadgeTextColor = UIColor(rgb: 0x000000) + } else { + badgeFillColor = UIColor(rgb: 0xeb5545) + badgeTextColor = UIColor(rgb: 0xffffff) + if initialAccentColor.lightness > 0.7 { + secondaryBadgeTextColor = UIColor(rgb: 0x000000) + } else { + secondaryBadgeTextColor = UIColor(rgb: 0xffffff) + + let hsb = initialAccentColor.hsb + accentColor = UIColor(hue: hsb.0, saturation: hsb.1, brightness: max(hsb.2, 0.55), alpha: 1.0) + } + } + + intro = intro.withUpdated(accentTextColor: accentColor, startButtonColor: accentColor) + rootController = rootController.withUpdated( + tabBar: rootController.tabBar.withUpdated(selectedIconColor: accentColor, selectedTextColor: accentColor, badgeBackgroundColor: badgeFillColor, badgeTextColor: badgeTextColor), + navigationBar: rootController.navigationBar.withUpdated(buttonColor: accentColor, accentTextColor: accentColor, badgeBackgroundColor: badgeFillColor, badgeTextColor: badgeTextColor), + navigationSearchBar: rootController.navigationSearchBar.withUpdated(accentColor: accentColor) + ) + list = list.withUpdated( + itemAccentColor: accentColor, + itemCheckColors: list.itemCheckColors.withUpdated(fillColor: accentColor, foregroundColor: secondaryBadgeTextColor), + itemBarChart: list.itemBarChart.withUpdated(color1: accentColor) + ) + chatList = chatList.withUpdated( + checkmarkColor: accentColor, + unreadBadgeActiveBackgroundColor: accentColor, + unreadBadgeActiveTextColor: secondaryBadgeTextColor, + verifiedIconFillColor: accentColor, + verifiedIconForegroundColor: badgeTextColor + ) + actionSheet = actionSheet.withUpdated( + standardActionTextColor: accentColor, + controlAccentColor: accentColor, + checkContentColor: secondaryBadgeTextColor + ) + } + + var defaultWallpaper: TelegramWallpaper? + if let forcedWallpaper = forcedWallpaper { + defaultWallpaper = forcedWallpaper + } else if let backgroundColors = backgroundColors { + if let secondColor = backgroundColors.1 { + defaultWallpaper = .gradient(backgroundColors.0.argb, secondColor.argb, WallpaperSettings()) + } else { + defaultWallpaper = .color(backgroundColors.0.argb) + } + } + + var outgoingBubbleFillColor: UIColor? + var outgoingBubbleFillGradientColor: UIColor? + var outgoingBubbleHighlightedFillColor: UIColor? + var outgoingPrimaryTextColor: UIColor? + var outgoingSecondaryTextColor: UIColor? + var outgoingLinkTextColor: UIColor? + var outgoingScamColor: UIColor? + var outgoingCheckColor: UIColor? + + if let bubbleColors = bubbleColors { + var topBubbleColor = bubbleColors.0 + var bottomBubbleColor = bubbleColors.1 ?? bubbleColors.0 + if topBubbleColor.rgb != bottomBubbleColor.rgb { + let topBubbleColorLightness = topBubbleColor.lightness + let bottomBubbleColorLightness = bottomBubbleColor.lightness + if abs(topBubbleColorLightness - bottomBubbleColorLightness) > 0.7 { + if topBubbleColorLightness > bottomBubbleColorLightness { + topBubbleColor = topBubbleColor.withMultiplied(hue: 1.0, saturation: 1.0, brightness: 0.85) + } else { + bottomBubbleColor = bottomBubbleColor.withMultiplied(hue: 1.0, saturation: 1.0, brightness: 0.85) + } + } + } + + outgoingBubbleFillColor = topBubbleColor + outgoingBubbleFillGradientColor = bottomBubbleColor + + let lightnessColor = topBubbleColor.mixedWith(bottomBubbleColor, alpha: 0.5) + if lightnessColor.lightness > 0.7 { + outgoingPrimaryTextColor = UIColor(rgb: 0x000000) + outgoingSecondaryTextColor = UIColor(rgb: 0x000000, alpha: 0.5) + outgoingLinkTextColor = UIColor(rgb: 0x000000) + outgoingScamColor = UIColor(rgb: 0x000000) + outgoingCheckColor = UIColor(rgb: 0x000000, alpha: 0.5) + } else { + outgoingPrimaryTextColor = UIColor(rgb: 0xffffff) + outgoingSecondaryTextColor = UIColor(rgb: 0xffffff, alpha: 0.5) + outgoingLinkTextColor = UIColor(rgb: 0xffffff) + outgoingScamColor = UIColor(rgb: 0xffffff) + outgoingCheckColor = UIColor(rgb: 0xffffff) + } + } + + chat = chat.withUpdated( + defaultWallpaper: defaultWallpaper, + message: chat.message.withUpdated( + incoming: chat.message.incoming.withUpdated( + linkTextColor: accentColor, + linkHighlightColor: accentColor?.withAlphaComponent(0.5), + accentTextColor: accentColor, + accentControlColor: accentColor, + mediaActiveControlColor: accentColor, + mediaInactiveControlColor: accentColor?.withAlphaComponent(0.4), + fileTitleColor: accentColor, + polls: chat.message.incoming.polls.withUpdated( + radioProgress: accentColor, + highlight: accentColor?.withAlphaComponent(0.12), + bar: accentColor, + barIconForeground: accentColor.flatMap { accentColor -> UIColor in + if accentColor.rgb == 0xffffff { + return .clear + } else { + return .white + } + } + ), + textSelectionColor: accentColor?.withAlphaComponent(0.2), + textSelectionKnobColor: accentColor + ), + outgoing: chat.message.outgoing.withUpdated( + bubble: chat.message.outgoing.bubble.withUpdated( + withWallpaper: chat.message.outgoing.bubble.withWallpaper.withUpdated( + fill: outgoingBubbleFillColor, + gradientFill: outgoingBubbleFillGradientColor, + highlightedFill: outgoingBubbleFillColor?.withMultipliedBrightnessBy(1.421), + stroke: .clear + ), + withoutWallpaper: chat.message.outgoing.bubble.withoutWallpaper.withUpdated( + fill: outgoingBubbleFillColor, + gradientFill: outgoingBubbleFillGradientColor, + highlightedFill: outgoingBubbleFillColor?.withMultipliedBrightnessBy(1.421), + stroke: .clear + ) + ), + primaryTextColor: outgoingPrimaryTextColor, + secondaryTextColor: outgoingSecondaryTextColor, + linkTextColor: outgoingLinkTextColor, + scamColor: outgoingScamColor, + accentTextColor: outgoingPrimaryTextColor, + accentControlColor: outgoingPrimaryTextColor, + mediaActiveControlColor: outgoingPrimaryTextColor, + mediaInactiveControlColor: outgoingSecondaryTextColor, + mediaControlInnerBackgroundColor: outgoingBubbleFillColor, + pendingActivityColor: outgoingSecondaryTextColor, + fileTitleColor: outgoingPrimaryTextColor, + fileDescriptionColor: outgoingSecondaryTextColor, + fileDurationColor: outgoingSecondaryTextColor, + polls: chat.message.outgoing.polls.withUpdated(radioButton: outgoingPrimaryTextColor, radioProgress: outgoingPrimaryTextColor, highlight: outgoingPrimaryTextColor?.withAlphaComponent(0.12), separator: outgoingSecondaryTextColor, bar: outgoingPrimaryTextColor) + ), + infoLinkTextColor: accentColor, + outgoingCheckColor: outgoingCheckColor, + selectionControlColors: chat.message.selectionControlColors.withUpdated(fillColor: accentColor, foregroundColor: badgeTextColor) + ), + inputPanel: chat.inputPanel.withUpdated( + panelControlAccentColor: accentColor, + actionControlFillColor: accentColor, + actionControlForegroundColor: secondaryBadgeTextColor, + mediaRecordingControl: chat.inputPanel.mediaRecordingControl.withUpdated( + buttonColor: accentColor, + micLevelColor: accentColor?.withAlphaComponent(0.2), + activeIconColor: secondaryBadgeTextColor + ) + ), + historyNavigation: chat.historyNavigation.withUpdated( + badgeBackgroundColor: accentColor, + badgeStrokeColor: accentColor, + badgeTextColor: badgeTextColor + ) + ) + + return PresentationTheme( + name: title.flatMap { .custom($0) } ?? theme.name, + index: theme.index, + referenceTheme: theme.referenceTheme, + overallDarkAppearance: theme.overallDarkAppearance, + intro: intro, + passcode: theme.passcode, + rootController: rootController, + list: list, + chatList: chatList, + chat: chat, + actionSheet: actionSheet, + contextMenu: theme.contextMenu, + inAppNotification: theme.inAppNotification, + preview: theme.preview + ) +} + +public func makeDefaultDarkPresentationTheme(extendingThemeReference: PresentationThemeReference? = nil, preview: Bool) -> PresentationTheme { let rootTabBar = PresentationThemeRootTabBar( backgroundColor: UIColor(rgb: 0x1c1c1d), separatorColor: UIColor(rgb: 0x3d3d40), iconColor: UIColor(rgb: 0x828282), - selectedIconColor: accentColor, + selectedIconColor: UIColor(rgb: 0xffffff), textColor: UIColor(rgb: 0x828282), - selectedTextColor: accentColor, - badgeBackgroundColor: badgeFillColor, + selectedTextColor: UIColor(rgb: 0xffffff), + badgeBackgroundColor: UIColor(rgb: 0xffffff), badgeStrokeColor: UIColor(rgb: 0x1c1c1d), - badgeTextColor: badgeTextColor + badgeTextColor: UIColor(rgb: 0x000000) ) let rootNavigationBar = PresentationThemeRootNavigationBar( - buttonColor: accentColor, + buttonColor: UIColor(rgb: 0xffffff), disabledButtonColor: UIColor(rgb: 0x525252), - primaryTextColor: .white, + primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), controlColor: UIColor(rgb: 0x767676), - accentTextColor: accentColor, + accentTextColor: UIColor(rgb: 0xffffff), backgroundColor: UIColor(rgb: 0x1c1c1d), separatorColor: UIColor(rgb: 0x3d3d40), - badgeBackgroundColor: badgeFillColor, + badgeBackgroundColor: UIColor(rgb: 0xffffff), badgeStrokeColor: UIColor(rgb: 0x1c1c1d), - badgeTextColor: badgeTextColor, + badgeTextColor: UIColor(rgb: 0x000000), segmentedBackgroundColor: UIColor(rgb: 0x3a3b3d), segmentedForegroundColor: UIColor(rgb: 0x6f7075), segmentedTextColor: UIColor(rgb: 0xffffff), @@ -94,9 +261,9 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta let navigationSearchBar = PresentationThemeNavigationSearchBar( backgroundColor: UIColor(rgb: 0x1c1c1d), - accentColor: accentColor, + accentColor: UIColor(rgb: 0xffffff), inputFillColor: UIColor(rgb: 0x0f0f0f), - inputTextColor: .white, + inputTextColor: UIColor(rgb: 0xffffff), inputPlaceholderTextColor: UIColor(rgb: 0x8f8f8f), inputIconColor: UIColor(rgb: 0x8f8f8f), inputClearButtonColor: UIColor(rgb: 0x8f8f8f), @@ -105,10 +272,10 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta let intro = PresentationThemeIntro( statusBarStyle: .white, - primaryTextColor: .white, - accentTextColor: accentColor, + primaryTextColor: UIColor(rgb: 0xffffff), + accentTextColor: UIColor(rgb: 0xffffff), disabledTextColor: UIColor(rgb: 0x525252), - startButtonColor: accentColor, + startButtonColor: UIColor(rgb: 0xffffff), dotColor: UIColor(rgb: 0x5e5e5e) ) @@ -129,8 +296,8 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta frameColor: UIColor(rgb: 0x39393d), handleColor: UIColor(rgb: 0x121212), contentColor: UIColor(rgb: 0x67ce67), - positiveColor: constructiveColor, - negativeColor: destructiveColor + positiveColor: UIColor(rgb: 0x08a723), + negativeColor: UIColor(rgb: 0xeb5545) ) let list = PresentationThemeList( @@ -139,9 +306,9 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta itemPrimaryTextColor: UIColor(rgb: 0xffffff), itemSecondaryTextColor: UIColor(rgb: 0x98989e), itemDisabledTextColor: UIColor(rgb: 0x8f8f8f), - itemAccentColor: accentColor, + itemAccentColor: UIColor(rgb: 0xffffff), itemHighlightedColor: UIColor(rgb: 0x28b772), - itemDestructiveColor: destructiveColor, + itemDestructiveColor: UIColor(rgb: 0xeb5545), itemPlaceholderTextColor: UIColor(rgb: 0x4d4d4d), itemBlocksBackgroundColor: UIColor(rgb: 0x1c1c1d), itemHighlightedBackgroundColor: UIColor(rgb: 0x313135), @@ -155,32 +322,39 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta freeMonoIconColor: UIColor(rgb: 0x8d8e93), itemSwitchColors: switchColors, itemDisclosureActions: PresentationThemeItemDisclosureActions( - neutral1: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x666666), foregroundColor: .white), - neutral2: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xcd7800), foregroundColor: .white), - destructive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xc70c0c), foregroundColor: .white), - constructive: PresentationThemeFillForeground(fillColor: constructiveColor, foregroundColor: .white), - accent: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x666666), foregroundColor: .white), - warning: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xcd7800), foregroundColor: .white), - inactive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x666666), foregroundColor: .white) + neutral1: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x666666), foregroundColor: UIColor(rgb: 0xffffff)), + neutral2: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xcd7800), foregroundColor: UIColor(rgb: 0xffffff)), + destructive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xc70c0c), foregroundColor: UIColor(rgb: 0xffffff)), + constructive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x08a723), foregroundColor: UIColor(rgb: 0xffffff)), + accent: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x666666), foregroundColor: UIColor(rgb: 0xffffff)), + warning: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xcd7800), foregroundColor: UIColor(rgb: 0xffffff)), + inactive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x666666), foregroundColor: UIColor(rgb: 0xffffff)) ), itemCheckColors: PresentationThemeFillStrokeForeground( - fillColor: accentColor, + fillColor: UIColor(rgb: 0xffffff), strokeColor: UIColor(rgb: 0xffffff, alpha: 0.5), - foregroundColor: secondaryBadgeTextColor + foregroundColor: UIColor(rgb: 0x000000) ), controlSecondaryColor: UIColor(rgb: 0xffffff, alpha: 0.5), freeInputField: PresentationInputFieldTheme( backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.5), strokeColor: UIColor(rgb: 0xffffff, alpha: 0.5), placeholderColor: UIColor(rgb: 0x4d4d4d), - primaryColor: .white, + primaryColor: UIColor(rgb: 0xffffff), controlColor: UIColor(rgb: 0x4d4d4d) ), - mediaPlaceholderColor: UIColor(rgb: 0x1c1c1d), - scrollIndicatorColor: UIColor(white: 1.0, alpha: 0.3), + freePlainInputField: PresentationInputFieldTheme( + backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.5), + strokeColor: UIColor(rgb: 0xffffff, alpha: 0.5), + placeholderColor: UIColor(rgb: 0x4d4d4d), + primaryColor: UIColor(rgb: 0xffffff), + controlColor: UIColor(rgb: 0x4d4d4d) + ), + mediaPlaceholderColor: UIColor(rgb: 0xffffff).mixedWith(UIColor(rgb: 0x1c1c1d), alpha: 0.9), + scrollIndicatorColor: UIColor(rgb: 0xffffff, alpha: 0.3), pageIndicatorInactiveColor: UIColor(white: 1.0, alpha: 0.3), inputClearButtonColor: UIColor(rgb: 0x8b9197), - itemBarChart: PresentationThemeItemBarChart(color1: accentColor, color2: UIColor(rgb: 0x929196), color3: UIColor(rgb: 0x333333)) + itemBarChart: PresentationThemeItemBarChart(color1: UIColor(rgb: 0xffffff), color2: UIColor(rgb: 0x929196), color3: UIColor(rgb: 0x333333)) ) let chatList = PresentationThemeChatList( @@ -191,19 +365,19 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta itemHighlightedBackgroundColor: UIColor(rgb: 0x191919), itemSelectedBackgroundColor: UIColor(rgb: 0x191919), titleColor: UIColor(rgb: 0xffffff), - secretTitleColor: secretColor, - dateTextColor: UIColor(rgb: 0x8e8e92), + secretTitleColor: UIColor(rgb: 0x00b12c), + dateTextColor: UIColor(rgb: 0x8d8e93), authorNameColor: UIColor(rgb: 0xffffff), - messageTextColor: UIColor(rgb: 0x8e8e92), + messageTextColor: UIColor(rgb: 0x8d8e93), messageHighlightedTextColor: UIColor(rgb: 0xffffff), messageDraftTextColor: UIColor(rgb: 0xdd4b39), - checkmarkColor: accentColor, + checkmarkColor: UIColor(rgb: 0xffffff), pendingIndicatorColor: UIColor(rgb: 0xffffff), - failedFillColor: destructiveColor, - failedForegroundColor: .white, - muteIconColor: UIColor(rgb: 0x8e8e92), - unreadBadgeActiveBackgroundColor: accentColor, - unreadBadgeActiveTextColor: secondaryBadgeTextColor, + failedFillColor: UIColor(rgb: 0xeb5545), + failedForegroundColor: UIColor(rgb: 0xffffff), + muteIconColor: UIColor(rgb: 0x8d8e93), + unreadBadgeActiveBackgroundColor: UIColor(rgb: 0xffffff), + unreadBadgeActiveTextColor: UIColor(rgb: 0x000000), unreadBadgeInactiveBackgroundColor: UIColor(rgb: 0x666666), unreadBadgeInactiveTextColor:UIColor(rgb: 0x000000), pinnedBadgeColor: UIColor(rgb: 0x767677), @@ -211,76 +385,77 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta regularSearchBarColor: UIColor(rgb: 0x272728), sectionHeaderFillColor: UIColor(rgb: 0x1c1c1d), sectionHeaderTextColor: UIColor(rgb: 0xffffff), - verifiedIconFillColor: accentColor, - verifiedIconForegroundColor: badgeTextColor, - secretIconColor: secretColor, - pinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x72d5fd), bottomColor: UIColor(rgb: 0x2a9ef1)), foregroundColor: .white), - unpinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x666666), bottomColor: UIColor(rgb: 0x666666)), foregroundColor: .black), + verifiedIconFillColor: UIColor(rgb: 0xffffff), + verifiedIconForegroundColor: UIColor(rgb: 0x000000), + secretIconColor: UIColor(rgb: 0x00b12c), + pinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x72d5fd), bottomColor: UIColor(rgb: 0x2a9ef1)), foregroundColor: UIColor(rgb: 0xffffff)), + unpinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x666666), bottomColor: UIColor(rgb: 0x666666)), foregroundColor: UIColor(rgb: 0x000000)), onlineDotColor: UIColor(rgb: 0x4cc91f) ) let message = PresentationThemeChatMessage( - incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x262628), highlightedFill: UIColor(rgb: 0x353539), stroke: UIColor(rgb: 0x262628)), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x262628), highlightedFill: UIColor(rgb: 0x353539), stroke: UIColor(rgb: 0x262628))), primaryTextColor: .white, secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: accentColor, linkHighlightColor: accentColor.withAlphaComponent(0.5), scamColor: destructiveColor, textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: accentColor, accentControlColor: accentColor, mediaActiveControlColor: accentColor, mediaInactiveControlColor: accentColor.withAlphaComponent(0.4), mediaControlInnerBackgroundColor: UIColor(rgb: 0x262628), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: accentColor, fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x1f1f1f).mixedWith(.white, alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0x737373), radioProgress: accentColor, highlight: accentColor.withAlphaComponent(0.12), separator: UIColor(rgb: 0x000000), bar: accentColor), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: accentColor.withAlphaComponent(0.2), textSelectionKnobColor: accentColor), - outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColor, highlightedFill: outgoingBubbleHighlightedFillColor, stroke: outgoingBubbleFillColor), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColor, highlightedFill: outgoingBubbleHighlightedFillColor, stroke: outgoingBubbleFillColor)), primaryTextColor: outgoingPrimaryTextColor, secondaryTextColor: outgoingSecondaryTextColor, linkTextColor: outgoingLinkTextColor, linkHighlightColor: UIColor.white.withAlphaComponent(0.5), scamColor: outgoingScamColor, textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: outgoingPrimaryTextColor, accentControlColor: outgoingPrimaryTextColor, mediaActiveControlColor: outgoingPrimaryTextColor, mediaInactiveControlColor: outgoingSecondaryTextColor, mediaControlInnerBackgroundColor: outgoingBubbleFillColor, pendingActivityColor: outgoingSecondaryTextColor, fileTitleColor: outgoingPrimaryTextColor, fileDescriptionColor: outgoingSecondaryTextColor, fileDurationColor: outgoingSecondaryTextColor, mediaPlaceholderColor: UIColor(rgb: 0x313131).mixedWith(.white, alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: outgoingPrimaryTextColor, radioProgress: outgoingPrimaryTextColor, highlight: outgoingPrimaryTextColor.withAlphaComponent(0.12), separator: outgoingSecondaryTextColor, bar: outgoingPrimaryTextColor), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor.white.withAlphaComponent(0.2), textSelectionKnobColor: UIColor.white), - freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x1f1f1f), highlightedFill: UIColor(rgb: 0x2a2a2a), stroke: UIColor(rgb: 0x1f1f1f)), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x1f1f1f), highlightedFill: UIColor(rgb: 0x2a2a2a), stroke: UIColor(rgb: 0x1f1f1f))), - infoPrimaryTextColor: .white, - infoLinkTextColor: accentColor, - outgoingCheckColor: outgoingCheckColor, + incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x262628), highlightedFill: UIColor(rgb: 0x353539), stroke: UIColor(rgb: 0x262628), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x262628), highlightedFill: UIColor(rgb: 0x353539), stroke: UIColor(rgb: 0x262628), shadow: nil)), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.4), mediaControlInnerBackgroundColor: UIColor(rgb: 0x262628), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x1f1f1f).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0x737373), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff, alpha: 0.12), separator: UIColor(rgb: 0x000000), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)), + outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x313131), gradientFill: UIColor(rgb: 0x313131), highlightedFill: UIColor(rgb: 0x464646), stroke: UIColor(rgb: 0x313131), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x313131), gradientFill: UIColor(rgb: 0x313131), highlightedFill: UIColor(rgb: 0x464646), stroke: UIColor(rgb: 0x313131), shadow: nil)), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaControlInnerBackgroundColor: UIColor(rgb: 0x313131), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x313131).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff).withAlphaComponent(0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.5), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0xffffff), barNegative: UIColor(rgb: 0xffffff)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)), + freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x1f1f1f), highlightedFill: UIColor(rgb: 0x2a2a2a), stroke: UIColor(rgb: 0x1f1f1f), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x1f1f1f), highlightedFill: UIColor(rgb: 0x2a2a2a), stroke: UIColor(rgb: 0x1f1f1f), shadow: nil)), + infoPrimaryTextColor: UIColor(rgb: 0xffffff), + infoLinkTextColor: UIColor(rgb: 0xffffff), + outgoingCheckColor: UIColor(rgb: 0xffffff), mediaDateAndStatusFillColor: UIColor(white: 0.0, alpha: 0.5), - mediaDateAndStatusTextColor: .white, + mediaDateAndStatusTextColor: UIColor(rgb: 0xffffff), shareButtonFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), shareButtonStrokeColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xb2b2b2, alpha: 0.18), withoutWallpaper: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), shareButtonForegroundColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xb2b2b2), withoutWallpaper: UIColor(rgb: 0xb2b2b2)), - mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.6), foregroundColor: .white), - selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: accentColor, strokeColor: .white, foregroundColor: badgeTextColor), - deliveryFailedColors: PresentationThemeFillForeground(fillColor: destructiveColor, foregroundColor: .white), + mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.6), foregroundColor: UIColor(rgb: 0xffffff)), + selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: UIColor(rgb: 0xffffff), strokeColor: UIColor(rgb: 0xffffff), foregroundColor: UIColor(rgb: 0x000000)), + deliveryFailedColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xeb5545), foregroundColor: UIColor(rgb: 0xffffff)), mediaHighlightOverlayColor: UIColor(white: 1.0, alpha: 0.6) ) let serviceMessage = PresentationThemeServiceMessage( - components: PresentationThemeServiceMessageColor(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0x1f1f1f, alpha: 1.0), primaryText: UIColor(rgb: 0xffffff), linkHighlight: UIColor(rgb: 0xffffff, alpha: 0.12), scam: destructiveColor, dateFillStatic: UIColor(rgb: 0x000000, alpha: 0.6), dateFillFloating: UIColor(rgb: 0x000000, alpha: 0.2)), withCustomWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0x1f1f1f, alpha: 1.0), primaryText: .white, linkHighlight: UIColor(rgb: 0xffffff, alpha: 0.12), scam: destructiveColor, dateFillStatic: UIColor(rgb: 0x000000, alpha: 0.6), dateFillFloating: UIColor(rgb: 0x000000, alpha: 0.2))), + components: PresentationThemeServiceMessageColor(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0x1f1f1f, alpha: 1.0), primaryText: UIColor(rgb: 0xffffff), linkHighlight: UIColor(rgb: 0xffffff, alpha: 0.12), scam: UIColor(rgb: 0xeb5545), dateFillStatic: UIColor(rgb: 0x000000, alpha: 0.6), dateFillFloating: UIColor(rgb: 0x000000, alpha: 0.2)), withCustomWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0x1f1f1f, alpha: 1.0), primaryText: UIColor(rgb: 0xffffff), linkHighlight: UIColor(rgb: 0xffffff, alpha: 0.12), scam: UIColor(rgb: 0xeb5545), dateFillStatic: UIColor(rgb: 0x000000, alpha: 0.6), dateFillFloating: UIColor(rgb: 0x000000, alpha: 0.2))), unreadBarFillColor: UIColor(rgb: 0x1b1b1b), unreadBarStrokeColor: UIColor(rgb: 0x1b1b1b), - unreadBarTextColor: .white, - dateTextColor: PresentationThemeVariableColor(color: .white) + unreadBarTextColor: UIColor(rgb: 0xffffff), + dateTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)) ) let inputPanelMediaRecordingControl = PresentationThemeChatInputPanelMediaRecordingControl( - buttonColor: accentColor, - micLevelColor: accentColor.withAlphaComponent(0.2), - activeIconColor: .white + buttonColor: UIColor(rgb: 0xffffff), + micLevelColor: UIColor(rgb: 0xffffff, alpha: 0.2), + activeIconColor: UIColor(rgb: 0x000000) ) let inputPanel = PresentationThemeChatInputPanel( panelBackgroundColor: UIColor(rgb: 0x1c1c1d), + panelBackgroundColorNoWallpaper: UIColor(rgb: 0x000000), panelSeparatorColor: UIColor(rgb: 0x3d3d40), - panelControlAccentColor: accentColor, + panelControlAccentColor: UIColor(rgb: 0xffffff), panelControlColor: UIColor(rgb: 0x808080), panelControlDisabledColor: UIColor(rgb: 0x808080, alpha: 0.5), panelControlDestructiveColor: UIColor(rgb: 0xff3b30), inputBackgroundColor: UIColor(rgb: 0x060606), inputStrokeColor: UIColor(rgb: 0x353537), inputPlaceholderColor: UIColor(rgb: 0x7b7b7b), - inputTextColor: .white, + inputTextColor: UIColor(rgb: 0xffffff), inputControlColor: UIColor(rgb: 0x7b7b7b), - actionControlFillColor: accentColor, - actionControlForegroundColor: secondaryBadgeTextColor, - primaryTextColor: .white, + actionControlFillColor: UIColor(rgb: 0xffffff), + actionControlForegroundColor: UIColor(rgb: 0x000000), + primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), - mediaRecordingDotColor: destructiveColor, + mediaRecordingDotColor: UIColor(rgb: 0xeb5545), mediaRecordingControl: inputPanelMediaRecordingControl ) let inputMediaPanel = PresentationThemeInputMediaPanel( panelSeparatorColor: UIColor(rgb: 0x3d3d40), panelIconColor: UIColor(rgb: 0x808080), - panelHighlightedIconBackgroundColor: UIColor(rgb: 0x000000), + panelHighlightedIconBackgroundColor: UIColor(rgb: 0x262628), stickersBackgroundColor: UIColor(rgb: 0x000000), stickersSectionTextColor: UIColor(rgb: 0x7b7b7b), stickersSearchBackgroundColor: UIColor(rgb: 0x1c1c1d), - stickersSearchPlaceholderColor: UIColor(rgb: 0x8e8e92), - stickersSearchPrimaryColor: .white, - stickersSearchControlColor: UIColor(rgb: 0x8e8e92), + stickersSearchPlaceholderColor: UIColor(rgb: 0x8d8e93), + stickersSearchPrimaryColor: UIColor(rgb: 0xffffff), + stickersSearchControlColor: UIColor(rgb: 0x8d8e93), gifsBackgroundColor: UIColor(rgb: 0x000000) ) @@ -291,16 +466,16 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta buttonStrokeColor: UIColor(rgb: 0x0c0c0c), buttonHighlightedFillColor: UIColor(rgb: 0x5a5a5a, alpha: 0.7), buttonHighlightedStrokeColor: UIColor(rgb: 0x0c0c0c), - buttonTextColor: .white + buttonTextColor: UIColor(rgb: 0xffffff) ) let historyNavigation = PresentationThemeChatHistoryNavigation( fillColor: UIColor(rgb: 0x1c1c1d), strokeColor: UIColor(rgb: 0x3d3d40), - foregroundColor: .white, - badgeBackgroundColor: accentColor, - badgeStrokeColor: accentColor, - badgeTextColor: badgeTextColor + foregroundColor: UIColor(rgb: 0xffffff), + badgeBackgroundColor: UIColor(rgb: 0xffffff), + badgeStrokeColor: UIColor(rgb: 0xffffff), + badgeTextColor: UIColor(rgb: 0x000000) ) let chat = PresentationThemeChat( @@ -321,52 +496,52 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta opaqueItemHighlightedBackgroundColor: UIColor(white: 0.0, alpha: 1.0), itemHighlightedBackgroundColor: UIColor(rgb: 0x000000, alpha: 0.5), opaqueItemSeparatorColor: UIColor(rgb: 0x3d3d40), - standardActionTextColor: accentColor, - destructiveActionTextColor: destructiveColor, + standardActionTextColor: UIColor(rgb: 0xffffff), + destructiveActionTextColor: UIColor(rgb: 0xeb5545), disabledActionTextColor: UIColor(rgb: 0x4d4d4d), - primaryTextColor: .white, + primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0x5e5e5e), - controlAccentColor: accentColor, + controlAccentColor: UIColor(rgb: 0xffffff), inputBackgroundColor: UIColor(rgb: 0x0f0f0f), inputHollowBackgroundColor: UIColor(rgb: 0x0f0f0f), inputBorderColor: UIColor(rgb: 0x0f0f0f), inputPlaceholderColor: UIColor(rgb: 0x8f8f8f), - inputTextColor: .white, + inputTextColor: UIColor(rgb: 0xffffff), inputClearButtonColor: UIColor(rgb: 0x8f8f8f), - checkContentColor: secondaryBadgeTextColor + checkContentColor: UIColor(rgb: 0x000000) ) let contextMenu = PresentationThemeContextMenu( dimColor: UIColor(rgb: 0x000000, alpha: 0.6), backgroundColor: UIColor(rgb: 0x252525, alpha: 0.78), - itemSeparatorColor: UIColor(rgb: 0xFFFFFF, alpha: 0.15), + itemSeparatorColor: UIColor(rgb: 0xffffff, alpha: 0.15), sectionSeparatorColor: UIColor(rgb: 0x000000, alpha: 0.2), itemBackgroundColor: UIColor(rgb: 0x000000, alpha: 0.0), - itemHighlightedBackgroundColor: UIColor(rgb: 0xFFFFFF, alpha: 0.15), + itemHighlightedBackgroundColor: UIColor(rgb: 0xffffff, alpha: 0.15), primaryColor: UIColor(rgb: 0xffffff, alpha: 1.0), secondaryColor: UIColor(rgb: 0xffffff, alpha: 0.8), - destructiveColor: destructiveColor + destructiveColor: UIColor(rgb: 0xeb5545) ) let inAppNotification = PresentationThemeInAppNotification( fillColor: UIColor(rgb: 0x1c1c1d), - primaryTextColor: .white, + primaryTextColor: UIColor(rgb: 0xffffff), expandedNotification: PresentationThemeExpandedNotification( backgroundType: .dark, navigationBar: PresentationThemeExpandedNotificationNavigationBar( backgroundColor: UIColor(rgb: 0x1c1c1d), - primaryTextColor: .white, - controlColor: .white, + primaryTextColor: UIColor(rgb: 0xffffff), + controlColor: UIColor(rgb: 0xffffff), separatorColor: UIColor(rgb: 0x000000) ) ) ) return PresentationTheme( - name: .builtin(.night), + name: extendingThemeReference?.name ?? .builtin(.night), + index: extendingThemeReference?.index ?? PresentationThemeReference.builtin(.night).index, referenceTheme: .night, overallDarkAppearance: true, - baseColor: baseColor, intro: intro, passcode: passcode, rootController: rootController, @@ -379,10 +554,3 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta preview: preview ) } - -public let defaultDarkPresentationTheme = makeDarkPresentationTheme(accentColor: .white, baseColor: .white, preview: false) - -public func makeDarkPresentationTheme(accentColor: UIColor?, baseColor: PresentationThemeBaseColor?, preview: Bool) -> PresentationTheme { - let accentColor = accentColor ?? .white - return makeDarkPresentationTheme(accentColor: accentColor, baseColor: baseColor, preview: preview) -} diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift index eb591bd818..0781eba55a 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift @@ -1,15 +1,448 @@ import Foundation import UIKit +import TelegramCore +import SyncCore import TelegramUIPreferences -private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: PresentationThemeBaseColor?, preview: Bool) -> PresentationTheme { - let destructiveColor: UIColor = UIColor(rgb: 0xff6767) - let constructiveColor: UIColor = UIColor(rgb: 0x08a723) - let secretColor: UIColor = UIColor(rgb: 0x89df9e) +private let defaultDarkTintedAccentColor = UIColor(rgb: 0x2ea6ff) +public let defaultDarkTintedPresentationTheme = makeDefaultDarkTintedPresentationTheme(preview: false) + +public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme, editing: Bool, title: String?, accentColor: UIColor?, backgroundColors: (UIColor, UIColor?)?, bubbleColors: (UIColor, UIColor?)?, wallpaper forcedWallpaper: TelegramWallpaper? = nil) -> PresentationTheme { + if (theme.referenceTheme != .nightAccent) { + return theme + } var accentColor = accentColor - let hsv = accentColor.hsv - accentColor = UIColor(hue: hsv.0, saturation: hsv.1, brightness: max(hsv.2, 0.18), alpha: 1.0) + if accentColor == PresentationThemeBaseColor.blue.color { + accentColor = defaultDarkTintedAccentColor + } + + var intro = theme.intro + var passcode = theme.passcode + var rootController = theme.rootController + var list = theme.list + var chatList = theme.chatList + var chat = theme.chat + var actionSheet = theme.actionSheet + var contextMenu = theme.contextMenu + var inAppNotification = theme.inAppNotification + + + var mainBackgroundColor: UIColor? + var mainSelectionColor: UIColor? + var additionalBackgroundColor: UIColor? + var mainSeparatorColor: UIColor? + var mainForegroundColor: UIColor? + var mainSecondaryColor: UIColor? + var mainSecondaryTextColor: UIColor? + var mainFreeTextColor: UIColor? + var secondaryBadgeTextColor: UIColor + var mainInputColor: UIColor? + var inputBackgroundColor: UIColor? + var buttonStrokeColor: UIColor? + + var suggestedWallpaper: TelegramWallpaper? + + var bubbleColors = bubbleColors + if bubbleColors == nil, editing { + if let accentColor = accentColor { + let color = accentColor.withMultiplied(hue: 1.024, saturation: 0.573, brightness: 0.18) + suggestedWallpaper = .color(color.argb) + } + + let accentColor = accentColor ?? defaultDarkTintedAccentColor + let bottomColor = accentColor.withMultiplied(hue: 1.019, saturation: 0.731, brightness: 0.59) + let topColor = bottomColor.withMultiplied(hue: 0.966, saturation: 0.61, brightness: 0.98) + bubbleColors = (topColor, bottomColor) + } + + if let initialAccentColor = accentColor { + let hsb = initialAccentColor.hsb + accentColor = UIColor(hue: hsb.0, saturation: hsb.1, brightness: max(hsb.2, 0.18), alpha: 1.0) + + if let lightness = accentColor?.lightness, lightness > 0.7 { + secondaryBadgeTextColor = UIColor(rgb: 0x000000) + } else { + secondaryBadgeTextColor = UIColor(rgb: 0xffffff) + } + + mainBackgroundColor = accentColor?.withMultiplied(hue: 1.024, saturation: 0.585, brightness: 0.25) + mainSelectionColor = accentColor?.withMultiplied(hue: 1.03, saturation: 0.585, brightness: 0.12) + additionalBackgroundColor = accentColor?.withMultiplied(hue: 1.024, saturation: 0.573, brightness: 0.18) + mainSeparatorColor = accentColor?.withMultiplied(hue: 1.033, saturation: 0.426, brightness: 0.34) + mainForegroundColor = accentColor?.withMultiplied(hue: 0.99, saturation: 0.256, brightness: 0.62) + mainSecondaryColor = accentColor?.withMultiplied(hue: 1.019, saturation: 0.109, brightness: 0.59) + mainSecondaryTextColor = accentColor?.withMultiplied(hue: 0.956, saturation: 0.17, brightness: 1.0) + mainFreeTextColor = accentColor?.withMultiplied(hue: 1.019, saturation: 0.097, brightness: 0.56) + mainInputColor = accentColor?.withMultiplied(hue: 1.029, saturation: 0.609, brightness: 0.19) + inputBackgroundColor = accentColor?.withMultiplied(hue: 1.02, saturation: 0.609, brightness: 0.15) + buttonStrokeColor = accentColor?.withMultiplied(hue: 1.014, saturation: 0.56, brightness: 0.64).withAlphaComponent(0.15) + + intro = intro.withUpdated( + accentTextColor: accentColor, + disabledTextColor: accentColor?.withMultiplied(hue: 1.033, saturation: 0.219, brightness: 0.44), + startButtonColor: accentColor, + dotColor: mainSecondaryColor + ) + passcode = passcode.withUpdated(backgroundColors: passcode.backgroundColors.withUpdated(topColor: accentColor?.withMultiplied(hue: 1.049, saturation: 0.573, brightness: 0.47), bottomColor: additionalBackgroundColor), buttonColor: mainBackgroundColor) + rootController = rootController.withUpdated( + tabBar: rootController.tabBar.withUpdated( + backgroundColor: mainBackgroundColor, + separatorColor: mainSeparatorColor, + iconColor: mainForegroundColor, + selectedIconColor: accentColor, + textColor: mainForegroundColor, + selectedTextColor: accentColor + ), + navigationBar: rootController.navigationBar.withUpdated( + buttonColor: accentColor, + disabledButtonColor: accentColor?.withMultiplied(hue: 1.033, saturation: 0.219, brightness: 0.44), + secondaryTextColor: mainSecondaryColor, + controlColor: mainSecondaryColor, + accentTextColor: accentColor, + backgroundColor: mainBackgroundColor, + separatorColor: mainSeparatorColor, + segmentedBackgroundColor: mainInputColor, + segmentedForegroundColor: mainBackgroundColor, + segmentedDividerColor: mainSecondaryTextColor?.withAlphaComponent(0.5) + ), + navigationSearchBar: rootController.navigationSearchBar.withUpdated( + backgroundColor: mainBackgroundColor, + accentColor: accentColor, + inputFillColor: mainInputColor, + inputPlaceholderTextColor: mainSecondaryColor, + inputIconColor: mainSecondaryColor, + inputClearButtonColor: mainSecondaryColor, + separatorColor: additionalBackgroundColor + ) + ) + list = list.withUpdated( + blocksBackgroundColor: additionalBackgroundColor, + plainBackgroundColor: additionalBackgroundColor, + itemSecondaryTextColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + itemDisabledTextColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + itemAccentColor: accentColor, + itemPlaceholderTextColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + itemBlocksBackgroundColor: mainBackgroundColor, + itemHighlightedBackgroundColor: mainSelectionColor, + itemBlocksSeparatorColor: mainSeparatorColor, + itemPlainSeparatorColor: mainSeparatorColor, + disclosureArrowColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + sectionHeaderTextColor: mainFreeTextColor, + freeTextColor: mainFreeTextColor, + freeMonoIconColor: mainFreeTextColor, + itemSwitchColors: list.itemSwitchColors.withUpdated( + frameColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + contentColor: accentColor + ), + itemDisclosureActions: list.itemDisclosureActions.withUpdated( + neutral1: list.itemDisclosureActions.neutral1.withUpdated(fillColor: accentColor), + accent: list.itemDisclosureActions.accent.withUpdated(fillColor: accentColor), + inactive: list.itemDisclosureActions.inactive.withUpdated(fillColor: accentColor?.withMultiplied(hue: 1.029, saturation: 0.609, brightness: 0.3)) + ), + itemCheckColors: list.itemCheckColors.withUpdated( + fillColor: accentColor, + strokeColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + foregroundColor: secondaryBadgeTextColor + ), + controlSecondaryColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + freeInputField: list.freeInputField.withUpdated( + backgroundColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + strokeColor: mainSecondaryTextColor?.withAlphaComponent(0.5) + ), + freePlainInputField: list.freePlainInputField.withUpdated( + backgroundColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + strokeColor: mainSecondaryTextColor?.withAlphaComponent(0.5) + ), + mediaPlaceholderColor: UIColor(rgb: 0xffffff).mixedWith(mainBackgroundColor ?? list.itemBlocksBackgroundColor, alpha: 0.9), + pageIndicatorInactiveColor: mainSecondaryTextColor?.withAlphaComponent(0.4), + inputClearButtonColor: mainSecondaryColor, + itemBarChart: list.itemBarChart.withUpdated( + color1: accentColor, + color2: mainSecondaryTextColor?.withAlphaComponent(0.5), + color3: accentColor?.withMultiplied(hue: 1.038, saturation: 0.329, brightness: 0.33) + ) + ) + chatList = chatList.withUpdated( + backgroundColor: additionalBackgroundColor, + itemSeparatorColor: mainSeparatorColor, + itemBackgroundColor: additionalBackgroundColor, + pinnedItemBackgroundColor: mainBackgroundColor, + itemHighlightedBackgroundColor: mainSelectionColor, + itemSelectedBackgroundColor: mainSelectionColor, + dateTextColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + messageTextColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + checkmarkColor: accentColor, + pendingIndicatorColor: mainSecondaryTextColor?.withAlphaComponent(0.4), + muteIconColor: mainSecondaryTextColor?.withAlphaComponent(0.4), + unreadBadgeActiveBackgroundColor: accentColor, + unreadBadgeActiveTextColor: secondaryBadgeTextColor, + unreadBadgeInactiveBackgroundColor: mainSecondaryTextColor?.withAlphaComponent(0.4), + unreadBadgeInactiveTextColor: additionalBackgroundColor, + pinnedBadgeColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + pinnedSearchBarColor: mainInputColor, + regularSearchBarColor: accentColor?.withMultiplied(hue: 1.029, saturation: 0.609, brightness: 0.12), + sectionHeaderFillColor: mainBackgroundColor, + sectionHeaderTextColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + verifiedIconFillColor: accentColor, + unpinnedArchiveAvatarColor: chatList.unpinnedArchiveAvatarColor.withUpdated( + backgroundColors: chatList.unpinnedArchiveAvatarColor.backgroundColors.withUpdated( + topColor: accentColor?.withMultiplied(hue: 0.985, saturation: 0.268, brightness: 0.47), + bottomColor: accentColor?.withMultiplied(hue: 0.98, saturation: 0.268, brightness: 0.39) + ), + foregroundColor: additionalBackgroundColor + ) + ) + actionSheet = actionSheet.withUpdated( + opaqueItemBackgroundColor: mainBackgroundColor, + itemBackgroundColor: mainBackgroundColor?.withAlphaComponent(0.8), + opaqueItemHighlightedBackgroundColor: mainSelectionColor, + itemHighlightedBackgroundColor: mainSelectionColor?.withAlphaComponent(0.2), + opaqueItemSeparatorColor: additionalBackgroundColor, + standardActionTextColor: accentColor, + controlAccentColor: accentColor, + inputBackgroundColor: mainInputColor, + inputHollowBackgroundColor: mainInputColor, + inputBorderColor: mainInputColor, + inputPlaceholderColor: mainSecondaryColor, + inputClearButtonColor: mainSecondaryColor, + checkContentColor: secondaryBadgeTextColor + ) + contextMenu = contextMenu.withUpdated(backgroundColor: mainBackgroundColor?.withAlphaComponent(0.78)) + inAppNotification = inAppNotification.withUpdated( + fillColor: mainBackgroundColor, + expandedNotification: inAppNotification.expandedNotification.withUpdated(navigationBar: inAppNotification.expandedNotification.navigationBar.withUpdated( + backgroundColor: mainBackgroundColor, + controlColor: accentColor, + separatorColor: mainSeparatorColor) + ) + ) + } + + var defaultWallpaper: TelegramWallpaper? + if let forcedWallpaper = forcedWallpaper { + defaultWallpaper = forcedWallpaper + } else if let backgroundColors = backgroundColors { + if let secondColor = backgroundColors.1 { + defaultWallpaper = .gradient(backgroundColors.0.argb, secondColor.argb, WallpaperSettings()) + } else { + defaultWallpaper = .color(backgroundColors.0.argb) + } + } else if let forcedWallpaper = suggestedWallpaper { + defaultWallpaper = forcedWallpaper + } + + var outgoingBubbleFillColor: UIColor? + var outgoingBubbleFillGradientColor: UIColor? + var outgoingBubbleHighlightedFillColor: UIColor? + var outgoingPrimaryTextColor: UIColor? + var outgoingSecondaryTextColor: UIColor? + var outgoingLinkTextColor: UIColor? + var outgoingScamColor: UIColor? + var outgoingCheckColor: UIColor? + var highlightedIncomingBubbleColor: UIColor? + var highlightedOutgoingBubbleColor: UIColor? + + if let bubbleColors = bubbleColors { + outgoingBubbleFillColor = bubbleColors.0 + outgoingBubbleFillGradientColor = bubbleColors.1 ?? bubbleColors.0 + + let lightnessColor = bubbleColors.0.mixedWith(bubbleColors.1 ?? bubbleColors.0, alpha: 0.5) + if lightnessColor.lightness > 0.7 { + outgoingPrimaryTextColor = UIColor(rgb: 0x000000) + outgoingSecondaryTextColor = UIColor(rgb: 0x000000, alpha: 0.5) + outgoingLinkTextColor = UIColor(rgb: 0x000000) + outgoingScamColor = UIColor(rgb: 0x000000) + outgoingCheckColor = UIColor(rgb: 0x000000, alpha: 0.5) + } else { + outgoingPrimaryTextColor = UIColor(rgb: 0xffffff) + outgoingSecondaryTextColor = UIColor(rgb: 0xffffff, alpha: 0.5) + outgoingLinkTextColor = UIColor(rgb: 0xffffff) + outgoingScamColor = UIColor(rgb: 0xffffff) + outgoingCheckColor = UIColor(rgb: 0xffffff, alpha: 0.5) + } + + highlightedIncomingBubbleColor = accentColor?.withMultiplied(hue: 1.03, saturation: 0.463, brightness: 0.29) + highlightedOutgoingBubbleColor = outgoingBubbleFillColor?.withMultiplied(hue: 1.019, saturation: 0.609, brightness: 0.63) + } + + chat = chat.withUpdated( + defaultWallpaper: defaultWallpaper, + message: chat.message.withUpdated( + incoming: chat.message.incoming.withUpdated( + bubble: chat.message.incoming.bubble.withUpdated( + withWallpaper: chat.message.outgoing.bubble.withWallpaper.withUpdated( + fill: mainBackgroundColor, + gradientFill: mainBackgroundColor, + highlightedFill: highlightedIncomingBubbleColor, + stroke: mainBackgroundColor + ), + withoutWallpaper: chat.message.outgoing.bubble.withoutWallpaper.withUpdated( + fill: mainBackgroundColor, + gradientFill: mainBackgroundColor, + highlightedFill: highlightedIncomingBubbleColor, + stroke: mainBackgroundColor + ) + ), + secondaryTextColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + linkTextColor: accentColor, + linkHighlightColor: accentColor?.withAlphaComponent(0.5), + accentTextColor: accentColor, + accentControlColor: accentColor, + mediaActiveControlColor: accentColor, + mediaInactiveControlColor: accentColor?.withAlphaComponent(0.5), + mediaControlInnerBackgroundColor: mainBackgroundColor, + pendingActivityColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + fileTitleColor: accentColor, + fileDescriptionColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + fileDurationColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + mediaPlaceholderColor: accentColor?.withMultiplied(hue: 1.019, saturation: 0.585, brightness: 0.23), + polls: chat.message.incoming.polls.withUpdated( + radioButton: accentColor?.withMultiplied(hue: 0.995, saturation: 0.317, brightness: 0.51), + radioProgress: accentColor, + highlight: accentColor?.withAlphaComponent(0.12), + separator: mainSeparatorColor, + bar: accentColor + ), + actionButtonsFillColor: chat.message.incoming.actionButtonsFillColor.withUpdated( + withWallpaper: additionalBackgroundColor?.withAlphaComponent(0.5), + withoutWallpaper: additionalBackgroundColor?.withAlphaComponent(0.5) + ), + actionButtonsStrokeColor: buttonStrokeColor.flatMap { PresentationThemeVariableColor(color: $0) }, + textSelectionColor: accentColor?.withAlphaComponent(0.2), + textSelectionKnobColor: accentColor + ), + outgoing: chat.message.outgoing.withUpdated( + bubble: chat.message.outgoing.bubble.withUpdated( + withWallpaper: chat.message.outgoing.bubble.withWallpaper.withUpdated( + fill: outgoingBubbleFillColor, + gradientFill: outgoingBubbleFillGradientColor, + highlightedFill: highlightedOutgoingBubbleColor, + stroke: .clear + ), + withoutWallpaper: chat.message.outgoing.bubble.withoutWallpaper.withUpdated( + fill: outgoingBubbleFillColor, + gradientFill: outgoingBubbleFillGradientColor, + highlightedFill: highlightedOutgoingBubbleColor, + stroke: .clear + ) + ), + primaryTextColor: outgoingPrimaryTextColor, + secondaryTextColor: outgoingSecondaryTextColor, + linkTextColor: outgoingLinkTextColor, + scamColor: outgoingScamColor, + accentTextColor: outgoingPrimaryTextColor, + accentControlColor: outgoingPrimaryTextColor, + mediaActiveControlColor: outgoingPrimaryTextColor, + mediaInactiveControlColor: outgoingSecondaryTextColor, + mediaControlInnerBackgroundColor: outgoingBubbleFillColor, + pendingActivityColor: outgoingSecondaryTextColor, + fileTitleColor: outgoingPrimaryTextColor, + fileDescriptionColor: outgoingSecondaryTextColor, + fileDurationColor: outgoingSecondaryTextColor, + mediaPlaceholderColor: accentColor?.withMultiplied(hue: 1.019, saturation: 0.804, brightness: 0.51), + polls: chat.message.outgoing.polls.withUpdated(radioButton: outgoingPrimaryTextColor, radioProgress: outgoingPrimaryTextColor, highlight: outgoingPrimaryTextColor?.withAlphaComponent(0.12), separator: mainSeparatorColor, bar: outgoingPrimaryTextColor), + actionButtonsFillColor: chat.message.outgoing.actionButtonsFillColor.withUpdated(withWallpaper: additionalBackgroundColor?.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor?.withAlphaComponent(0.5)), + actionButtonsStrokeColor: buttonStrokeColor.flatMap { PresentationThemeVariableColor(color: $0) } + ), + freeform: chat.message.freeform.withUpdated( + withWallpaper: chat.message.freeform.withWallpaper.withUpdated( + fill: mainBackgroundColor, + highlightedFill: highlightedIncomingBubbleColor, + stroke: mainBackgroundColor + ), withoutWallpaper: chat.message.freeform.withoutWallpaper.withUpdated( + fill: mainBackgroundColor, + highlightedFill: highlightedIncomingBubbleColor, + stroke: mainBackgroundColor + ) + ), + infoLinkTextColor: accentColor, + outgoingCheckColor: outgoingCheckColor, + shareButtonFillColor: additionalBackgroundColor.flatMap { PresentationThemeVariableColor(color: $0.withAlphaComponent(0.5)) }, + shareButtonStrokeColor: buttonStrokeColor.flatMap { PresentationThemeVariableColor(color: $0) }, + selectionControlColors: chat.message.selectionControlColors.withUpdated(fillColor: accentColor) + ), + + serviceMessage: chat.serviceMessage.withUpdated( + components: chat.serviceMessage.components.withUpdated( + withDefaultWallpaper: chat.serviceMessage.components.withDefaultWallpaper.withUpdated( + fill: additionalBackgroundColor, + dateFillStatic: additionalBackgroundColor?.withAlphaComponent(0.6), + dateFillFloating: additionalBackgroundColor?.withAlphaComponent(0.2) + ), + withCustomWallpaper: chat.serviceMessage.components.withCustomWallpaper.withUpdated( + fill: additionalBackgroundColor, + dateFillStatic: additionalBackgroundColor?.withAlphaComponent(0.6), + dateFillFloating: additionalBackgroundColor?.withAlphaComponent(0.2) + ) + ), + unreadBarFillColor: mainBackgroundColor, + unreadBarStrokeColor: mainBackgroundColor + ), + inputPanel: chat.inputPanel.withUpdated( + panelBackgroundColor: mainBackgroundColor, + panelSeparatorColor: mainSeparatorColor, + panelControlAccentColor: accentColor, + panelControlColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + inputBackgroundColor: inputBackgroundColor, + inputStrokeColor: accentColor?.withMultiplied(hue: 1.038, saturation: 0.463, brightness: 0.26), + inputPlaceholderColor: mainSecondaryTextColor?.withAlphaComponent(0.4), + inputControlColor: mainSecondaryTextColor?.withAlphaComponent(0.4), + actionControlFillColor: accentColor, + mediaRecordingDotColor: accentColor, + mediaRecordingControl: chat.inputPanel.mediaRecordingControl.withUpdated( + buttonColor: accentColor, + micLevelColor: accentColor?.withAlphaComponent(0.2) + ) + ), + inputMediaPanel: chat.inputMediaPanel.withUpdated( + panelSeparatorColor: mainBackgroundColor, + panelIconColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + panelHighlightedIconBackgroundColor: inputBackgroundColor, + stickersBackgroundColor: additionalBackgroundColor, + stickersSectionTextColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + stickersSearchBackgroundColor: accentColor?.withMultiplied(hue: 1.009, saturation: 0.621, brightness: 0.15), + stickersSearchPlaceholderColor: accentColor?.withMultiplied(hue: 0.99, saturation: 0.243, brightness: 0.59), + stickersSearchControlColor: accentColor?.withMultiplied(hue: 0.99, saturation: 0.243, brightness: 0.59), + gifsBackgroundColor: additionalBackgroundColor + ), + inputButtonPanel: chat.inputButtonPanel.withUpdated( + panelSeparatorColor: mainBackgroundColor, + panelBackgroundColor: accentColor?.withMultiplied(hue: 1.048, saturation: 0.378, brightness: 0.13), + buttonFillColor: accentColor?.withMultiplied(hue: 1.0, saturation: 0.085, brightness: 0.38), + buttonStrokeColor: accentColor?.withMultiplied(hue: 1.019, saturation: 0.39, brightness: 0.07), + buttonHighlightedFillColor: accentColor?.withMultiplied(hue: 1.0, saturation: 0.085, brightness: 0.38).withAlphaComponent(0.7), + buttonHighlightedStrokeColor: accentColor?.withMultiplied(hue: 1.019, saturation: 0.39, brightness: 0.07) + ), + historyNavigation: chat.historyNavigation.withUpdated( + fillColor: mainBackgroundColor, + strokeColor: mainSeparatorColor, + foregroundColor: mainSecondaryTextColor?.withAlphaComponent(0.5), + badgeBackgroundColor: accentColor, + badgeStrokeColor: accentColor + ) + ) + + return PresentationTheme( + name: title.flatMap { .custom($0) } ?? theme.name, + index: theme.index, + referenceTheme: theme.referenceTheme, + overallDarkAppearance: theme.overallDarkAppearance, + intro: intro, + passcode: passcode, + rootController: rootController, + list: list, + chatList: chatList, + chat: chat, + actionSheet: actionSheet, + contextMenu: contextMenu, + inAppNotification: inAppNotification, + preview: theme.preview + ) +} + +public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: PresentationThemeReference? = nil, preview: Bool) -> PresentationTheme { + let accentColor = defaultDarkTintedAccentColor let secondaryBadgeTextColor: UIColor let lightness = accentColor.lightness @@ -22,16 +455,21 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta let mainBackgroundColor = accentColor.withMultiplied(hue: 1.024, saturation: 0.585, brightness: 0.25) let mainSelectionColor = accentColor.withMultiplied(hue: 1.03, saturation: 0.585, brightness: 0.12) let additionalBackgroundColor = accentColor.withMultiplied(hue: 1.024, saturation: 0.573, brightness: 0.18) - let mainSeparatorColor = accentColor.withMultiplied(hue: 1.033, saturation: 0.426, brightness: 0.34) let mainForegroundColor = accentColor.withMultiplied(hue: 0.99, saturation: 0.256, brightness: 0.62) - let mainSecondaryColor = accentColor.withMultiplied(hue: 1.019, saturation: 0.109, brightness: 0.59) let mainSecondaryTextColor = accentColor.withMultiplied(hue: 0.956, saturation: 0.17, brightness: 1.0) - let mainFreeTextColor = accentColor.withMultiplied(hue: 1.019, saturation: 0.097, brightness: 0.56) - let outgoingBubbleColor = accentColor.withMultiplied(hue: 1.019, saturation: 0.731, brightness: 0.59) + let outgoingBubbleFillGradientColor = accentColor.withMultiplied(hue: 1.019, saturation: 0.731, brightness: 0.59) + let outgoingBubbleFillColor = outgoingBubbleFillGradientColor.withMultiplied(hue: 0.966, saturation: 0.61, brightness: 0.98) + let outgoingBubbleHighlightedFillColor: UIColor + let outgoingScamColor = UIColor(rgb: 0xffffff) + let outgoingPrimaryTextColor = UIColor(rgb: 0xffffff) + let outgoingSecondaryTextColor = UIColor(rgb: 0xffffff, alpha: 0.5) + let outgoingLinkTextColor = UIColor(rgb: 0xffffff) + let outgoingCheckColor = UIColor(rgb: 0xffffff, alpha: 0.5) + let highlightedIncomingBubbleColor = accentColor.withMultiplied(hue: 1.03, saturation: 0.463, brightness: 0.29) let highlightedOutgoingBubbleColor = accentColor.withMultiplied(hue: 1.019, saturation: 0.609, brightness: 0.63) @@ -105,8 +543,8 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta frameColor: mainSecondaryTextColor.withAlphaComponent(0.5), handleColor: UIColor(rgb: 0x121212), contentColor: accentColor, - positiveColor: constructiveColor, - negativeColor: destructiveColor + positiveColor: UIColor(rgb: 0x08a723), + negativeColor: UIColor(rgb: 0xff6767) ) let list = PresentationThemeList( @@ -117,7 +555,7 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta itemDisabledTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), itemAccentColor: accentColor, itemHighlightedColor: UIColor(rgb: 0x28b772), - itemDestructiveColor: destructiveColor, + itemDestructiveColor: UIColor(rgb: 0xff6767), itemPlaceholderTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), itemBlocksBackgroundColor: mainBackgroundColor, itemHighlightedBackgroundColor: mainSelectionColor, @@ -126,7 +564,7 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta disclosureArrowColor: mainSecondaryTextColor.withAlphaComponent(0.5), sectionHeaderTextColor: mainFreeTextColor, freeTextColor: mainFreeTextColor, - freeTextErrorColor: destructiveColor, + freeTextErrorColor: UIColor(rgb: 0xff6767), freeTextSuccessColor: UIColor(rgb: 0x30cf30), freeMonoIconColor: mainFreeTextColor, itemSwitchColors: switchColors, @@ -134,7 +572,7 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta neutral1: PresentationThemeFillForeground(fillColor: accentColor, foregroundColor: .white), neutral2: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xcd7800), foregroundColor: .white), destructive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xc70c0c), foregroundColor: .white), - constructive: PresentationThemeFillForeground(fillColor: constructiveColor, foregroundColor: .white), + constructive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x08a723), foregroundColor: .white), accent: PresentationThemeFillForeground(fillColor: accentColor, foregroundColor: .white), warning: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xcd7800), foregroundColor: .white), inactive: PresentationThemeFillForeground(fillColor: accentColor.withMultiplied(hue: 1.029, saturation: 0.609, brightness: 0.3), foregroundColor: .white) @@ -152,7 +590,14 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta primaryColor: .white, controlColor: UIColor(rgb: 0x4d4d4d) ), - mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.585, brightness: 0.23), + freePlainInputField: PresentationInputFieldTheme( + backgroundColor: mainSecondaryTextColor.withAlphaComponent(0.5), + strokeColor: mainSecondaryTextColor.withAlphaComponent(0.5), + placeholderColor: UIColor(rgb: 0x4d4d4d), + primaryColor: .white, + controlColor: UIColor(rgb: 0x4d4d4d) + ), + mediaPlaceholderColor: UIColor(rgb: 0xffffff).mixedWith(mainBackgroundColor, alpha: 0.9), scrollIndicatorColor: UIColor(white: 1.0, alpha: 0.3), pageIndicatorInactiveColor: mainSecondaryTextColor.withAlphaComponent(0.4), inputClearButtonColor: mainSecondaryColor, @@ -167,7 +612,7 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta itemHighlightedBackgroundColor: mainSelectionColor, itemSelectedBackgroundColor: mainSelectionColor, titleColor: UIColor(rgb: 0xffffff), - secretTitleColor: secretColor, + secretTitleColor: UIColor(rgb: 0x89df9e), dateTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), authorNameColor: UIColor(rgb: 0xffffff), messageTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), @@ -175,7 +620,7 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta messageDraftTextColor: UIColor(rgb: 0xdd4b39), checkmarkColor: accentColor, pendingIndicatorColor: mainSecondaryTextColor.withAlphaComponent(0.4), - failedFillColor: destructiveColor, + failedFillColor: UIColor(rgb: 0xff6767), failedForegroundColor: .white, muteIconColor: mainSecondaryTextColor.withAlphaComponent(0.4), unreadBadgeActiveBackgroundColor: accentColor, @@ -189,7 +634,7 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta sectionHeaderTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), verifiedIconFillColor: accentColor, verifiedIconForegroundColor: .white, - secretIconColor: secretColor, + secretIconColor: UIColor(rgb: 0x89df9e), pinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x72d5fd), bottomColor: UIColor(rgb: 0x2a9ef1)), foregroundColor: .white), unpinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: accentColor.withMultiplied(hue: 0.985, saturation: 0.268, brightness: 0.47), bottomColor: accentColor.withMultiplied(hue: 0.98, saturation: 0.268, brightness: 0.39)), foregroundColor: additionalBackgroundColor), onlineDotColor: UIColor(rgb: 0x4cc91f) @@ -198,25 +643,25 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta let buttonStrokeColor = accentColor.withMultiplied(hue: 1.014, saturation: 0.56, brightness: 0.64).withAlphaComponent(0.15) let message = PresentationThemeChatMessage( - incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor)), primaryTextColor: .white, secondaryTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), linkTextColor: accentColor, linkHighlightColor: accentColor.withAlphaComponent(0.5), scamColor: destructiveColor, textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: accentColor, accentControlColor: accentColor, mediaActiveControlColor: accentColor, mediaInactiveControlColor: accentColor.withAlphaComponent(0.5), mediaControlInnerBackgroundColor: mainBackgroundColor, pendingActivityColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileTitleColor: accentColor, fileDescriptionColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileDurationColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.585, brightness: 0.23), polls: PresentationThemeChatBubblePolls(radioButton: accentColor.withMultiplied(hue: 0.995, saturation: 0.317, brightness: 0.51), radioProgress: accentColor, highlight: accentColor.withAlphaComponent(0.12), separator: mainSeparatorColor, bar: accentColor), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: accentColor.withAlphaComponent(0.2), textSelectionKnobColor: accentColor), - outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleColor, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleColor), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleColor, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleColor)), primaryTextColor: .white, secondaryTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), linkTextColor: accentColor, linkHighlightColor: accentColor.withAlphaComponent(0.5), scamColor: destructiveColor, textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: .white, accentControlColor: .white, mediaActiveControlColor: .white, mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaControlInnerBackgroundColor: outgoingBubbleColor, pendingActivityColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileTitleColor: .white, fileDescriptionColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileDurationColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.804, brightness: 0.51), polls: PresentationThemeChatBubblePolls(radioButton: .white, radioProgress: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0), highlight: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0).withAlphaComponent(0.12), separator: mainSeparatorColor, bar: .white), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: UIColor.white.withAlphaComponent(0.2), textSelectionKnobColor: UIColor.white), - freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor)), + incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor, shadow: nil)), primaryTextColor: .white, secondaryTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), linkTextColor: accentColor, linkHighlightColor: accentColor.withAlphaComponent(0.5), scamColor: UIColor(rgb: 0xff6767), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: accentColor, accentControlColor: accentColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: accentColor, mediaInactiveControlColor: accentColor.withAlphaComponent(0.5), mediaControlInnerBackgroundColor: mainBackgroundColor, pendingActivityColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileTitleColor: accentColor, fileDescriptionColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileDurationColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.585, brightness: 0.23), polls: PresentationThemeChatBubblePolls(radioButton: accentColor.withMultiplied(hue: 0.995, saturation: 0.317, brightness: 0.51), radioProgress: accentColor, highlight: accentColor.withAlphaComponent(0.12), separator: mainSeparatorColor, bar: accentColor, barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: accentColor.withAlphaComponent(0.2), textSelectionKnobColor: accentColor), + outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColor, gradientFill: outgoingBubbleFillGradientColor, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleFillColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColor, gradientFill: outgoingBubbleFillGradientColor, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleFillColor, shadow: nil)), primaryTextColor: outgoingPrimaryTextColor, secondaryTextColor: outgoingSecondaryTextColor, linkTextColor: outgoingLinkTextColor, linkHighlightColor: UIColor.white.withAlphaComponent(0.5), scamColor: outgoingScamColor, textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: outgoingPrimaryTextColor, accentControlColor: outgoingPrimaryTextColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: outgoingPrimaryTextColor, mediaInactiveControlColor: outgoingSecondaryTextColor, mediaControlInnerBackgroundColor: outgoingBubbleFillColor, pendingActivityColor: outgoingSecondaryTextColor, fileTitleColor: outgoingPrimaryTextColor, fileDescriptionColor: outgoingSecondaryTextColor, fileDurationColor: outgoingSecondaryTextColor, mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.804, brightness: 0.51), polls: PresentationThemeChatBubblePolls(radioButton: outgoingPrimaryTextColor, radioProgress: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0), highlight: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0).withAlphaComponent(0.12), separator: mainSeparatorColor, bar: outgoingPrimaryTextColor, barIconForeground: .clear, barPositive: outgoingPrimaryTextColor, barNegative: outgoingPrimaryTextColor), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: UIColor.white.withAlphaComponent(0.2), textSelectionKnobColor: UIColor.white), + freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: mainBackgroundColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor, shadow: nil)), infoPrimaryTextColor: UIColor(rgb: 0xffffff), infoLinkTextColor: accentColor, - outgoingCheckColor: accentColor.withMultiplied(hue: 0.99, saturation: 0.743, brightness: 1.0), + outgoingCheckColor: outgoingCheckColor, mediaDateAndStatusFillColor: UIColor(white: 0.0, alpha: 0.5), - mediaDateAndStatusTextColor: .white, + mediaDateAndStatusTextColor: UIColor(rgb: 0xffffff), shareButtonFillColor: PresentationThemeVariableColor(color: additionalBackgroundColor.withAlphaComponent(0.5)), shareButtonStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), shareButtonForegroundColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2)), mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.6), foregroundColor: .white), selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: accentColor, strokeColor: .white, foregroundColor: .white), - deliveryFailedColors: PresentationThemeFillForeground(fillColor: destructiveColor, foregroundColor: .white), + deliveryFailedColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff6767), foregroundColor: .white), mediaHighlightOverlayColor: UIColor(white: 1.0, alpha: 0.6) ) let serviceMessage = PresentationThemeServiceMessage( - components: PresentationThemeServiceMessageColor(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents(fill: additionalBackgroundColor, primaryText: .white, linkHighlight: UIColor(rgb: 0xffffff, alpha: 0.12), scam: destructiveColor, dateFillStatic: additionalBackgroundColor.withAlphaComponent(0.6), dateFillFloating: additionalBackgroundColor.withAlphaComponent(0.2)), withCustomWallpaper: PresentationThemeServiceMessageColorComponents(fill: additionalBackgroundColor, primaryText: .white, linkHighlight: UIColor(rgb: 0xffffff, alpha: 0.12), scam: destructiveColor, dateFillStatic: additionalBackgroundColor.withAlphaComponent(0.6), dateFillFloating: additionalBackgroundColor.withAlphaComponent(0.2))), + components: PresentationThemeServiceMessageColor(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents(fill: additionalBackgroundColor, primaryText: .white, linkHighlight: UIColor(rgb: 0xffffff, alpha: 0.12), scam: UIColor(rgb: 0xff6767), dateFillStatic: additionalBackgroundColor.withAlphaComponent(0.6), dateFillFloating: additionalBackgroundColor.withAlphaComponent(0.2)), withCustomWallpaper: PresentationThemeServiceMessageColorComponents(fill: additionalBackgroundColor, primaryText: .white, linkHighlight: UIColor(rgb: 0xffffff, alpha: 0.12), scam: UIColor(rgb: 0xff6767), dateFillStatic: additionalBackgroundColor.withAlphaComponent(0.6), dateFillFloating: additionalBackgroundColor.withAlphaComponent(0.2))), unreadBarFillColor: mainBackgroundColor, unreadBarStrokeColor: mainBackgroundColor, unreadBarTextColor: .white, @@ -231,11 +676,12 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta let inputPanel = PresentationThemeChatInputPanel( panelBackgroundColor: mainBackgroundColor, + panelBackgroundColorNoWallpaper: accentColor.withMultiplied(hue: 1.024, saturation: 0.573, brightness: 0.18), panelSeparatorColor: mainSeparatorColor, panelControlAccentColor: accentColor, panelControlColor: mainSecondaryTextColor.withAlphaComponent(0.5), panelControlDisabledColor: UIColor(rgb: 0x90979F, alpha: 0.5), - panelControlDestructiveColor: destructiveColor, + panelControlDestructiveColor: UIColor(rgb: 0xff6767), inputBackgroundColor: inputBackgroundColor, inputStrokeColor: accentColor.withMultiplied(hue: 1.038, saturation: 0.463, brightness: 0.26), inputPlaceholderColor: mainSecondaryTextColor.withAlphaComponent(0.4), @@ -282,7 +728,7 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta ) let chat = PresentationThemeChat( - defaultWallpaper: .color(Int32(bitPattern: accentColor.withMultiplied(hue: 1.024, saturation: 0.573, brightness: 0.18).rgb)), + defaultWallpaper: .color(accentColor.withMultiplied(hue: 1.024, saturation: 0.573, brightness: 0.18).argb), message: message, serviceMessage: serviceMessage, inputPanel: inputPanel, @@ -300,7 +746,7 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta itemHighlightedBackgroundColor: mainSelectionColor.withAlphaComponent(0.2), opaqueItemSeparatorColor: additionalBackgroundColor, standardActionTextColor: accentColor, - destructiveActionTextColor: destructiveColor, + destructiveActionTextColor: UIColor(rgb: 0xff6767), disabledActionTextColor: UIColor(white: 1.0, alpha: 0.5), primaryTextColor: .white, secondaryTextColor: UIColor(white: 1.0, alpha: 0.5), @@ -317,13 +763,13 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta let contextMenu = PresentationThemeContextMenu( dimColor: UIColor(rgb: 0x000000, alpha: 0.6), backgroundColor: rootNavigationBar.backgroundColor.withAlphaComponent(0.78), - itemSeparatorColor: UIColor(rgb: 0xFFFFFF, alpha: 0.15), + itemSeparatorColor: UIColor(rgb: 0xffffff, alpha: 0.15), sectionSeparatorColor: UIColor(rgb: 0x000000, alpha: 0.2), itemBackgroundColor: UIColor(rgb: 0x000000, alpha: 0.0), - itemHighlightedBackgroundColor: UIColor(rgb: 0xFFFFFF, alpha: 0.15), + itemHighlightedBackgroundColor: UIColor(rgb: 0xffffff, alpha: 0.15), primaryColor: UIColor(rgb: 0xffffff, alpha: 1.0), secondaryColor: UIColor(rgb: 0xffffff, alpha: 0.8), - destructiveColor: destructiveColor + destructiveColor: UIColor(rgb: 0xff6767) ) let inAppNotification = PresentationThemeInAppNotification( @@ -341,10 +787,10 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta ) return PresentationTheme( - name: .builtin(.nightAccent), + name: extendingThemeReference?.name ?? .builtin(.nightAccent), + index: extendingThemeReference?.index ?? PresentationThemeReference.builtin(.nightAccent).index, referenceTheme: .nightAccent, overallDarkAppearance: true, - baseColor: baseColor, intro: intro, passcode: passcode, rootController: rootController, @@ -357,14 +803,3 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta preview: preview ) } - -public let defaultDarkAccentColor = UIColor(rgb: 0x2ea6ff) -public let defaultDarkAccentPresentationTheme = makeDarkAccentPresentationTheme(accentColor: UIColor(rgb: 0x2ea6ff), baseColor: .blue, preview: false) - -public func makeDarkAccentPresentationTheme(accentColor: UIColor?, baseColor: PresentationThemeBaseColor?, preview: Bool) -> PresentationTheme { - var accentColor = accentColor ?? defaultDarkAccentColor - if accentColor == PresentationThemeBaseColor.blue.color { - accentColor = defaultDarkAccentColor - } - return makeDarkPresentationTheme(accentColor: accentColor, baseColor: baseColor, preview: preview) -} diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index 94dfd4913c..d18947cb4d 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -4,93 +4,325 @@ import TelegramCore import SyncCore import TelegramUIPreferences -private func makeDefaultDayPresentationTheme(accentColor: UIColor, serviceBackgroundColor: UIColor, baseColor: PresentationThemeBaseColor?, day: Bool, preview: Bool) -> PresentationTheme { - let destructiveColor: UIColor = UIColor(rgb: 0xff3b30) - let constructiveColor: UIColor = UIColor(rgb: 0x00c900) - let secretColor: UIColor = UIColor(rgb: 0x00b12c) - - var accentColor = accentColor - - let outgoingPrimaryTextColor: UIColor - let outgoingSecondaryTextColor: UIColor - let outgoingLinkTextColor: UIColor - let outgoingCheckColor: UIColor - let outgoingControlColor: UIColor - let outgoingBubbleFillColor: UIColor - let outgoingBubbleStrokeColor: UIColor - let outgoingSelectionBaseColor: UIColor - if accentColor.lightness > 0.705 { - outgoingPrimaryTextColor = .black - outgoingSecondaryTextColor = UIColor(rgb: 0x000000, alpha: 0.55) - outgoingLinkTextColor = .black - outgoingCheckColor = .black - outgoingControlColor = outgoingPrimaryTextColor - outgoingBubbleFillColor = accentColor - if outgoingBubbleFillColor.distance(to: UIColor.white) < 200 { - outgoingBubbleStrokeColor = UIColor(rgb: 0xc8c7cc) - } else { - outgoingBubbleStrokeColor = outgoingBubbleFillColor - } - - let hsv = accentColor.hsv - accentColor = UIColor(hue: hsv.0, saturation: min(1.0, hsv.1 * 1.1), brightness: min(hsv.2, 0.6), alpha: 1.0) - outgoingSelectionBaseColor = accentColor - } else { - outgoingPrimaryTextColor = .white - outgoingSecondaryTextColor = UIColor(rgb: 0xffffff, alpha: 0.65) - outgoingLinkTextColor = .white - outgoingCheckColor = .white - outgoingControlColor = outgoingPrimaryTextColor - outgoingBubbleFillColor = accentColor - outgoingBubbleStrokeColor = outgoingBubbleFillColor - outgoingSelectionBaseColor = .white +public let defaultServiceBackgroundColor = UIColor(rgb: 0x000000, alpha: 0.3) +public let defaultPresentationTheme = makeDefaultDayPresentationTheme(serviceBackgroundColor: defaultServiceBackgroundColor, day: false, preview: false) +public let defaultDayAccentColor = UIColor(rgb: 0x007ee5) + +public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, title: String?, accentColor: UIColor?, backgroundColors: (UIColor, UIColor?)?, bubbleColors: (UIColor, UIColor?)?, wallpaper forcedWallpaper: TelegramWallpaper? = nil, serviceBackgroundColor: UIColor?) -> PresentationTheme { + if (theme.referenceTheme != .day && theme.referenceTheme != .dayClassic) { + return theme } - let rootTabBar = PresentationThemeRootTabBar( - backgroundColor: UIColor(rgb: 0xf7f7f7), - separatorColor: UIColor(rgb: 0xa3a3a3), - iconColor: UIColor(rgb: 0x959595), - selectedIconColor: accentColor, - textColor: UIColor(rgb: 0x959595), - selectedTextColor: accentColor, - badgeBackgroundColor: UIColor(rgb: 0xff3b30), - badgeStrokeColor: UIColor(rgb: 0xff3b30), - badgeTextColor: .white + let day = theme.referenceTheme == .day + var intro = theme.intro + var rootController = theme.rootController + var list = theme.list + var chatList = theme.chatList + var chat = theme.chat + var actionSheet = theme.actionSheet + + var outgoingAccent: UIColor? + var suggestedWallpaper: TelegramWallpaper? + + var bubbleColors = bubbleColors + if bubbleColors == nil, editing { + if day { + let accentColor = accentColor ?? defaultDayAccentColor + bubbleColors = (accentColor.withMultiplied(hue: 0.966, saturation: 0.61, brightness: 0.98), accentColor) + } else { + if let accentColor = accentColor { + let hsb = accentColor.hsb + bubbleColors = (UIColor(hue: hsb.0, saturation: (hsb.1 > 0.0 && hsb.2 > 0.0) ? 0.14 : 0.0, brightness: 0.79 + hsb.2 * 0.21, alpha: 1.0), nil) + if accentColor.lightness > 0.705 { + outgoingAccent = UIColor(hue: hsb.0, saturation: min(1.0, hsb.1 * 1.1), brightness: min(hsb.2, 0.6), alpha: 1.0) + } else { + outgoingAccent = accentColor + } + + let topColor = accentColor.withMultiplied(hue: 1.010, saturation: 0.414, brightness: 0.957) + let bottomColor = accentColor.withMultiplied(hue: 1.019, saturation: 0.867, brightness: 0.965) + suggestedWallpaper = .gradient(topColor.argb, bottomColor.argb, WallpaperSettings()) + } else { + bubbleColors = (UIColor(rgb: 0xe1ffc7), nil) + suggestedWallpaper = .builtin(WallpaperSettings()) + } + } + } + + var accentColor = accentColor + if let initialAccentColor = accentColor, initialAccentColor.lightness > 0.705 { + let hsb = initialAccentColor.hsb + accentColor = UIColor(hue: hsb.0, saturation: min(1.0, hsb.1 * 1.1), brightness: min(hsb.2, 0.6), alpha: 1.0) + } + + if let accentColor = accentColor { + intro = intro.withUpdated(accentTextColor: accentColor) + rootController = rootController.withUpdated( + tabBar: rootController.tabBar.withUpdated(selectedIconColor: accentColor, selectedTextColor: accentColor), + navigationBar: rootController.navigationBar.withUpdated(buttonColor: accentColor, accentTextColor: accentColor), + navigationSearchBar: rootController.navigationSearchBar.withUpdated(accentColor: accentColor) + ) + list = list.withUpdated( + itemAccentColor: accentColor, + itemDisclosureActions: list.itemDisclosureActions.withUpdated(accent: list.itemDisclosureActions.accent.withUpdated(fillColor: accentColor)), + itemCheckColors: list.itemCheckColors.withUpdated(fillColor: accentColor), + itemBarChart: list.itemBarChart.withUpdated(color1: accentColor) + ) + chatList = chatList.withUpdated( + checkmarkColor: day ? accentColor : nil, + unreadBadgeActiveBackgroundColor: accentColor, + verifiedIconFillColor: day ? accentColor : nil + ) + actionSheet = actionSheet.withUpdated( + standardActionTextColor: accentColor, + controlAccentColor: accentColor + ) + } + + var incomingBubbleStrokeColor: UIColor? + var outgoingBubbleFillColor: UIColor? + var outgoingBubbleFillGradientColor: UIColor? + var outgoingBubbleHighlightedFill: UIColor? + var outgoingBubbleStrokeColor: UIColor? + var outgoingPrimaryTextColor: UIColor? + var outgoingSecondaryTextColor: UIColor? + var outgoingLinkTextColor: UIColor? + var outgoingScamColor: UIColor? + var outgoingAccentTextColor: UIColor? + var outgoingControlColor: UIColor? + var outgoingInactiveControlColor: UIColor? + var outgoingPendingActivityColor: UIColor? + var outgoingFileTitleColor: UIColor? + var outgoingFileDescriptionColor: UIColor? + var outgoingFileDurationColor: UIColor? + var outgoingMediaPlaceholderColor: UIColor? + var outgoingPollsButtonColor: UIColor? + var outgoingPollsProgressColor: UIColor? + var outgoingSelectionColor: UIColor? + var outgoingSelectionBaseColor: UIColor? + var outgoingCheckColor: UIColor? + + if !day { + let bubbleStrokeColor = serviceBackgroundColor?.withMultiplied(hue: 0.999, saturation: 1.667, brightness: 1.1).withAlphaComponent(0.2) + incomingBubbleStrokeColor = bubbleStrokeColor + outgoingBubbleStrokeColor = bubbleStrokeColor + } + + if let bubbleColors = bubbleColors { + var topBubbleColor = bubbleColors.0 + var bottomBubbleColor = bubbleColors.1 ?? bubbleColors.0 + + if topBubbleColor.rgb != bottomBubbleColor.rgb { + let topBubbleColorLightness = topBubbleColor.lightness + let bottomBubbleColorLightness = bottomBubbleColor.lightness + if abs(topBubbleColorLightness - bottomBubbleColorLightness) > 0.7 { + if topBubbleColorLightness > bottomBubbleColorLightness { + topBubbleColor = topBubbleColor.withMultiplied(hue: 1.0, saturation: 1.0, brightness: 0.85) + } else { + bottomBubbleColor = bottomBubbleColor.withMultiplied(hue: 1.0, saturation: 1.0, brightness: 0.85) + } + } + } + + outgoingBubbleFillColor = topBubbleColor + outgoingBubbleFillGradientColor = bottomBubbleColor + + if day { + outgoingBubbleStrokeColor = .clear + } + + outgoingBubbleHighlightedFill = outgoingBubbleFillColor?.withMultiplied(hue: 1.054, saturation: 1.589, brightness: 0.96) + + let lightnessColor = bubbleColors.0.mixedWith(bubbleColors.1 ?? bubbleColors.0, alpha: 0.5) + if lightnessColor.lightness > 0.705 { + let hueFactor: CGFloat = 0.75 + let saturationFactor: CGFloat = 1.1 + outgoingPrimaryTextColor = UIColor(rgb: 0x000000) + outgoingSecondaryTextColor = outgoingBubbleFillColor?.withMultiplied(hue: 1.344 * hueFactor, saturation: 4.554 * saturationFactor, brightness: 0.549).withAlphaComponent(0.8) + + if let outgoingAccent = outgoingAccent { + outgoingAccentTextColor = outgoingAccent + outgoingLinkTextColor = outgoingAccent + outgoingScamColor = UIColor(rgb: 0xff3b30) + outgoingControlColor = outgoingAccent + outgoingInactiveControlColor = outgoingAccent //1111 + outgoingFileTitleColor = outgoingAccent + outgoingPollsProgressColor = accentColor + outgoingSelectionColor = outgoingAccent.withMultiplied(hue: 1.0, saturation: 1.292, brightness: 0.871) + outgoingSelectionBaseColor = outgoingControlColor + outgoingCheckColor = outgoingAccent + } else { + let outgoingBubbleMixedColor = lightnessColor + + outgoingAccentTextColor = outgoingBubbleMixedColor.withMultiplied(hue: 1.302 * hueFactor, saturation: 4.554 * saturationFactor, brightness: 0.655) + outgoingLinkTextColor = UIColor(rgb: 0x004bad) + outgoingScamColor = UIColor(rgb: 0xff3b30) + outgoingControlColor = outgoingBubbleMixedColor.withMultiplied(hue: 1.283 * hueFactor, saturation: 3.176, brightness: 0.765) + outgoingInactiveControlColor = outgoingBubbleMixedColor.withMultiplied(hue: 1.207 * hueFactor, saturation: 1.721, brightness: 0.851) + outgoingFileTitleColor = outgoingBubbleMixedColor.withMultiplied(hue: 1.285 * hueFactor, saturation: 2.946, brightness: 0.667) + outgoingPollsProgressColor = outgoingBubbleMixedColor.withMultiplied(hue: 1.283 * hueFactor, saturation: 3.176, brightness: 0.765) + outgoingSelectionColor = outgoingBubbleMixedColor.withMultiplied(hue: 1.013 * hueFactor, saturation: 1.292, brightness: 0.871) + outgoingSelectionBaseColor = outgoingControlColor + outgoingCheckColor = outgoingBubbleMixedColor.withMultiplied(hue: 1.344 * hueFactor, saturation: 4.554 * saturationFactor, brightness: 0.549).withAlphaComponent(0.8) + } + outgoingPendingActivityColor = outgoingCheckColor + + outgoingFileDescriptionColor = outgoingBubbleFillColor?.withMultiplied(hue: 1.257 * hueFactor, saturation: 1.842, brightness: 0.698) + outgoingFileDurationColor = outgoingBubbleFillColor?.withMultiplied(hue: 1.344 * hueFactor, saturation: 4.554, brightness: 0.549).withAlphaComponent(0.8) + outgoingMediaPlaceholderColor = outgoingBubbleFillColor?.withMultiplied(hue: 0.998, saturation: 1.129, brightness: 0.949) + outgoingPollsButtonColor = outgoingBubbleFillColor?.withMultiplied(hue: 1.207 * hueFactor, saturation: 1.721, brightness: 0.851) + + if day { + if let distance = outgoingBubbleFillColor?.distance(to: UIColor(rgb: 0xffffff)), distance < 200 { + outgoingBubbleStrokeColor = UIColor(rgb: 0xc8c7cc) + } + } + } else { + outgoingPrimaryTextColor = UIColor(rgb: 0xffffff) + outgoingSecondaryTextColor = UIColor(rgb: 0xffffff, alpha: 0.65) + outgoingAccentTextColor = outgoingPrimaryTextColor + outgoingLinkTextColor = UIColor(rgb: 0xffffff) + outgoingScamColor = outgoingPrimaryTextColor + outgoingControlColor = outgoingPrimaryTextColor + outgoingInactiveControlColor = outgoingSecondaryTextColor + outgoingPendingActivityColor = outgoingSecondaryTextColor + outgoingFileTitleColor = outgoingPrimaryTextColor + outgoingFileDescriptionColor = outgoingSecondaryTextColor + outgoingFileDurationColor = outgoingSecondaryTextColor + outgoingMediaPlaceholderColor = outgoingBubbleFillColor?.withMultipliedBrightnessBy(0.95) + outgoingPollsButtonColor = outgoingSecondaryTextColor + outgoingPollsProgressColor = outgoingPrimaryTextColor + outgoingSelectionBaseColor = UIColor(rgb: 0xffffff) + outgoingSelectionColor = outgoingSelectionBaseColor?.withAlphaComponent(0.2) + outgoingCheckColor = UIColor(rgb: 0xffffff) + } + } + + var defaultWallpaper: TelegramWallpaper? + if let forcedWallpaper = forcedWallpaper { + defaultWallpaper = forcedWallpaper + } else if let backgroundColors = backgroundColors { + if let secondColor = backgroundColors.1 { + defaultWallpaper = .gradient(backgroundColors.0.argb, secondColor.argb, WallpaperSettings()) + } else { + defaultWallpaper = .color(backgroundColors.0.argb) + } + } else if let forcedWallpaper = suggestedWallpaper { + defaultWallpaper = forcedWallpaper + } + + chat = chat.withUpdated( + defaultWallpaper: defaultWallpaper, + message: chat.message.withUpdated( + incoming: chat.message.incoming.withUpdated( + bubble: chat.message.incoming.bubble.withUpdated( + withWallpaper: chat.message.incoming.bubble.withWallpaper.withUpdated( + stroke: incomingBubbleStrokeColor + ), + withoutWallpaper: chat.message.incoming.bubble.withoutWallpaper.withUpdated( + stroke: incomingBubbleStrokeColor + ) + ), + linkHighlightColor: accentColor?.withAlphaComponent(0.3), + accentTextColor: accentColor, + accentControlColor: accentColor, + accentControlDisabledColor: accentColor?.withAlphaComponent(0.7), + mediaActiveControlColor: accentColor, + fileTitleColor: accentColor, + polls: chat.message.incoming.polls.withUpdated( + radioProgress: accentColor, + highlight: accentColor?.withAlphaComponent(0.12), + bar: accentColor + ), + actionButtonsFillColor: serviceBackgroundColor.flatMap { chat.message.incoming.actionButtonsFillColor.withUpdated(withWallpaper: $0) }, + actionButtonsStrokeColor: day ? chat.message.incoming.actionButtonsStrokeColor.withUpdated(withoutWallpaper: accentColor) : nil, + actionButtonsTextColor: day ? chat.message.incoming.actionButtonsTextColor.withUpdated(withoutWallpaper: accentColor) : nil, + textSelectionColor: accentColor?.withAlphaComponent(0.2), + textSelectionKnobColor: accentColor + ), + outgoing: chat.message.outgoing.withUpdated( + bubble: chat.message.outgoing.bubble.withUpdated( + withWallpaper: chat.message.outgoing.bubble.withWallpaper.withUpdated( + fill: outgoingBubbleFillColor, + gradientFill: outgoingBubbleFillGradientColor, + highlightedFill: outgoingBubbleHighlightedFill, + stroke: outgoingBubbleStrokeColor + ), + withoutWallpaper: chat.message.outgoing.bubble.withoutWallpaper.withUpdated( + fill: outgoingBubbleFillColor, + gradientFill: outgoingBubbleFillGradientColor, + highlightedFill: outgoingBubbleHighlightedFill, + stroke: outgoingBubbleStrokeColor + ) + ), + primaryTextColor: outgoingPrimaryTextColor, + secondaryTextColor: outgoingSecondaryTextColor, + linkTextColor: outgoingLinkTextColor, + linkHighlightColor: day ? nil : accentColor?.withAlphaComponent(0.3), + scamColor: outgoingScamColor, + accentTextColor: outgoingAccentTextColor, + accentControlColor: outgoingControlColor, + accentControlDisabledColor: outgoingControlColor?.withAlphaComponent(0.7), + mediaActiveControlColor: outgoingControlColor, + mediaInactiveControlColor: outgoingInactiveControlColor, + mediaControlInnerBackgroundColor: .clear, + pendingActivityColor: outgoingPendingActivityColor, + fileTitleColor: outgoingFileTitleColor, + fileDescriptionColor: outgoingFileDescriptionColor, + fileDurationColor: outgoingFileDurationColor, + mediaPlaceholderColor: day ? accentColor?.withMultipliedBrightnessBy(0.95) : outgoingMediaPlaceholderColor, + polls: chat.message.outgoing.polls.withUpdated(radioButton: outgoingPollsButtonColor, radioProgress: outgoingPollsProgressColor, highlight: outgoingPollsProgressColor?.withAlphaComponent(0.12), separator: outgoingPollsButtonColor, bar: outgoingPollsProgressColor, barIconForeground: .clear, barPositive: outgoingPollsProgressColor, barNegative: outgoingPollsProgressColor), + actionButtonsFillColor: chat.message.outgoing.actionButtonsFillColor.withUpdated(withWallpaper: serviceBackgroundColor), + actionButtonsStrokeColor: day ? chat.message.outgoing.actionButtonsStrokeColor.withUpdated(withoutWallpaper: accentColor) : nil, + actionButtonsTextColor: day ? chat.message.outgoing.actionButtonsTextColor.withUpdated(withoutWallpaper: accentColor) : nil, + textSelectionColor: outgoingSelectionColor, + textSelectionKnobColor: outgoingSelectionBaseColor), + outgoingCheckColor: outgoingCheckColor, + shareButtonFillColor: serviceBackgroundColor.flatMap { chat.message.shareButtonFillColor.withUpdated(withWallpaper: $0) }, + shareButtonForegroundColor: chat.message.shareButtonForegroundColor.withUpdated(withoutWallpaper: day ? accentColor : nil), + selectionControlColors: chat.message.selectionControlColors.withUpdated(fillColor: accentColor)), + serviceMessage: serviceBackgroundColor.flatMap { + chat.serviceMessage.withUpdated(components: chat.serviceMessage.components.withUpdated(withCustomWallpaper: chat.serviceMessage.components.withCustomWallpaper.withUpdated(fill: $0, dateFillStatic: $0, dateFillFloating: $0.withAlphaComponent($0.alpha * 0.6667)))) + }, + inputPanel: chat.inputPanel.withUpdated( + panelControlAccentColor: accentColor, + actionControlFillColor: accentColor, + mediaRecordingControl: chat.inputPanel.mediaRecordingControl.withUpdated( + buttonColor: accentColor, + micLevelColor: accentColor?.withAlphaComponent(0.2) + ) + ), + historyNavigation: chat.historyNavigation.withUpdated( + badgeBackgroundColor: accentColor, + badgeStrokeColor: accentColor + ) ) - let rootNavigationBar = PresentationThemeRootNavigationBar( - buttonColor: accentColor, - disabledButtonColor: UIColor(rgb: 0xd0d0d0), - primaryTextColor: .black, - secondaryTextColor: UIColor(rgb: 0x787878), - controlColor: UIColor(rgb: 0x7e8791), - accentTextColor: accentColor, - backgroundColor: UIColor(rgb: 0xf7f7f7), - separatorColor: UIColor(rgb: 0xb1b1b1), - badgeBackgroundColor: UIColor(rgb: 0xff3b30), - badgeStrokeColor: UIColor(rgb: 0xff3b30), - badgeTextColor: .white, - segmentedBackgroundColor: UIColor(rgb: 0xe9e9e9), - segmentedForegroundColor: UIColor(rgb: 0xf7f7f7), - segmentedTextColor: UIColor(rgb: 0x000000), - segmentedDividerColor: UIColor(rgb: 0xd6d6dc) - ) - - let navigationSearchBar = PresentationThemeNavigationSearchBar( - backgroundColor: .white, - accentColor: accentColor, - inputFillColor: UIColor(rgb: 0xe9e9e9), - inputTextColor: .black, - inputPlaceholderTextColor: UIColor(rgb: 0x8e8e93), - inputIconColor: UIColor(rgb: 0x8e8e93), - inputClearButtonColor: UIColor(rgb: 0x7b7b81), - separatorColor: UIColor(rgb: 0xb1b1b1) + return PresentationTheme( + name: title.flatMap { .custom($0) } ?? theme.name, + index: theme.index, + referenceTheme: theme.referenceTheme, + overallDarkAppearance: theme.overallDarkAppearance, + intro: intro, + passcode: theme.passcode, + rootController: rootController, + list: list, + chatList: chatList, + chat: chat, + actionSheet: actionSheet, + contextMenu: theme.contextMenu, + inAppNotification: theme.inAppNotification, + preview: theme.preview ) +} + +public func makeDefaultDayPresentationTheme(extendingThemeReference: PresentationThemeReference? = nil, serviceBackgroundColor: UIColor?, day: Bool, preview: Bool) -> PresentationTheme { + var serviceBackgroundColor = serviceBackgroundColor ?? defaultServiceBackgroundColor let intro = PresentationThemeIntro( statusBarStyle: .black, - primaryTextColor: .black, - accentTextColor: accentColor, + primaryTextColor: UIColor(rgb: 0x000000), + accentTextColor: UIColor(rgb: 0x007ee5), disabledTextColor: UIColor(rgb: 0xd0d0d0), startButtonColor: UIColor(rgb: 0x2ca5e0), dotColor: UIColor(rgb: 0xd9d9d9) @@ -100,7 +332,48 @@ private func makeDefaultDayPresentationTheme(accentColor: UIColor, serviceBackgr backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x46739e), bottomColor: UIColor(rgb: 0x2a5982)), buttonColor: .clear ) + + let rootTabBar = PresentationThemeRootTabBar( + backgroundColor: UIColor(rgb: 0xf7f7f7), + separatorColor: UIColor(rgb: 0xa3a3a3), + iconColor: UIColor(rgb: 0x959595), + selectedIconColor: UIColor(rgb: 0x007ee5), + textColor: UIColor(rgb: 0x959595), + selectedTextColor: UIColor(rgb: 0x007ee5), + badgeBackgroundColor: UIColor(rgb: 0xff3b30), + badgeStrokeColor: UIColor(rgb: 0xff3b30), + badgeTextColor: UIColor(rgb: 0xffffff) + ) + let rootNavigationBar = PresentationThemeRootNavigationBar( + buttonColor: UIColor(rgb: 0x007ee5), + disabledButtonColor: UIColor(rgb: 0xd0d0d0), + primaryTextColor: UIColor(rgb: 0x000000), + secondaryTextColor: UIColor(rgb: 0x787878), + controlColor: UIColor(rgb: 0x7e8791), + accentTextColor: UIColor(rgb: 0x007ee5), + backgroundColor: UIColor(rgb: 0xf7f7f7), + separatorColor: UIColor(rgb: 0xb1b1b1), + badgeBackgroundColor: UIColor(rgb: 0xff3b30), + badgeStrokeColor: UIColor(rgb: 0xff3b30), + badgeTextColor: UIColor(rgb: 0xffffff), + segmentedBackgroundColor: UIColor(rgb: 0xe9e9e9), + segmentedForegroundColor: UIColor(rgb: 0xf7f7f7), + segmentedTextColor: UIColor(rgb: 0x000000), + segmentedDividerColor: UIColor(rgb: 0xd6d6dc) + ) + + let navigationSearchBar = PresentationThemeNavigationSearchBar( + backgroundColor: UIColor(rgb: 0xffffff), + accentColor: UIColor(rgb: 0x007ee5), + inputFillColor: UIColor(rgb: 0xe9e9e9), + inputTextColor: UIColor(rgb: 0x000000), + inputPlaceholderTextColor: UIColor(rgb: 0x8e8e93), + inputIconColor: UIColor(rgb: 0x8e8e93), + inputClearButtonColor: UIColor(rgb: 0x7b7b81), + separatorColor: UIColor(rgb: 0xb1b1b1) + ) + let rootController = PresentationThemeRootController( statusBarStyle: .black, tabBar: rootTabBar, @@ -113,21 +386,21 @@ private func makeDefaultDayPresentationTheme(accentColor: UIColor, serviceBackgr frameColor: UIColor(rgb: 0xe9e9ea), handleColor: UIColor(rgb: 0xffffff), contentColor: UIColor(rgb: 0x35c759), - positiveColor: constructiveColor, - negativeColor: destructiveColor + positiveColor: UIColor(rgb: 0x00c900), + negativeColor: UIColor(rgb: 0xff3b30) ) let list = PresentationThemeList( blocksBackgroundColor: UIColor(rgb: 0xefeff4), - plainBackgroundColor: .white, - itemPrimaryTextColor: .black, + plainBackgroundColor: UIColor(rgb: 0xffffff), + itemPrimaryTextColor: UIColor(rgb: 0x000000), itemSecondaryTextColor: UIColor(rgb: 0x8e8e93), itemDisabledTextColor: UIColor(rgb: 0x8e8e93), - itemAccentColor: accentColor, - itemHighlightedColor: secretColor, - itemDestructiveColor: destructiveColor, + itemAccentColor: UIColor(rgb: 0x007ee5), + itemHighlightedColor: UIColor(rgb: 0x00b12c), + itemDestructiveColor: UIColor(rgb: 0xff3b30), itemPlaceholderTextColor: UIColor(rgb: 0xc8c8ce), - itemBlocksBackgroundColor: .white, + itemBlocksBackgroundColor: UIColor(rgb: 0xffffff), itemHighlightedBackgroundColor: UIColor(rgb: 0xe5e5ea), itemBlocksSeparatorColor: UIColor(rgb: 0xc8c7cc), itemPlainSeparatorColor: UIColor(rgb: 0xc8c7cc), @@ -139,143 +412,246 @@ private func makeDefaultDayPresentationTheme(accentColor: UIColor, serviceBackgr freeMonoIconColor: UIColor(rgb: 0x7e7e87), itemSwitchColors: switchColors, itemDisclosureActions: PresentationThemeItemDisclosureActions( - neutral1: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x4892f2), foregroundColor: .white), - neutral2: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xf09a37), foregroundColor: .white), - destructive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3824), foregroundColor: .white), - constructive: PresentationThemeFillForeground(fillColor: constructiveColor, foregroundColor: .white), - accent: PresentationThemeFillForeground(fillColor: accentColor, foregroundColor: .white), - warning: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff9500), foregroundColor: .white), - inactive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xbcbcc3), foregroundColor: .white) + neutral1: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x4892f2), foregroundColor: UIColor(rgb: 0xffffff)), + neutral2: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xf09a37), foregroundColor: UIColor(rgb: 0xffffff)), + destructive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3824), foregroundColor: UIColor(rgb: 0xffffff)), + constructive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x00c900), foregroundColor: UIColor(rgb: 0xffffff)), + accent: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x007ee5), foregroundColor: UIColor(rgb: 0xffffff)), + warning: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff9500), foregroundColor: UIColor(rgb: 0xffffff)), + inactive: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xbcbcc3), foregroundColor: UIColor(rgb: 0xffffff)) ), itemCheckColors: PresentationThemeFillStrokeForeground( - fillColor: accentColor, + fillColor: UIColor(rgb: 0x007ee5), strokeColor: UIColor(rgb: 0xc7c7cc), - foregroundColor: .white + foregroundColor: UIColor(rgb: 0xffffff) ), controlSecondaryColor: UIColor(rgb: 0xdedede), freeInputField: PresentationInputFieldTheme( backgroundColor: UIColor(rgb: 0xd6d6dc), strokeColor: UIColor(rgb: 0xd6d6dc), placeholderColor: UIColor(rgb: 0x96979d), - primaryColor: .black, + primaryColor: UIColor(rgb: 0x000000), controlColor: UIColor(rgb: 0x96979d) ), - mediaPlaceholderColor: UIColor(rgb: 0xe4e4e4), + freePlainInputField: PresentationInputFieldTheme( + backgroundColor: UIColor(rgb: 0xe9e9e9), + strokeColor: UIColor(rgb: 0xe9e9e9), + placeholderColor: UIColor(rgb: 0x8e8d92), + primaryColor: UIColor(rgb: 0x000000), + controlColor: UIColor(rgb: 0xbcbcc0) + ), + mediaPlaceholderColor: UIColor(rgb: 0xEFEFF4), scrollIndicatorColor: UIColor(white: 0.0, alpha: 0.3), pageIndicatorInactiveColor: UIColor(rgb: 0xe3e3e7), inputClearButtonColor: UIColor(rgb: 0xcccccc), - itemBarChart: PresentationThemeItemBarChart(color1: accentColor, color2: UIColor(rgb: 0xc8c7cc), color3: UIColor(rgb: 0xf2f1f7)) + itemBarChart: PresentationThemeItemBarChart(color1: UIColor(rgb: 0x007ee5), color2: UIColor(rgb: 0xc8c7cc), color3: UIColor(rgb: 0xf2f1f7)) ) let chatList = PresentationThemeChatList( - backgroundColor: .white, + backgroundColor: UIColor(rgb: 0xffffff), itemSeparatorColor: UIColor(rgb: 0xc8c7cc), - itemBackgroundColor: .white, + itemBackgroundColor: UIColor(rgb: 0xffffff), pinnedItemBackgroundColor: UIColor(rgb: 0xf7f7f7), itemHighlightedBackgroundColor: UIColor(rgb: 0xe5e5ea), itemSelectedBackgroundColor: UIColor(rgb: 0xe9f0fa), - titleColor: .black, - secretTitleColor: secretColor, + titleColor: UIColor(rgb: 0x000000), + secretTitleColor: UIColor(rgb: 0x00b12c), dateTextColor: UIColor(rgb: 0x8e8e93), - authorNameColor: .black, + authorNameColor: UIColor(rgb: 0x000000), messageTextColor: UIColor(rgb: 0x8e8e93), - messageHighlightedTextColor: .black, + messageHighlightedTextColor: UIColor(rgb: 0x000000), messageDraftTextColor: UIColor(rgb: 0xdd4b39), - checkmarkColor: day ? accentColor : UIColor(rgb: 0x21c004), + checkmarkColor: day ? UIColor(rgb: 0x007ee5) : UIColor(rgb: 0x21c004), pendingIndicatorColor: UIColor(rgb: 0x8e8e93), - failedFillColor: destructiveColor, - failedForegroundColor: .white, + failedFillColor: UIColor(rgb: 0xff3b30), + failedForegroundColor: UIColor(rgb: 0xffffff), muteIconColor: UIColor(rgb: 0xa7a7ad), - unreadBadgeActiveBackgroundColor: accentColor, - unreadBadgeActiveTextColor: .white, + unreadBadgeActiveBackgroundColor: UIColor(rgb: 0x007ee5), + unreadBadgeActiveTextColor: UIColor(rgb: 0xffffff), unreadBadgeInactiveBackgroundColor: UIColor(rgb: 0xb6b6bb), - unreadBadgeInactiveTextColor: .white, + unreadBadgeInactiveTextColor: UIColor(rgb: 0xffffff), pinnedBadgeColor: UIColor(rgb: 0xb6b6bb), pinnedSearchBarColor: UIColor(rgb: 0xe5e5e5), regularSearchBarColor: UIColor(rgb: 0xe9e9e9), sectionHeaderFillColor: UIColor(rgb: 0xf7f7f7), sectionHeaderTextColor: UIColor(rgb: 0x8e8e93), - verifiedIconFillColor: accentColor, - verifiedIconForegroundColor: .white, - secretIconColor: secretColor, - pinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x72d5fd), bottomColor: UIColor(rgb: 0x2a9ef1)), foregroundColor: .white), - unpinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0xdedee5), bottomColor: UIColor(rgb: 0xc5c6cc)), foregroundColor: .white), + verifiedIconFillColor: UIColor(rgb: 0x007ee5), + verifiedIconForegroundColor: UIColor(rgb: 0xffffff), + secretIconColor: UIColor(rgb: 0x00b12c), + pinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x72d5fd), bottomColor: UIColor(rgb: 0x2a9ef1)), foregroundColor: UIColor(rgb: 0xffffff)), + unpinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0xdedee5), bottomColor: UIColor(rgb: 0xc5c6cc)), foregroundColor: UIColor(rgb: 0xffffff)), onlineDotColor: UIColor(rgb: 0x4cc91f) ) + let bubbleStrokeColor = serviceBackgroundColor.withMultiplied(hue: 0.999, saturation: 1.667, brightness: 1.1).withAlphaComponent(0.2) + let message = PresentationThemeChatMessage( - incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xffffff), highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: UIColor(rgb: 0x86A9C9, alpha: 0.5)), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xffffff), highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: UIColor(rgb: 0x86A9C9, alpha: 0.5))), primaryTextColor: .black, secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6), linkTextColor: UIColor(rgb: 0x004bad), linkHighlightColor: accentColor.withAlphaComponent(0.3), scamColor: destructiveColor, textHighlightColor: UIColor(rgb: 0xffe438), accentTextColor: UIColor(rgb: 0x007ee5), accentControlColor: UIColor(rgb: 0x007ee5), mediaActiveControlColor: UIColor(rgb: 0x007ee5), mediaInactiveControlColor: UIColor(rgb: 0xcacaca), mediaControlInnerBackgroundColor: UIColor(rgb: 0xffffff), pendingActivityColor: UIColor(rgb: 0x525252, alpha: 0.6), fileTitleColor: UIColor(rgb: 0x0b8bed), fileDescriptionColor: UIColor(rgb: 0x999999), fileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6), mediaPlaceholderColor: UIColor(rgb: 0xe8ecf0), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xc8c7cc), radioProgress: UIColor(rgb: 0x007ee5), highlight: UIColor(rgb: 0x007ee5).withAlphaComponent(0.08), separator: UIColor(rgb: 0xc8c7cc), bar: UIColor(rgb: 0x007ee5)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x596e89, alpha: 0.35)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: .clear), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: accentColor.withAlphaComponent(0.2), textSelectionKnobColor: accentColor), - outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xe1ffc7), highlightedFill: UIColor(rgb: 0xc8ffa6), stroke: UIColor(rgb: 0x86a9c9, alpha: 0.5)), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xe1ffc7), highlightedFill: UIColor(rgb: 0xc8ffa6), stroke: UIColor(rgb: 0x86a9c9, alpha: 0.5))), primaryTextColor: .black, secondaryTextColor: UIColor(rgb: 0x008c09, alpha: 0.8), linkTextColor: UIColor(rgb: 0x004bad), linkHighlightColor: accentColor.withAlphaComponent(0.3), scamColor: destructiveColor, textHighlightColor: UIColor(rgb: 0xffe438), accentTextColor: UIColor(rgb: 0x00a700), accentControlColor: UIColor(rgb: 0x3fc33b), mediaActiveControlColor: UIColor(rgb: 0x3fc33b), mediaInactiveControlColor: UIColor(rgb: 0x93d987), mediaControlInnerBackgroundColor: UIColor(rgb: 0xe1ffc7), pendingActivityColor: UIColor(rgb: 0x42b649), fileTitleColor: UIColor(rgb: 0x3faa3c), fileDescriptionColor: UIColor(rgb: 0x6fb26a), fileDurationColor: UIColor(rgb: 0x008c09, alpha: 0.8), mediaPlaceholderColor: UIColor(rgb: 0xd2f2b6), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0x93d987), radioProgress: UIColor(rgb: 0x3fc33b), highlight: UIColor(rgb: 0x3fc33b).withAlphaComponent(0.08), separator: UIColor(rgb: 0x93d987), bar: UIColor(rgb: 0x3fc33b)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x596e89, alpha: 0.35)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: .clear), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: UIColor(rgb: 0xBBDE9F), textSelectionKnobColor: UIColor(rgb: 0x3FC33B)), - freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xffffff), highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: UIColor(rgb: 0x86A9C9, alpha: 0.5)), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xffffff), highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: UIColor(rgb: 0x86A9C9, alpha: 0.5))), + incoming: PresentationThemePartedColors( + bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xffffff), highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: bubbleStrokeColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xffffff), highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: bubbleStrokeColor, shadow: nil)), + primaryTextColor: UIColor(rgb: 0x000000), + secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6), + linkTextColor: UIColor(rgb: 0x004bad), + linkHighlightColor: UIColor(rgb: 0x007ee5).withAlphaComponent(0.3), + scamColor: UIColor(rgb: 0xff3b30), + textHighlightColor: UIColor(rgb: 0xffe438), + accentTextColor: UIColor(rgb: 0x007ee5), + accentControlColor: UIColor(rgb: 0x007ee5), + accentControlDisabledColor: UIColor(rgb: 0x525252, alpha: 0.6), + mediaActiveControlColor: UIColor(rgb: 0x007ee5), + mediaInactiveControlColor: UIColor(rgb: 0xcacaca), + mediaControlInnerBackgroundColor: UIColor(rgb: 0xffffff), + pendingActivityColor: UIColor(rgb: 0x525252, alpha: 0.6), + fileTitleColor: UIColor(rgb: 0x0b8bed), + fileDescriptionColor: UIColor(rgb: 0x999999), + fileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6), + mediaPlaceholderColor: UIColor(rgb: 0xe8ecf0), + polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xc8c7cc), radioProgress: UIColor(rgb: 0x007ee5), highlight: UIColor(rgb: 0x007ee5, alpha: 0.08), separator: UIColor(rgb: 0xc8c7cc), bar: UIColor(rgb: 0x007ee5), barIconForeground: .white, barPositive: UIColor(rgb: 0x2dba45), barNegative: UIColor(rgb: 0xFE3824)), + actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x596e89, alpha: 0.35)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: .clear), + actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0x007ee5, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0x007ee5)), + outgoing: PresentationThemePartedColors( + bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xe1ffc7), highlightedFill: UIColor(rgb: 0xc8ffa6), stroke: bubbleStrokeColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xe1ffc7), highlightedFill: UIColor(rgb: 0xc8ffa6), stroke: bubbleStrokeColor, shadow: nil)), + primaryTextColor: UIColor(rgb: 0x000000), + secondaryTextColor: UIColor(rgb: 0x008c09, alpha: 0.8), + linkTextColor: UIColor(rgb: 0x004bad), + linkHighlightColor: UIColor(rgb: 0x007ee5).withAlphaComponent(0.3), + scamColor: UIColor(rgb: 0xff3b30), + textHighlightColor: UIColor(rgb: 0xffe438), + accentTextColor: UIColor(rgb: 0x00a700), + accentControlColor: UIColor(rgb: 0x3fc33b), + accentControlDisabledColor: UIColor(rgb: 0x3fc33b).withAlphaComponent(0.7), + mediaActiveControlColor: UIColor(rgb: 0x3fc33b), + mediaInactiveControlColor: UIColor(rgb: 0x93d987), + mediaControlInnerBackgroundColor: UIColor(rgb: 0xe1ffc7), + pendingActivityColor: UIColor(rgb: 0x42b649), + fileTitleColor: UIColor(rgb: 0x3faa3c), + fileDescriptionColor: UIColor(rgb: 0x6fb26a), + fileDurationColor: UIColor(rgb: 0x008c09, alpha: 0.8), + mediaPlaceholderColor: UIColor(rgb: 0xd2f2b6), + polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0x93d987), radioProgress: UIColor(rgb: 0x3fc33b), highlight: UIColor(rgb: 0x3fc33b).withAlphaComponent(0.08), separator: UIColor(rgb: 0x93d987), bar: UIColor(rgb: 0x00A700), barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0x00A700)), + actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x596e89, alpha: 0.35)), + actionButtonsStrokeColor: PresentationThemeVariableColor(color: .clear), + actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), + textSelectionColor: UIColor(rgb: 0xbbde9f), + textSelectionKnobColor: UIColor(rgb: 0x3fc33b)), + freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xffffff), highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: UIColor(rgb: 0x86a9c9, alpha: 0.5), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xffffff), highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: UIColor(rgb: 0x86a9c9, alpha: 0.5), shadow: nil)), infoPrimaryTextColor: UIColor(rgb: 0x000000), infoLinkTextColor: UIColor(rgb: 0x004bad), outgoingCheckColor: UIColor(rgb: 0x19c700), mediaDateAndStatusFillColor: UIColor(white: 0.0, alpha: 0.5), - mediaDateAndStatusTextColor: .white, + mediaDateAndStatusTextColor: UIColor(rgb: 0xffffff), shareButtonFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x748391, alpha: 0.45)), shareButtonStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: .clear), - shareButtonForegroundColor: PresentationThemeVariableColor(withWallpaper: .white, withoutWallpaper: .white), - mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.6), foregroundColor: .white), - selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: accentColor, strokeColor: UIColor(rgb: 0xc7c7cc), foregroundColor: .white), - deliveryFailedColors: PresentationThemeFillForeground(fillColor: destructiveColor, foregroundColor: .white), + shareButtonForegroundColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0xffffff)), + mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.6), foregroundColor: UIColor(rgb: 0xffffff)), + selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: UIColor(rgb: 0x007ee5), strokeColor: UIColor(rgb: 0xc7c7cc), foregroundColor: UIColor(rgb: 0xffffff)), + deliveryFailedColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3b30), foregroundColor: UIColor(rgb: 0xffffff)), mediaHighlightOverlayColor: UIColor(white: 1.0, alpha: 0.6) ) let messageDay = PresentationThemeChatMessage( - incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xffffff), highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xffffff)), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xf1f1f4), highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xf1f1f4))), primaryTextColor: .black, secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6), linkTextColor: UIColor(rgb: 0x004bad), linkHighlightColor: accentColor.withAlphaComponent(0.3), scamColor: destructiveColor, textHighlightColor: UIColor(rgb: 0xffc738), accentTextColor: accentColor, accentControlColor: accentColor, mediaActiveControlColor: accentColor, mediaInactiveControlColor: UIColor(rgb: 0xcacaca), mediaControlInnerBackgroundColor: UIColor(rgb: 0xffffff), pendingActivityColor: UIColor(rgb: 0x525252, alpha: 0.6), fileTitleColor: accentColor, fileDescriptionColor: UIColor(rgb: 0x999999), fileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6), mediaPlaceholderColor: UIColor(rgb: 0xffffff).withMultipliedBrightnessBy(0.95), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xc8c7cc), radioProgress: accentColor, highlight: accentColor.withAlphaComponent(0.12), separator: UIColor(rgb: 0xc8c7cc), bar: accentColor), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)), actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: accentColor), actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: .white, withoutWallpaper: accentColor), textSelectionColor: accentColor.withAlphaComponent(0.3), textSelectionKnobColor: accentColor), - outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColor, highlightedFill: outgoingBubbleFillColor.withMultipliedBrightnessBy(0.7), stroke: outgoingBubbleFillColor), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColor, highlightedFill: outgoingBubbleFillColor.withMultipliedBrightnessBy(0.7), stroke: outgoingBubbleStrokeColor)), primaryTextColor: outgoingPrimaryTextColor, secondaryTextColor: outgoingSecondaryTextColor, linkTextColor: outgoingLinkTextColor, linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.3), scamColor: outgoingPrimaryTextColor, textHighlightColor: UIColor(rgb: 0xffc738), accentTextColor: outgoingPrimaryTextColor, accentControlColor: outgoingPrimaryTextColor, mediaActiveControlColor: outgoingPrimaryTextColor, mediaInactiveControlColor: outgoingSecondaryTextColor, mediaControlInnerBackgroundColor: outgoingBubbleFillColor, pendingActivityColor: outgoingSecondaryTextColor, fileTitleColor: outgoingPrimaryTextColor, fileDescriptionColor: outgoingSecondaryTextColor, fileDurationColor: outgoingSecondaryTextColor, mediaPlaceholderColor: accentColor.withMultipliedBrightnessBy(0.95), polls: PresentationThemeChatBubblePolls(radioButton: outgoingSecondaryTextColor, radioProgress: outgoingPrimaryTextColor, highlight: outgoingPrimaryTextColor.withAlphaComponent(0.12), separator: outgoingSecondaryTextColor, bar: outgoingPrimaryTextColor), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)), actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: accentColor), actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: .white, withoutWallpaper: accentColor), textSelectionColor: outgoingSelectionBaseColor.withAlphaComponent(0.2), textSelectionKnobColor: outgoingSelectionBaseColor), - freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xE5E5EA), highlightedFill: UIColor(rgb: 0xDADADE), stroke: UIColor(rgb: 0xE5E5EA)), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xE5E5EA), highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xE5E5EA))), + incoming: PresentationThemePartedColors( + bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xffffff), highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xffffff), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xf1f1f4), highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xf1f1f4), shadow: nil)), + primaryTextColor: UIColor(rgb: 0x000000), + secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6), + linkTextColor: UIColor(rgb: 0x004bad), + linkHighlightColor: UIColor(rgb: 0x007ee5, alpha: 0.3), + scamColor: UIColor(rgb: 0xff3b30), + textHighlightColor: UIColor(rgb: 0xffc738), + accentTextColor: UIColor(rgb: 0x007ee5), + accentControlColor: UIColor(rgb: 0x007ee5), + accentControlDisabledColor: UIColor(rgb: 0x525252, alpha: 0.6), + mediaActiveControlColor: UIColor(rgb: 0x007ee5), + mediaInactiveControlColor: UIColor(rgb: 0xcacaca), + mediaControlInnerBackgroundColor: UIColor(rgb: 0xffffff), + pendingActivityColor: UIColor(rgb: 0x525252, alpha: 0.6), + fileTitleColor: UIColor(rgb: 0x007ee5), + fileDescriptionColor: UIColor(rgb: 0x999999), + fileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6), + mediaPlaceholderColor: UIColor(rgb: 0xffffff).withMultipliedBrightnessBy(0.95), + polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xc8c7cc), radioProgress: UIColor(rgb: 0x007ee5), highlight: UIColor(rgb: 0x007ee5, alpha: 0.12), separator: UIColor(rgb: 0xc8c7cc), bar: UIColor(rgb: 0x007ee5), barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), + actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)), + actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: UIColor(rgb: 0x007ee5)), + actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0x007ee5)), + textSelectionColor: UIColor(rgb: 0x007ee5, alpha: 0.3), + textSelectionKnobColor: UIColor(rgb: 0x007ee5)), + outgoing: PresentationThemePartedColors( + bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x57b2e0), gradientFill: UIColor(rgb: 0x007ee5), highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), stroke: .clear, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x57b2e0), gradientFill: UIColor(rgb: 0x007ee5), highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), stroke: .clear, shadow: nil)), + primaryTextColor: UIColor(rgb: 0xffffff), + secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.65), + linkTextColor: UIColor(rgb: 0xffffff), + linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.3), + scamColor: UIColor(rgb: 0xffffff), + textHighlightColor: UIColor(rgb: 0xffc738), + accentTextColor: UIColor(rgb: 0xffffff), + accentControlColor: UIColor(rgb: 0xffffff), + accentControlDisabledColor: UIColor(rgb: 0xffffff).withAlphaComponent(0.5), + mediaActiveControlColor: UIColor(rgb: 0xffffff), + mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.65), + mediaControlInnerBackgroundColor: .clear, + pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.65), + fileTitleColor: UIColor(rgb: 0xffffff), + fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.65), + fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.65), + mediaPlaceholderColor: UIColor(rgb: 0x0077d9), + polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff, alpha: 0.65), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff, alpha: 0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.65), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0xffffff), barNegative: UIColor(rgb: 0xffffff)), + actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)), + actionButtonsStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: UIColor(rgb: 0x007ee5)), + actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0x007ee5)), + textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), + textSelectionKnobColor: UIColor(rgb: 0xffffff)), + freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xe5e5ea), highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xe5e5ea), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0xe5e5ea), highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xe5e5ea), shadow: nil)), infoPrimaryTextColor: UIColor(rgb: 0x000000), infoLinkTextColor: UIColor(rgb: 0x004bad), - outgoingCheckColor: outgoingCheckColor, + outgoingCheckColor: UIColor(rgb: 0xffffff), mediaDateAndStatusFillColor: UIColor(rgb: 0x000000, alpha: 0.5), - mediaDateAndStatusTextColor: .white, + mediaDateAndStatusTextColor: UIColor(rgb: 0xffffff), shareButtonFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)), shareButtonStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: UIColor(rgb: 0xe5e5ea)), - shareButtonForegroundColor: PresentationThemeVariableColor(withWallpaper: .white, withoutWallpaper: accentColor), - mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.6), foregroundColor: .white), - selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: accentColor, strokeColor: UIColor(rgb: 0xc7c7cc), foregroundColor: .white), - deliveryFailedColors: PresentationThemeFillForeground(fillColor: destructiveColor, foregroundColor: .white), + shareButtonForegroundColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0x007ee5)), + mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.6), foregroundColor: UIColor(rgb: 0xffffff)), + selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: UIColor(rgb: 0x007ee5), strokeColor: UIColor(rgb: 0xc7c7cc), foregroundColor: UIColor(rgb: 0xffffff)), + deliveryFailedColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3b30), foregroundColor: UIColor(rgb: 0xffffff)), mediaHighlightOverlayColor: UIColor(rgb: 0xffffff, alpha: 0.6) ) let serviceMessage = PresentationThemeServiceMessage( - components: PresentationThemeServiceMessageColor(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0x748391, alpha: 0.45), primaryText: .white, linkHighlight: UIColor(rgb: 0x748391, alpha: 0.25), scam: .white, dateFillStatic: UIColor(rgb: 0x748391, alpha: 0.45), dateFillFloating: UIColor(rgb: 0x939fab, alpha: 0.5)), withCustomWallpaper: PresentationThemeServiceMessageColorComponents(fill: serviceBackgroundColor, primaryText: .white, linkHighlight: UIColor(rgb: 0x748391, alpha: 0.25), scam: .white, dateFillStatic: serviceBackgroundColor, dateFillFloating: serviceBackgroundColor.withAlphaComponent(serviceBackgroundColor.alpha * 0.6667))), + components: PresentationThemeServiceMessageColor(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0x748391, alpha: 0.45), primaryText: UIColor(rgb: 0xffffff), linkHighlight: UIColor(rgb: 0x748391, alpha: 0.25), scam: UIColor(rgb: 0xffffff), dateFillStatic: UIColor(rgb: 0x748391, alpha: 0.45), dateFillFloating: UIColor(rgb: 0x939fab, alpha: 0.5)), withCustomWallpaper: PresentationThemeServiceMessageColorComponents(fill: serviceBackgroundColor, primaryText: UIColor(rgb: 0xffffff), linkHighlight: UIColor(rgb: 0x748391, alpha: 0.25), scam: UIColor(rgb: 0xffffff), dateFillStatic: serviceBackgroundColor, dateFillFloating: serviceBackgroundColor.withAlphaComponent(serviceBackgroundColor.alpha * 0.6667))), unreadBarFillColor: UIColor(white: 1.0, alpha: 0.9), unreadBarStrokeColor: UIColor(white: 0.0, alpha: 0.2), unreadBarTextColor: UIColor(rgb: 0x86868d), - dateTextColor: PresentationThemeVariableColor(color: .white) + dateTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)) ) let serviceMessageDay = PresentationThemeServiceMessage( - components: PresentationThemeServiceMessageColor(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0xffffff, alpha: 0.8), primaryText: UIColor(rgb: 0x8d8e93), linkHighlight: UIColor(rgb: 0x748391, alpha: 0.25), scam: destructiveColor, dateFillStatic: UIColor(rgb: 0xffffff, alpha: 0.8), dateFillFloating: UIColor(rgb: 0xffffff, alpha: 0.8)), withCustomWallpaper: PresentationThemeServiceMessageColorComponents(fill: serviceBackgroundColor, primaryText: .white, linkHighlight: UIColor(rgb: 0x748391, alpha: 0.25), scam: destructiveColor, dateFillStatic: serviceBackgroundColor, dateFillFloating: serviceBackgroundColor.withAlphaComponent(serviceBackgroundColor.alpha * 0.6667))), + components: PresentationThemeServiceMessageColor(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0xffffff, alpha: 0.8), primaryText: UIColor(rgb: 0x8d8e93), linkHighlight: UIColor(rgb: 0x748391, alpha: 0.25), scam: UIColor(rgb: 0xff3b30), dateFillStatic: UIColor(rgb: 0xffffff, alpha: 0.8), dateFillFloating: UIColor(rgb: 0xffffff, alpha: 0.8)), withCustomWallpaper: PresentationThemeServiceMessageColorComponents(fill: serviceBackgroundColor, primaryText: UIColor(rgb: 0xffffff), linkHighlight: UIColor(rgb: 0x748391, alpha: 0.25), scam: UIColor(rgb: 0xff3b30), dateFillStatic: serviceBackgroundColor, dateFillFloating: serviceBackgroundColor.withAlphaComponent(serviceBackgroundColor.alpha * 0.6667))), unreadBarFillColor: UIColor(rgb: 0xffffff), unreadBarStrokeColor: UIColor(rgb: 0xffffff), unreadBarTextColor: UIColor(rgb: 0x8d8e93), - dateTextColor: PresentationThemeVariableColor(withWallpaper: .white, withoutWallpaper: UIColor(rgb: 0x8d8e93)) + dateTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0x8d8e93)) ) let inputPanelMediaRecordingControl = PresentationThemeChatInputPanelMediaRecordingControl( - buttonColor: accentColor, - micLevelColor: accentColor.withAlphaComponent(0.2), - activeIconColor: .white + buttonColor: UIColor(rgb: 0x007ee5), + micLevelColor: UIColor(rgb: 0x007ee5, alpha: 0.2), + activeIconColor: UIColor(rgb: 0xffffff) ) let inputPanel = PresentationThemeChatInputPanel( panelBackgroundColor: UIColor(rgb: 0xf7f7f7), + panelBackgroundColorNoWallpaper: UIColor(rgb: 0xffffff), panelSeparatorColor: UIColor(rgb: 0xb2b2b2), - panelControlAccentColor: accentColor, + panelControlAccentColor: UIColor(rgb: 0x007ee5), panelControlColor: UIColor(rgb: 0x858e99), panelControlDisabledColor: UIColor(rgb: 0x727b87, alpha: 0.5), panelControlDestructiveColor: UIColor(rgb: 0xff3b30), inputBackgroundColor: UIColor(rgb: 0xffffff), inputStrokeColor: UIColor(rgb: 0xd9dcdf), inputPlaceholderColor: UIColor(rgb: 0xbebec0), - inputTextColor: .black, + inputTextColor: UIColor(rgb: 0x000000), inputControlColor: UIColor(rgb: 0xa0a7b0), - actionControlFillColor: accentColor, - actionControlForegroundColor: .white, - primaryTextColor: .black, + actionControlFillColor: UIColor(rgb: 0x007ee5), + actionControlForegroundColor: UIColor(rgb: 0xffffff), + primaryTextColor: UIColor(rgb: 0x000000), secondaryTextColor: UIColor(rgb: 0x8e8e93), mediaRecordingDotColor: UIColor(rgb: 0xed2521), mediaRecordingControl: inputPanelMediaRecordingControl @@ -289,28 +665,28 @@ private func makeDefaultDayPresentationTheme(accentColor: UIColor, serviceBackgr stickersSectionTextColor: UIColor(rgb: 0x9099a2), stickersSearchBackgroundColor: UIColor(rgb: 0xd9dbe1), stickersSearchPlaceholderColor: UIColor(rgb: 0x8e8e93), - stickersSearchPrimaryColor: .black, + stickersSearchPrimaryColor: UIColor(rgb: 0x000000), stickersSearchControlColor: UIColor(rgb: 0x8e8e93), - gifsBackgroundColor: .white + gifsBackgroundColor: UIColor(rgb: 0xffffff) ) let inputButtonPanel = PresentationThemeInputButtonPanel( panelSeparatorColor: UIColor(rgb: 0xbec2c6), panelBackgroundColor: UIColor(rgb: 0xdee2e6), - buttonFillColor: .white, + buttonFillColor: UIColor(rgb: 0xffffff), buttonStrokeColor: UIColor(rgb: 0xc3c7c9), buttonHighlightedFillColor: UIColor(rgb: 0xa8b3c0), buttonHighlightedStrokeColor: UIColor(rgb: 0xc3c7c9), - buttonTextColor: .black + buttonTextColor: UIColor(rgb: 0x000000) ) let historyNavigation = PresentationThemeChatHistoryNavigation( fillColor: UIColor(rgb: 0xf7f7f7), strokeColor: UIColor(rgb: 0xb1b1b1), foregroundColor: UIColor(rgb: 0x88888d), - badgeBackgroundColor: accentColor, - badgeStrokeColor: accentColor, - badgeTextColor: .white + badgeBackgroundColor: UIColor(rgb: 0x007ee5), + badgeStrokeColor: UIColor(rgb: 0x007ee5), + badgeTextColor: UIColor(rgb: 0xffffff) ) let chat = PresentationThemeChat( @@ -326,24 +702,24 @@ private func makeDefaultDayPresentationTheme(accentColor: UIColor, serviceBackgr let actionSheet = PresentationThemeActionSheet( dimColor: UIColor(white: 0.0, alpha: 0.4), backgroundType: .light, - opaqueItemBackgroundColor: .white, + opaqueItemBackgroundColor: UIColor(rgb: 0xffffff), itemBackgroundColor: UIColor(white: 1.0, alpha: 0.87), opaqueItemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 1.0), itemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 0.7), opaqueItemSeparatorColor: UIColor(white: 0.9, alpha: 1.0), - standardActionTextColor: accentColor, - destructiveActionTextColor: destructiveColor, + standardActionTextColor: UIColor(rgb: 0x007ee5), + destructiveActionTextColor: UIColor(rgb: 0xff3b30), disabledActionTextColor: UIColor(rgb: 0xb3b3b3), - primaryTextColor: .black, + primaryTextColor: UIColor(rgb: 0x000000), secondaryTextColor: UIColor(rgb: 0x5e5e5e), - controlAccentColor: accentColor, + controlAccentColor: UIColor(rgb: 0x007ee5), inputBackgroundColor: UIColor(rgb: 0xe9e9e9), - inputHollowBackgroundColor: .white, + inputHollowBackgroundColor: UIColor(rgb: 0xffffff), inputBorderColor: UIColor(rgb: 0xe4e4e6), - inputPlaceholderColor: UIColor(rgb: 0x8E8D92), - inputTextColor: .black, - inputClearButtonColor: UIColor(rgb: 0xBCBCC0), - checkContentColor: .white + inputPlaceholderColor: UIColor(rgb: 0x8e8d92), + inputTextColor: UIColor(rgb: 0x000000), + inputClearButtonColor: UIColor(rgb: 0xbcbcc0), + checkContentColor: UIColor(rgb: 0xffffff) ) let contextMenu = PresentationThemeContextMenu( @@ -355,17 +731,17 @@ private func makeDefaultDayPresentationTheme(accentColor: UIColor, serviceBackgr itemHighlightedBackgroundColor: UIColor(rgb: 0x3c3c43, alpha: 0.2), primaryColor: UIColor(rgb: 0x000000, alpha: 1.0), secondaryColor: UIColor(rgb: 0x000000, alpha: 0.8), - destructiveColor: destructiveColor + destructiveColor: UIColor(rgb: 0xff3b30) ) let inAppNotification = PresentationThemeInAppNotification( - fillColor: .white, - primaryTextColor: .black, + fillColor: UIColor(rgb: 0xffffff), + primaryTextColor: UIColor(rgb: 0x000000), expandedNotification: PresentationThemeExpandedNotification( backgroundType: .light, navigationBar: PresentationThemeExpandedNotificationNavigationBar( - backgroundColor: .white, - primaryTextColor: .black, + backgroundColor: UIColor(rgb: 0xffffff), + primaryTextColor: UIColor(rgb: 0x000000), controlColor: UIColor(rgb: 0x7e8791), separatorColor: UIColor(rgb: 0xb1b1b1) ) @@ -373,10 +749,10 @@ private func makeDefaultDayPresentationTheme(accentColor: UIColor, serviceBackgr ) return PresentationTheme( - name: .builtin(day ? .day : .dayClassic), + name: extendingThemeReference?.name ?? .builtin(day ? .day : .dayClassic), + index: extendingThemeReference?.index ?? PresentationThemeReference.builtin(day ? .day : .dayClassic).index, referenceTheme: day ? .day : .dayClassic, overallDarkAppearance: false, - baseColor: baseColor, intro: intro, passcode: passcode, rootController: rootController, @@ -389,13 +765,3 @@ private func makeDefaultDayPresentationTheme(accentColor: UIColor, serviceBackgr preview: preview ) } - -public let defaultPresentationTheme = makeDefaultDayPresentationTheme(accentColor: UIColor(rgb: 0x007ee5), serviceBackgroundColor: defaultServiceBackgroundColor, baseColor: nil, day: false, preview: false) - -public let defaultDayAccentColor = UIColor(rgb: 0x007ee5) -public let defaultServiceBackgroundColor = UIColor(rgb: 0x000000, alpha: 0.3) - -public func makeDefaultDayPresentationTheme(accentColor: UIColor? = nil, serviceBackgroundColor: UIColor, baseColor: PresentationThemeBaseColor?, day: Bool, preview: Bool) -> PresentationTheme { - let accentColor = accentColor ?? defaultDayAccentColor - return makeDefaultDayPresentationTheme(accentColor: accentColor, serviceBackgroundColor: serviceBackgroundColor, baseColor: baseColor, day: day, preview: preview) -} diff --git a/submodules/TelegramPresentationData/Sources/MakePresentationTheme.swift b/submodules/TelegramPresentationData/Sources/MakePresentationTheme.swift index c3b2b6b0e6..6bd5dbfcec 100644 --- a/submodules/TelegramPresentationData/Sources/MakePresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/MakePresentationTheme.swift @@ -1,37 +1,66 @@ import Foundation import UIKit import Postbox +import SyncCore import TelegramUIPreferences -public func makeDefaultPresentationTheme(reference: PresentationBuiltinThemeReference, accentColor: UIColor?, serviceBackgroundColor: UIColor, baseColor: PresentationThemeBaseColor?, preview: Bool = false) -> PresentationTheme { +public func makeDefaultPresentationTheme(reference: PresentationBuiltinThemeReference, extendingThemeReference: PresentationThemeReference? = nil, serviceBackgroundColor: UIColor?, preview: Bool = false) -> PresentationTheme { let theme: PresentationTheme switch reference { case .dayClassic: - theme = makeDefaultDayPresentationTheme(serviceBackgroundColor: serviceBackgroundColor, baseColor: baseColor, day: false, preview: preview) - case .night: - theme = makeDarkPresentationTheme(accentColor: accentColor, baseColor: baseColor, preview: preview) - case .nightAccent: - theme = makeDarkAccentPresentationTheme(accentColor: accentColor, baseColor: nil, preview: preview) + theme = makeDefaultDayPresentationTheme(extendingThemeReference: extendingThemeReference, serviceBackgroundColor: serviceBackgroundColor, day: false, preview: preview) case .day: - theme = makeDefaultDayPresentationTheme(accentColor: accentColor, serviceBackgroundColor: serviceBackgroundColor, baseColor: baseColor, day: true, preview: preview) + theme = makeDefaultDayPresentationTheme(extendingThemeReference: extendingThemeReference, serviceBackgroundColor: serviceBackgroundColor, day: true, preview: preview) + case .night: + theme = makeDefaultDarkPresentationTheme(extendingThemeReference: extendingThemeReference, preview: preview) + case .nightAccent: + theme = makeDefaultDarkTintedPresentationTheme(extendingThemeReference: extendingThemeReference, preview: preview) } return theme } -public func makePresentationTheme(mediaBox: MediaBox, themeReference: PresentationThemeReference, accentColor: UIColor?, serviceBackgroundColor: UIColor, baseColor: PresentationThemeBaseColor?, preview: Bool = false) -> PresentationTheme? { +public func customizePresentationTheme(_ theme: PresentationTheme, editing: Bool, title: String? = nil, accentColor: UIColor?, backgroundColors: (UIColor, UIColor?)?, bubbleColors: (UIColor, UIColor?)?, wallpaper: TelegramWallpaper? = nil) -> PresentationTheme { + if accentColor == nil && bubbleColors == nil && backgroundColors == nil && wallpaper == nil { + return theme + } + switch theme.referenceTheme { + case .day, .dayClassic: + return customizeDefaultDayTheme(theme: theme, editing: editing, title: title, accentColor: accentColor, backgroundColors: backgroundColors, bubbleColors: bubbleColors, wallpaper: wallpaper, serviceBackgroundColor: nil) + case .night: + return customizeDefaultDarkPresentationTheme(theme: theme, editing: editing, title: title, accentColor: accentColor, backgroundColors: backgroundColors, bubbleColors: bubbleColors, wallpaper: wallpaper) + case .nightAccent: + return customizeDefaultDarkTintedPresentationTheme(theme: theme, editing: editing, title: title, accentColor: accentColor, backgroundColors: backgroundColors, bubbleColors: bubbleColors, wallpaper: wallpaper) + } + + return theme +} + +public func makePresentationTheme(settings: TelegramThemeSettings, title: String? = nil, serviceBackgroundColor: UIColor? = nil) -> PresentationTheme? { + let defaultTheme = makeDefaultPresentationTheme(reference: PresentationBuiltinThemeReference(baseTheme: settings.baseTheme), extendingThemeReference: nil, serviceBackgroundColor: serviceBackgroundColor, preview: false) + return customizePresentationTheme(defaultTheme, editing: true, title: title, accentColor: UIColor(argb: settings.accentColor), backgroundColors: nil, bubbleColors: settings.messageColors.flatMap { (UIColor(argb: $0.top), UIColor(argb: $0.bottom)) }, wallpaper: settings.wallpaper) +} + +public func makePresentationTheme(mediaBox: MediaBox, themeReference: PresentationThemeReference, extendingThemeReference: PresentationThemeReference? = nil, accentColor: UIColor? = nil, backgroundColors: (UIColor, UIColor?)? = nil, bubbleColors: (UIColor, UIColor?)? = nil, wallpaper: TelegramWallpaper? = nil, serviceBackgroundColor: UIColor? = nil, preview: Bool = false) -> PresentationTheme? { let theme: PresentationTheme switch themeReference { case let .builtin(reference): - theme = makeDefaultPresentationTheme(reference: reference, accentColor: accentColor, serviceBackgroundColor: serviceBackgroundColor, baseColor: baseColor, preview: preview) + let defaultTheme = makeDefaultPresentationTheme(reference: reference, extendingThemeReference: extendingThemeReference, serviceBackgroundColor: serviceBackgroundColor, preview: preview) + theme = customizePresentationTheme(defaultTheme, editing: true, accentColor: accentColor, backgroundColors: backgroundColors, bubbleColors: bubbleColors, wallpaper: wallpaper) case let .local(info): - if let path = mediaBox.completedResourcePath(info.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead), let loadedTheme = makePresentationTheme(data: data, resolvedWallpaper: info.resolvedWallpaper) { - theme = loadedTheme + if let path = mediaBox.completedResourcePath(info.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead), let loadedTheme = makePresentationTheme(data: data, themeReference: themeReference, resolvedWallpaper: info.resolvedWallpaper) { + theme = customizePresentationTheme(loadedTheme, editing: false, accentColor: accentColor, backgroundColors: backgroundColors, bubbleColors: bubbleColors, wallpaper: wallpaper) } else { return nil } case let .cloud(info): - if let file = info.theme.file, let path = mediaBox.completedResourcePath(file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead), let loadedTheme = makePresentationTheme(data: data, resolvedWallpaper: info.resolvedWallpaper) { - theme = loadedTheme + if let settings = info.theme.settings { + if let loadedTheme = makePresentationTheme(mediaBox: mediaBox, themeReference: .builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)), extendingThemeReference: themeReference, accentColor: accentColor ?? UIColor(argb: settings.accentColor), backgroundColors: nil, bubbleColors: bubbleColors ?? settings.messageColors.flatMap { (UIColor(argb: $0.top), UIColor(argb: $0.bottom)) }, wallpaper: wallpaper ?? settings.wallpaper, serviceBackgroundColor: serviceBackgroundColor, preview: preview) { + theme = loadedTheme + } else { + return nil + } + } else if let file = info.theme.file, let path = mediaBox.completedResourcePath(file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead), let loadedTheme = makePresentationTheme(data: data, themeReference: themeReference, resolvedWallpaper: info.resolvedWallpaper) { + theme = customizePresentationTheme(loadedTheme, editing: false, accentColor: accentColor, backgroundColors: backgroundColors, bubbleColors: bubbleColors, wallpaper: wallpaper) } else { return nil } diff --git a/submodules/TelegramPresentationData/Sources/PresentationData.swift b/submodules/TelegramPresentationData/Sources/PresentationData.swift index ffeb2c0335..f33079da79 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationData.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationData.swift @@ -48,24 +48,40 @@ public enum PresentationDateFormat { case dayFirst } +public struct PresentationChatBubbleCorners: Equatable, Hashable { + public var mainRadius: CGFloat + public var auxiliaryRadius: CGFloat + public var mergeBubbleCorners: Bool + + public init(mainRadius: CGFloat, auxiliaryRadius: CGFloat, mergeBubbleCorners: Bool) { + self.mainRadius = mainRadius + self.auxiliaryRadius = auxiliaryRadius + self.mergeBubbleCorners = mergeBubbleCorners + } +} + public final class PresentationData: Equatable { public let strings: PresentationStrings public let theme: PresentationTheme public let autoNightModeTriggered: Bool public let chatWallpaper: TelegramWallpaper - public let fontSize: PresentationFontSize + public let chatFontSize: PresentationFontSize + public let chatBubbleCorners: PresentationChatBubbleCorners + public let listsFontSize: PresentationFontSize public let dateTimeFormat: PresentationDateTimeFormat public let nameDisplayOrder: PresentationPersonNameOrder public let nameSortOrder: PresentationPersonNameOrder public let disableAnimations: Bool public let largeEmoji: Bool - public init(strings: PresentationStrings, theme: PresentationTheme, autoNightModeTriggered: Bool, chatWallpaper: TelegramWallpaper, fontSize: PresentationFontSize, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, nameSortOrder: PresentationPersonNameOrder, disableAnimations: Bool, largeEmoji: Bool) { + public init(strings: PresentationStrings, theme: PresentationTheme, autoNightModeTriggered: Bool, chatWallpaper: TelegramWallpaper, chatFontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, listsFontSize: PresentationFontSize, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, nameSortOrder: PresentationPersonNameOrder, disableAnimations: Bool, largeEmoji: Bool) { self.strings = strings self.theme = theme self.autoNightModeTriggered = autoNightModeTriggered self.chatWallpaper = chatWallpaper - self.fontSize = fontSize + self.chatFontSize = chatFontSize + self.chatBubbleCorners = chatBubbleCorners + self.listsFontSize = listsFontSize self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder self.nameSortOrder = nameSortOrder @@ -74,7 +90,7 @@ public final class PresentationData: Equatable { } public static func ==(lhs: PresentationData, rhs: PresentationData) -> Bool { - return lhs.strings === rhs.strings && lhs.theme === rhs.theme && lhs.autoNightModeTriggered == rhs.autoNightModeTriggered && lhs.chatWallpaper == rhs.chatWallpaper && lhs.fontSize == rhs.fontSize && lhs.dateTimeFormat == rhs.dateTimeFormat && lhs.disableAnimations == rhs.disableAnimations && lhs.largeEmoji == rhs.largeEmoji + return lhs.strings === rhs.strings && lhs.theme === rhs.theme && lhs.autoNightModeTriggered == rhs.autoNightModeTriggered && lhs.chatWallpaper == rhs.chatWallpaper && lhs.chatFontSize == rhs.chatFontSize && lhs.chatBubbleCorners == rhs.chatBubbleCorners && lhs.listsFontSize == rhs.listsFontSize && lhs.dateTimeFormat == rhs.dateTimeFormat && lhs.disableAnimations == rhs.disableAnimations && lhs.largeEmoji == rhs.largeEmoji } } @@ -165,14 +181,16 @@ private func currentPersonNameSortOrder() -> PresentationPersonNameOrder { public final class InitialPresentationDataAndSettings { public let presentationData: PresentationData public let automaticMediaDownloadSettings: MediaAutoDownloadSettings + public let autodownloadSettings: AutodownloadSettings public let callListSettings: CallListSettings public let inAppNotificationSettings: InAppNotificationSettings public let mediaInputSettings: MediaInputSettings public let experimentalUISettings: ExperimentalUISettings - public init(presentationData: PresentationData, automaticMediaDownloadSettings: MediaAutoDownloadSettings, callListSettings: CallListSettings, inAppNotificationSettings: InAppNotificationSettings, mediaInputSettings: MediaInputSettings, experimentalUISettings: ExperimentalUISettings) { + public init(presentationData: PresentationData, automaticMediaDownloadSettings: MediaAutoDownloadSettings, autodownloadSettings: AutodownloadSettings, callListSettings: CallListSettings, inAppNotificationSettings: InAppNotificationSettings, mediaInputSettings: MediaInputSettings, experimentalUISettings: ExperimentalUISettings) { self.presentationData = presentationData self.automaticMediaDownloadSettings = automaticMediaDownloadSettings + self.autodownloadSettings = autodownloadSettings self.callListSettings = callListSettings self.inAppNotificationSettings = inAppNotificationSettings self.mediaInputSettings = mediaInputSettings @@ -203,6 +221,13 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager, s automaticMediaDownloadSettings = MediaAutoDownloadSettings.defaultSettings } + let autodownloadSettings: AutodownloadSettings + if let value = transaction.getSharedData(SharedDataKeys.autodownloadSettings) as? AutodownloadSettings { + autodownloadSettings = value + } else { + autodownloadSettings = .defaultSettings + } + let callListSettings: CallListSettings if let value = transaction.getSharedData(ApplicationSpecificSharedDataKeys.callListSettings) as? CallListSettings { callListSettings = value @@ -228,11 +253,7 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager, s let contactSettings: ContactSynchronizationSettings = (transaction.getSharedData(ApplicationSpecificSharedDataKeys.contactSynchronizationSettings) as? ContactSynchronizationSettings) ?? ContactSynchronizationSettings.defaultSettings - let themeValue: PresentationTheme - let effectiveTheme: PresentationThemeReference - var effectiveChatWallpaper: TelegramWallpaper = themeSettings.chatWallpaper - let parameters = AutomaticThemeSwitchParameters(settings: themeSettings.automaticThemeSwitchSetting) let autoNightModeTriggered: Bool if automaticThemeShouldSwitchNow(parameters, systemUserInterfaceStyle: systemUserInterfaceStyle) { @@ -243,17 +264,11 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager, s autoNightModeTriggered = false } - let effectiveAccentColor = themeSettings.themeSpecificAccentColors[effectiveTheme.index]?.color - themeValue = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: effectiveTheme, accentColor: effectiveAccentColor, serviceBackgroundColor: defaultServiceBackgroundColor, baseColor: themeSettings.themeSpecificAccentColors[effectiveTheme.index]?.baseColor ?? .blue) ?? defaultPresentationTheme + let effectiveColors = themeSettings.themeSpecificAccentColors[effectiveTheme.index] + let theme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: effectiveTheme, accentColor: effectiveColors?.color, bubbleColors: effectiveColors?.customBubbleColors) ?? defaultPresentationTheme - if effectiveTheme != themeSettings.theme { - switch effectiveChatWallpaper { - case .builtin, .color: - effectiveChatWallpaper = themeValue.chat.defaultWallpaper - default: - break - } - } + + let effectiveChatWallpaper: TelegramWallpaper = (themeSettings.themeSpecificChatWallpapers[coloredThemeIndex(reference: effectiveTheme, accentColor: effectiveColors)] ?? themeSettings.themeSpecificChatWallpapers[effectiveTheme.index]) ?? theme.chat.defaultWallpaper let dateTimeFormat = currentDateTimeFormat() let stringsValue: PresentationStrings @@ -264,7 +279,12 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager, s } let nameDisplayOrder = contactSettings.nameDisplayOrder let nameSortOrder = currentPersonNameSortOrder() - return InitialPresentationDataAndSettings(presentationData: PresentationData(strings: stringsValue, theme: themeValue, autoNightModeTriggered: autoNightModeTriggered, chatWallpaper: effectiveChatWallpaper, fontSize: themeSettings.fontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, disableAnimations: themeSettings.disableAnimations, largeEmoji: themeSettings.largeEmoji), automaticMediaDownloadSettings: automaticMediaDownloadSettings, callListSettings: callListSettings, inAppNotificationSettings: inAppNotificationSettings, mediaInputSettings: mediaInputSettings, experimentalUISettings: experimentalUISettings) + + let (chatFontSize, listsFontSize) = resolveFontSize(settings: themeSettings) + + let chatBubbleCorners = PresentationChatBubbleCorners(mainRadius: CGFloat(themeSettings.chatBubbleSettings.mainRadius), auxiliaryRadius: CGFloat(themeSettings.chatBubbleSettings.auxiliaryRadius), mergeBubbleCorners: themeSettings.chatBubbleSettings.mergeBubbleCorners) + + return InitialPresentationDataAndSettings(presentationData: PresentationData(strings: stringsValue, theme: theme, autoNightModeTriggered: autoNightModeTriggered, chatWallpaper: effectiveChatWallpaper, chatFontSize: chatFontSize, chatBubbleCorners: chatBubbleCorners, listsFontSize: listsFontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, disableAnimations: themeSettings.disableAnimations, largeEmoji: themeSettings.largeEmoji), automaticMediaDownloadSettings: automaticMediaDownloadSettings, autodownloadSettings: autodownloadSettings, callListSettings: callListSettings, inAppNotificationSettings: inAppNotificationSettings, mediaInputSettings: mediaInputSettings, experimentalUISettings: experimentalUISettings) } } @@ -371,22 +391,60 @@ private func serviceColor(for data: Signal) -> Signa } } +public func averageColor(from image: UIImage) -> UIColor { + let context = DrawingContext(size: CGSize(width: 1.0, height: 1.0), scale: 1.0, clear: false) + context.withFlippedContext({ context in + if let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)) + } + }) + return context.colorAt(CGPoint()) +} + public func serviceColor(from image: Signal) -> Signal { return image |> mapToSignal { image -> Signal in if let image = image { - let context = DrawingContext(size: CGSize(width: 1.0, height: 1.0), scale: 1.0, clear: false) - context.withFlippedContext({ context in - if let cgImage = image.cgImage { - context.draw(cgImage, in: CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)) - } - }) - return .single(serviceColor(with: context.colorAt(CGPoint()))) + return .single(serviceColor(with: averageColor(from: image))) } return .complete() } } +public func serviceColor(for wallpaper: (TelegramWallpaper, UIImage?)) -> UIColor { + switch wallpaper.0 { + case .builtin: + return UIColor(rgb: 0x748391, alpha: 0.45) + case let .color(color): + return serviceColor(with: UIColor(argb: color)) + case let .gradient(topColor, bottomColor, _): + let mixedColor = UIColor(argb: topColor).mixedWith(UIColor(argb: bottomColor), alpha: 0.5) + return serviceColor(with: mixedColor) + case .image: + if let image = wallpaper.1 { + return serviceColor(with: averageColor(from: image)) + } else { + return UIColor(rgb: 0x000000, alpha: 0.3) + } + case let .file(file): + if wallpaper.0.isPattern { + if let color = file.settings.color { + var mixedColor = UIColor(argb: color) + if let bottomColor = file.settings.bottomColor { + mixedColor = mixedColor.mixedWith(UIColor(argb: bottomColor), alpha: 0.5) + } + return serviceColor(with: mixedColor) + } else { + return UIColor(rgb: 0x000000, alpha: 0.3) + } + } else if let image = wallpaper.1 { + return serviceColor(with: averageColor(from: image)) + } else { + return UIColor(rgb: 0x000000, alpha: 0.3) + } + } +} + public func serviceColor(with color: UIColor) -> UIColor { var hue: CGFloat = 0.0 var saturation: CGFloat = 0.0 @@ -413,7 +471,10 @@ public func chatServiceBackgroundColor(wallpaper: TelegramWallpaper, mediaBox: M case .builtin: return .single(UIColor(rgb: 0x748391, alpha: 0.45)) case let .color(color): - return .single(serviceColor(with: UIColor(rgb: UInt32(bitPattern: color)))) + return .single(serviceColor(with: UIColor(argb: color))) + case let .gradient(topColor, bottomColor, _): + let mixedColor = UIColor(argb: topColor).mixedWith(UIColor(rgb: bottomColor), alpha: 0.5) + return .single(serviceColor(with: mixedColor)) case let .image(representations, _): if let largest = largestImageRepresentation(representations) { return Signal { subscriber in @@ -435,9 +496,13 @@ public func chatServiceBackgroundColor(wallpaper: TelegramWallpaper, mediaBox: M return .single(UIColor(rgb: 0x000000, alpha: 0.3)) } case let .file(file): - if file.isPattern { + if wallpaper.isPattern { if let color = file.settings.color { - return .single(serviceColor(with: UIColor(rgb: UInt32(bitPattern: color)))) + var mixedColor = UIColor(argb: color) + if let bottomColor = file.settings.bottomColor { + mixedColor = mixedColor.mixedWith(UIColor(rgb: bottomColor), alpha: 0.5) + } + return .single(serviceColor(with: mixedColor)) } else { return .single(UIColor(rgb: 0x000000, alpha: 0.3)) } @@ -472,14 +537,21 @@ public func updatedPresentationData(accountManager: AccountManager, applicationI let contactSettings: ContactSynchronizationSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.contactSynchronizationSettings] as? ContactSynchronizationSettings ?? ContactSynchronizationSettings.defaultSettings + var currentColors = themeSettings.themeSpecificAccentColors[themeSettings.theme.index] + if let colors = currentColors, colors.baseColor == .theme { + currentColors = nil + } + let themeSpecificWallpaper = (themeSettings.themeSpecificChatWallpapers[coloredThemeIndex(reference: themeSettings.theme, accentColor: currentColors)] ?? themeSettings.themeSpecificChatWallpapers[themeSettings.theme.index]) + let currentWallpaper: TelegramWallpaper - if let themeSpecificWallpaper = themeSettings.themeSpecificChatWallpapers[themeSettings.theme.index] { + if let themeSpecificWallpaper = themeSpecificWallpaper { currentWallpaper = themeSpecificWallpaper } else { - currentWallpaper = themeSettings.chatWallpaper + let theme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: themeSettings.theme, accentColor: currentColors?.color, bubbleColors: currentColors?.customBubbleColors, wallpaper: currentColors?.wallpaper) ?? defaultPresentationTheme + currentWallpaper = theme.chat.defaultWallpaper } - return (.single(UIColor(rgb: 0x000000, alpha: 0.3)) + return (.single(defaultServiceBackgroundColor) |> then(chatServiceBackgroundColor(wallpaper: currentWallpaper, mediaBox: accountManager.mediaBox))) |> mapToSignal { serviceBackgroundColor in return applicationInForeground @@ -489,27 +561,36 @@ public func updatedPresentationData(accountManager: AccountManager, applicationI |> distinctUntilChanged |> map { autoNightModeTriggered in var effectiveTheme: PresentationThemeReference - var effectiveChatWallpaper: TelegramWallpaper = currentWallpaper + var effectiveChatWallpaper = currentWallpaper + var effectiveColors = currentColors + var switchedToNightModeWallpaper = false if autoNightModeTriggered { let automaticTheme = themeSettings.automaticThemeSwitchSetting.theme - if let themeSpecificWallpaper = themeSettings.themeSpecificChatWallpapers[automaticTheme.index] { + effectiveColors = themeSettings.themeSpecificAccentColors[automaticTheme.index] + let themeSpecificWallpaper = (themeSettings.themeSpecificChatWallpapers[coloredThemeIndex(reference: automaticTheme, accentColor: effectiveColors)] ?? themeSettings.themeSpecificChatWallpapers[automaticTheme.index]) + + if let themeSpecificWallpaper = themeSpecificWallpaper { effectiveChatWallpaper = themeSpecificWallpaper + switchedToNightModeWallpaper = true } effectiveTheme = automaticTheme } else { effectiveTheme = themeSettings.theme } - let effectiveAccentColor = themeSettings.themeSpecificAccentColors[effectiveTheme.index]?.color - let themeValue = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: effectiveTheme, accentColor: effectiveAccentColor, serviceBackgroundColor: serviceBackgroundColor, baseColor: themeSettings.themeSpecificAccentColors[effectiveTheme.index]?.baseColor ?? .blue) ?? defaultPresentationTheme + if let colors = effectiveColors, colors.baseColor == .theme { + effectiveColors = nil + } - if effectiveTheme != themeSettings.theme && themeSettings.themeSpecificChatWallpapers[effectiveTheme.index] == nil { + let themeValue = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: effectiveTheme, accentColor: effectiveColors?.color, bubbleColors: effectiveColors?.customBubbleColors, wallpaper: effectiveColors?.wallpaper, serviceBackgroundColor: serviceBackgroundColor) ?? defaultPresentationTheme + + if autoNightModeTriggered && !switchedToNightModeWallpaper { switch effectiveChatWallpaper { - case .builtin, .color: + case .builtin, .color, .gradient: effectiveChatWallpaper = themeValue.chat.defaultWallpaper case let .file(file): - if file.isPattern { + if effectiveChatWallpaper.isPattern { effectiveChatWallpaper = themeValue.chat.defaultWallpaper } default: @@ -534,7 +615,11 @@ public func updatedPresentationData(accountManager: AccountManager, applicationI let nameDisplayOrder = contactSettings.nameDisplayOrder let nameSortOrder = currentPersonNameSortOrder() - return PresentationData(strings: stringsValue, theme: themeValue, autoNightModeTriggered: autoNightModeTriggered, chatWallpaper: effectiveChatWallpaper, fontSize: themeSettings.fontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, disableAnimations: themeSettings.disableAnimations, largeEmoji: themeSettings.largeEmoji) + let (chatFontSize, listsFontSize) = resolveFontSize(settings: themeSettings) + + let chatBubbleCorners = PresentationChatBubbleCorners(mainRadius: CGFloat(themeSettings.chatBubbleSettings.mainRadius), auxiliaryRadius: CGFloat(themeSettings.chatBubbleSettings.auxiliaryRadius), mergeBubbleCorners: themeSettings.chatBubbleSettings.mergeBubbleCorners) + + return PresentationData(strings: stringsValue, theme: themeValue, autoNightModeTriggered: autoNightModeTriggered, chatWallpaper: effectiveChatWallpaper, chatFontSize: chatFontSize, chatBubbleCorners: chatBubbleCorners, listsFontSize: listsFontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, disableAnimations: themeSettings.disableAnimations, largeEmoji: themeSettings.largeEmoji) } } else { return .complete() @@ -544,11 +629,44 @@ public func updatedPresentationData(accountManager: AccountManager, applicationI } } +private func resolveFontSize(settings: PresentationThemeSettings) -> (chat: PresentationFontSize, lists: PresentationFontSize) { + let fontSize: PresentationFontSize + let listsFontSize: PresentationFontSize + if settings.useSystemFont { + let pointSize = UIFont.preferredFont(forTextStyle: .body).pointSize + fontSize = PresentationFontSize(systemFontSize: pointSize) + listsFontSize = fontSize + } else { + fontSize = settings.fontSize + listsFontSize = settings.listsFontSize + } + return (fontSize, listsFontSize) +} + public func defaultPresentationData() -> PresentationData { let dateTimeFormat = currentDateTimeFormat() let nameDisplayOrder: PresentationPersonNameOrder = .firstLast let nameSortOrder = currentPersonNameSortOrder() let themeSettings = PresentationThemeSettings.defaultSettings - return PresentationData(strings: defaultPresentationStrings, theme: defaultPresentationTheme, autoNightModeTriggered: false, chatWallpaper: .builtin(WallpaperSettings()), fontSize: themeSettings.fontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, disableAnimations: themeSettings.disableAnimations, largeEmoji: themeSettings.largeEmoji) + + let (chatFontSize, listsFontSize) = resolveFontSize(settings: themeSettings) + + let chatBubbleCorners = PresentationChatBubbleCorners(mainRadius: CGFloat(themeSettings.chatBubbleSettings.mainRadius), auxiliaryRadius: CGFloat(themeSettings.chatBubbleSettings.auxiliaryRadius), mergeBubbleCorners: themeSettings.chatBubbleSettings.mergeBubbleCorners) + + return PresentationData(strings: defaultPresentationStrings, theme: defaultPresentationTheme, autoNightModeTriggered: false, chatWallpaper: .builtin(WallpaperSettings()), chatFontSize: chatFontSize, chatBubbleCorners: chatBubbleCorners, listsFontSize: listsFontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, disableAnimations: themeSettings.disableAnimations, largeEmoji: themeSettings.largeEmoji) +} + +public extension PresentationData { + func withFontSizes(chatFontSize: PresentationFontSize, listsFontSize: PresentationFontSize) -> PresentationData { + return PresentationData(strings: self.strings, theme: self.theme, autoNightModeTriggered: self.autoNightModeTriggered, chatWallpaper: self.chatWallpaper, chatFontSize: chatFontSize, chatBubbleCorners: self.chatBubbleCorners, listsFontSize: listsFontSize, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, nameSortOrder: self.nameSortOrder, disableAnimations: self.disableAnimations, largeEmoji: self.largeEmoji) + } + + func withChatBubbleCorners(_ chatBubbleCorners: PresentationChatBubbleCorners) -> PresentationData { + return PresentationData(strings: self.strings, theme: self.theme, autoNightModeTriggered: self.autoNightModeTriggered, chatWallpaper: self.chatWallpaper, chatFontSize: self.chatFontSize, chatBubbleCorners: chatBubbleCorners, listsFontSize: self.listsFontSize, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, nameSortOrder: self.nameSortOrder, disableAnimations: self.disableAnimations, largeEmoji: self.largeEmoji) + } + + func withStrings(_ strings: PresentationStrings) -> PresentationData { + return PresentationData(strings: strings, theme: self.theme, autoNightModeTriggered: self.autoNightModeTriggered, chatWallpaper: self.chatWallpaper, chatFontSize: self.chatFontSize, chatBubbleCorners: chatBubbleCorners, listsFontSize: self.listsFontSize, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, nameSortOrder: self.nameSortOrder, disableAnimations: self.disableAnimations, largeEmoji: self.largeEmoji) + } } diff --git a/submodules/TelegramPresentationData/Sources/PresentationStrings.swift b/submodules/TelegramPresentationData/Sources/PresentationStrings.swift index 4fd4d06b1d..df5ea4620e 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationStrings.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationStrings.swift @@ -192,4753 +192,4932 @@ public final class PresentationStrings: Equatable { public var Map_PullUpForPlaces: String { return self._s[2]! } public var Channel_Status: String { return self._s[4]! } public var Wallet_Updated_JustNow: String { return self._s[5]! } - public var TwoStepAuth_ChangePassword: String { return self._s[6]! } - public var Map_LiveLocationFor1Hour: String { return self._s[7]! } - public var CheckoutInfo_ShippingInfoAddress2Placeholder: String { return self._s[8]! } - public var Settings_AppleWatch: String { return self._s[9]! } - public var Login_InvalidCountryCode: String { return self._s[10]! } - public var WebSearch_RecentSectionTitle: String { return self._s[11]! } - public var UserInfo_DeleteContact: String { return self._s[12]! } - public var ShareFileTip_CloseTip: String { return self._s[13]! } - public var UserInfo_Invite: String { return self._s[14]! } - public var Passport_Identity_MiddleName: String { return self._s[15]! } - public var Passport_Identity_FrontSideHelp: String { return self._s[16]! } - public var Month_GenDecember: String { return self._s[18]! } - public var Common_Yes: String { return self._s[19]! } + public var TwoStepAuth_ChangePassword: String { return self._s[7]! } + public var Map_LiveLocationFor1Hour: String { return self._s[8]! } + public var CheckoutInfo_ShippingInfoAddress2Placeholder: String { return self._s[9]! } + public var Settings_AppleWatch: String { return self._s[10]! } + public var Login_InvalidCountryCode: String { return self._s[11]! } + public var WebSearch_RecentSectionTitle: String { return self._s[12]! } + public var UserInfo_DeleteContact: String { return self._s[13]! } + public var ShareFileTip_CloseTip: String { return self._s[14]! } + public var UserInfo_Invite: String { return self._s[15]! } + public var Passport_Identity_MiddleName: String { return self._s[16]! } + public var Passport_Identity_FrontSideHelp: String { return self._s[17]! } + public var Month_GenDecember: String { return self._s[19]! } + public var Common_Yes: String { return self._s[20]! } public func EncryptionKey_Description(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[20]!, self._r[20]!, [_1, _2]) + return formatWithArgumentRanges(self._s[21]!, self._r[21]!, [_1, _2]) } - public var Channel_AdminLogFilter_EventsLeaving: String { return self._s[21]! } - public var WallpaperPreview_PreviewBottomText: String { return self._s[22]! } + public var Channel_AdminLogFilter_EventsLeaving: String { return self._s[22]! } + public var WallpaperPreview_PreviewBottomText: String { return self._s[23]! } public func Notification_PinnedStickerMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[23]!, self._r[23]!, [_0]) + return formatWithArgumentRanges(self._s[24]!, self._r[24]!, [_0]) } - public var Passport_Address_ScansHelp: String { return self._s[24]! } - public var FastTwoStepSetup_PasswordHelp: String { return self._s[25]! } - public var SettingsSearch_Synonyms_Notifications_Title: String { return self._s[26]! } - public var StickerPacksSettings_AnimatedStickers: String { return self._s[27]! } - public var Wallet_WordCheck_IncorrectText: String { return self._s[28]! } + public var Passport_Address_ScansHelp: String { return self._s[25]! } + public var FastTwoStepSetup_PasswordHelp: String { return self._s[26]! } + public var SettingsSearch_Synonyms_Notifications_Title: String { return self._s[27]! } + public var StickerPacksSettings_AnimatedStickers: String { return self._s[28]! } + public var Wallet_WordCheck_IncorrectText: String { return self._s[29]! } public func Items_NOfM(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[29]!, self._r[29]!, [_1, _2]) + return formatWithArgumentRanges(self._s[30]!, self._r[30]!, [_1, _2]) } - public var AutoDownloadSettings_Files: String { return self._s[30]! } - public var TextFormat_AddLinkPlaceholder: String { return self._s[31]! } - public var LastSeen_Lately: String { return self._s[36]! } + public var AutoDownloadSettings_Files: String { return self._s[31]! } + public var TextFormat_AddLinkPlaceholder: String { return self._s[32]! } + public var LastSeen_Lately: String { return self._s[37]! } public func PUSH_CHANNEL_MESSAGE_VIDEOS(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[37]!, self._r[37]!, [_1, _2]) + return formatWithArgumentRanges(self._s[38]!, self._r[38]!, [_1, _2]) } - public var Camera_Discard: String { return self._s[38]! } - public var Channel_EditAdmin_PermissinAddAdminOff: String { return self._s[39]! } - public var Login_InvalidPhoneError: String { return self._s[41]! } - public var SettingsSearch_Synonyms_Privacy_AuthSessions: String { return self._s[42]! } - public var GroupInfo_LabelOwner: String { return self._s[43]! } - public var Conversation_Moderate_Delete: String { return self._s[44]! } - public var ClearCache_ClearCache: String { return self._s[45]! } - public var Conversation_DeleteMessagesForEveryone: String { return self._s[46]! } - public var WatchRemote_AlertOpen: String { return self._s[47]! } + public var Camera_Discard: String { return self._s[39]! } + public var Channel_EditAdmin_PermissinAddAdminOff: String { return self._s[40]! } + public var Login_InvalidPhoneError: String { return self._s[42]! } + public var SettingsSearch_Synonyms_Privacy_AuthSessions: String { return self._s[43]! } + public var GroupInfo_LabelOwner: String { return self._s[44]! } + public var Conversation_Moderate_Delete: String { return self._s[45]! } + public var ClearCache_ClearCache: String { return self._s[46]! } + public var Conversation_DeleteMessagesForEveryone: String { return self._s[47]! } + public var WatchRemote_AlertOpen: String { return self._s[48]! } public func MediaPicker_Nof(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[48]!, self._r[48]!, [_0]) + return formatWithArgumentRanges(self._s[49]!, self._r[49]!, [_0]) } - public var EditTheme_Expand_Preview_IncomingReplyName: String { return self._s[49]! } - public var AutoDownloadSettings_MediaTypes: String { return self._s[51]! } - public var Watch_GroupInfo_Title: String { return self._s[52]! } - public var Passport_Identity_AddPersonalDetails: String { return self._s[53]! } - public var Channel_Info_Members: String { return self._s[54]! } - public var LoginPassword_InvalidPasswordError: String { return self._s[56]! } - public var Conversation_LiveLocation: String { return self._s[57]! } - public var Wallet_Month_ShortNovember: String { return self._s[58]! } - public var PrivacyLastSeenSettings_CustomShareSettingsHelp: String { return self._s[59]! } - public var NetworkUsageSettings_BytesReceived: String { return self._s[61]! } - public var Stickers_Search: String { return self._s[63]! } - public var NotificationsSound_Synth: String { return self._s[64]! } - public var LogoutOptions_LogOutInfo: String { return self._s[65]! } + public var ChatState_ConnectingToProxy: String { return self._s[50]! } + public var EditTheme_Expand_Preview_IncomingReplyName: String { return self._s[51]! } + public var AutoDownloadSettings_MediaTypes: String { return self._s[53]! } + public var Watch_GroupInfo_Title: String { return self._s[54]! } + public var Passport_Identity_AddPersonalDetails: String { return self._s[55]! } + public var Channel_Info_Members: String { return self._s[56]! } + public var LoginPassword_InvalidPasswordError: String { return self._s[58]! } + public var Conversation_LiveLocation: String { return self._s[59]! } + public var Wallet_Month_ShortNovember: String { return self._s[60]! } + public var PrivacyLastSeenSettings_CustomShareSettingsHelp: String { return self._s[61]! } + public var NetworkUsageSettings_BytesReceived: String { return self._s[63]! } + public var Stickers_Search: String { return self._s[65]! } + public var NotificationsSound_Synth: String { return self._s[66]! } + public var LogoutOptions_LogOutInfo: String { return self._s[67]! } public func VoiceOver_Chat_ForwardedFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[67]!, self._r[67]!, [_0]) + return formatWithArgumentRanges(self._s[69]!, self._r[69]!, [_0]) } - public var NetworkUsageSettings_MediaAudioDataSection: String { return self._s[68]! } - public var ChatList_Context_HideArchive: String { return self._s[70]! } - public var AutoNightTheme_UseSunsetSunrise: String { return self._s[71]! } - public var FastTwoStepSetup_Title: String { return self._s[72]! } - public var EditTheme_Create_Preview_IncomingReplyText: String { return self._s[73]! } - public var Channel_Info_BlackList: String { return self._s[74]! } - public var Channel_AdminLog_InfoPanelTitle: String { return self._s[75]! } - public var Conversation_OpenFile: String { return self._s[77]! } - public var SecretTimer_ImageDescription: String { return self._s[78]! } - public var StickerSettings_ContextInfo: String { return self._s[79]! } - public var TwoStepAuth_GenericHelp: String { return self._s[81]! } - public var AutoDownloadSettings_Unlimited: String { return self._s[82]! } - public var PrivacyLastSeenSettings_NeverShareWith_Title: String { return self._s[83]! } - public var AutoDownloadSettings_DataUsageHigh: String { return self._s[84]! } + public var NetworkUsageSettings_MediaAudioDataSection: String { return self._s[70]! } + public var ChatList_Context_HideArchive: String { return self._s[72]! } + public var AutoNightTheme_UseSunsetSunrise: String { return self._s[73]! } + public var FastTwoStepSetup_Title: String { return self._s[74]! } + public var EditTheme_Create_Preview_IncomingReplyText: String { return self._s[75]! } + public var Channel_Info_BlackList: String { return self._s[76]! } + public var Channel_AdminLog_InfoPanelTitle: String { return self._s[77]! } + public var Conversation_OpenFile: String { return self._s[79]! } + public var SecretTimer_ImageDescription: String { return self._s[80]! } + public var StickerSettings_ContextInfo: String { return self._s[81]! } + public var TwoStepAuth_GenericHelp: String { return self._s[83]! } + public var AutoDownloadSettings_Unlimited: String { return self._s[84]! } + public var PrivacyLastSeenSettings_NeverShareWith_Title: String { return self._s[85]! } + public var AutoDownloadSettings_DataUsageHigh: String { return self._s[86]! } public func PUSH_CHAT_MESSAGE_VIDEO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[85]!, self._r[85]!, [_1, _2]) + return formatWithArgumentRanges(self._s[87]!, self._r[87]!, [_1, _2]) } - public var Notifications_AddExceptionTitle: String { return self._s[86]! } - public var Watch_MessageView_Reply: String { return self._s[87]! } - public var Tour_Text6: String { return self._s[88]! } - public var TwoStepAuth_SetupPasswordEnterPasswordChange: String { return self._s[89]! } + public var AuthSessions_AddDevice_ScanInfo: String { return self._s[88]! } + public var Notifications_AddExceptionTitle: String { return self._s[89]! } + public var Watch_MessageView_Reply: String { return self._s[90]! } + public var Tour_Text6: String { return self._s[91]! } + public var TwoStepAuth_SetupPasswordEnterPasswordChange: String { return self._s[92]! } public func Notification_PinnedAnimationMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[90]!, self._r[90]!, [_0]) + return formatWithArgumentRanges(self._s[93]!, self._r[93]!, [_0]) } public func ShareFileTip_Text(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[91]!, self._r[91]!, [_0]) + return formatWithArgumentRanges(self._s[94]!, self._r[94]!, [_0]) } - public var Wallet_Configuration_BlockchainIdPlaceholder: String { return self._s[92]! } - public var AccessDenied_LocationDenied: String { return self._s[93]! } - public var CallSettings_RecentCalls: String { return self._s[94]! } - public var ConversationProfile_LeaveDeleteAndExit: String { return self._s[95]! } - public var Channel_Members_AddAdminErrorBlacklisted: String { return self._s[96]! } - public var Passport_Authorize: String { return self._s[97]! } - public var StickerPacksSettings_ArchivedMasks_Info: String { return self._s[98]! } - public var AutoDownloadSettings_Videos: String { return self._s[99]! } - public var TwoStepAuth_ReEnterPasswordTitle: String { return self._s[100]! } - public var Wallet_Info_Send: String { return self._s[101]! } - public var Wallet_TransactionInfo_SendGrams: String { return self._s[102]! } - public var Tour_StartButton: String { return self._s[103]! } - public var Watch_AppName: String { return self._s[105]! } - public var StickerPack_ErrorNotFound: String { return self._s[106]! } - public var Channel_Info_Subscribers: String { return self._s[107]! } + public var Wallet_Configuration_BlockchainIdPlaceholder: String { return self._s[95]! } + public var AccessDenied_LocationDenied: String { return self._s[96]! } + public var CallSettings_RecentCalls: String { return self._s[97]! } + public var ConversationProfile_LeaveDeleteAndExit: String { return self._s[98]! } + public var Channel_Members_AddAdminErrorBlacklisted: String { return self._s[100]! } + public var Passport_Authorize: String { return self._s[101]! } + public var StickerPacksSettings_ArchivedMasks_Info: String { return self._s[102]! } + public var AutoDownloadSettings_Videos: String { return self._s[103]! } + public var TwoStepAuth_ReEnterPasswordTitle: String { return self._s[104]! } + public var Wallet_Info_Send: String { return self._s[105]! } + public var AuthSessions_AddDevice_UrlLoginHint: String { return self._s[106]! } + public var Wallet_TransactionInfo_SendGrams: String { return self._s[107]! } + public var Tour_StartButton: String { return self._s[108]! } + public var Watch_AppName: String { return self._s[110]! } + public var StickerPack_ErrorNotFound: String { return self._s[111]! } + public var Channel_Info_Subscribers: String { return self._s[112]! } public func Channel_AdminLog_MessageGroupPreHistoryVisible(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[108]!, self._r[108]!, [_0]) + return formatWithArgumentRanges(self._s[113]!, self._r[113]!, [_0]) } public func DialogList_PinLimitError(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[109]!, self._r[109]!, [_0]) + return formatWithArgumentRanges(self._s[114]!, self._r[114]!, [_0]) } - public var Appearance_RemoveTheme: String { return self._s[110]! } + public var Appearance_RemoveTheme: String { return self._s[115]! } public func Wallet_Info_TransactionBlockchainFee(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[111]!, self._r[111]!, [_0]) + return formatWithArgumentRanges(self._s[116]!, self._r[116]!, [_0]) } - public var Conversation_StopLiveLocation: String { return self._s[113]! } - public var Channel_AdminLogFilter_EventsAll: String { return self._s[114]! } - public var GroupInfo_InviteLink_CopyAlert_Success: String { return self._s[116]! } - public var Username_LinkCopied: String { return self._s[118]! } - public var GroupRemoved_Title: String { return self._s[119]! } - public var SecretVideo_Title: String { return self._s[120]! } + public var Conversation_StopLiveLocation: String { return self._s[119]! } + public var Channel_AdminLogFilter_EventsAll: String { return self._s[120]! } + public var GroupInfo_InviteLink_CopyAlert_Success: String { return self._s[122]! } + public var Username_LinkCopied: String { return self._s[124]! } + public var GroupRemoved_Title: String { return self._s[125]! } + public var SecretVideo_Title: String { return self._s[126]! } public func PUSH_PINNED_VIDEO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[121]!, self._r[121]!, [_1]) + return formatWithArgumentRanges(self._s[127]!, self._r[127]!, [_1]) } - public var AccessDenied_PhotosAndVideos: String { return self._s[122]! } - public var Appearance_ThemePreview_Chat_1_Text: String { return self._s[123]! } + public var AccessDenied_PhotosAndVideos: String { return self._s[128]! } + public var Appearance_ThemePreview_Chat_1_Text: String { return self._s[129]! } public func PUSH_CHANNEL_MESSAGE_GEOLIVE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[125]!, self._r[125]!, [_1]) + return formatWithArgumentRanges(self._s[131]!, self._r[131]!, [_1]) } - public var Map_OpenInGoogleMaps: String { return self._s[126]! } + public var Map_OpenInGoogleMaps: String { return self._s[133]! } public func Time_PreciseDate_m12(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[127]!, self._r[127]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[134]!, self._r[134]!, [_1, _2, _3]) } public func Channel_AdminLog_MessageKickedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[128]!, self._r[128]!, [_1, _2]) + return formatWithArgumentRanges(self._s[135]!, self._r[135]!, [_1, _2]) } - public var Call_StatusRinging: String { return self._s[129]! } - public var SettingsSearch_Synonyms_EditProfile_Username: String { return self._s[130]! } - public var Group_Username_InvalidStartsWithNumber: String { return self._s[131]! } - public var UserInfo_NotificationsEnabled: String { return self._s[132]! } - public var Map_Search: String { return self._s[133]! } - public var ClearCache_StorageFree: String { return self._s[135]! } - public var Login_TermsOfServiceHeader: String { return self._s[136]! } + public var Call_StatusRinging: String { return self._s[136]! } + public var SettingsSearch_Synonyms_EditProfile_Username: String { return self._s[137]! } + public var Group_Username_InvalidStartsWithNumber: String { return self._s[138]! } + public var UserInfo_NotificationsEnabled: String { return self._s[139]! } + public var PeopleNearby_MakeVisibleDescription: String { return self._s[140]! } + public var Map_Search: String { return self._s[141]! } + public var ClearCache_StorageFree: String { return self._s[143]! } + public var Login_TermsOfServiceHeader: String { return self._s[144]! } public func Notification_PinnedVideoMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[137]!, self._r[137]!, [_0]) + return formatWithArgumentRanges(self._s[145]!, self._r[145]!, [_0]) } public func Channel_AdminLog_MessageToggleSignaturesOn(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[138]!, self._r[138]!, [_0]) + return formatWithArgumentRanges(self._s[147]!, self._r[147]!, [_0]) } - public var Wallet_Sent_Title: String { return self._s[139]! } - public var TwoStepAuth_SetupPasswordConfirmPassword: String { return self._s[140]! } - public var Weekday_Today: String { return self._s[141]! } + public var Wallet_Sent_Title: String { return self._s[148]! } + public var TwoStepAuth_SetupPasswordConfirmPassword: String { return self._s[149]! } + public var Weekday_Today: String { return self._s[150]! } public func InstantPage_AuthorAndDateTitle(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[143]!, self._r[143]!, [_1, _2]) + return formatWithArgumentRanges(self._s[152]!, self._r[152]!, [_1, _2]) } public func Conversation_MessageDialogRetryAll(_ _1: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[144]!, self._r[144]!, ["\(_1)"]) + return formatWithArgumentRanges(self._s[153]!, self._r[153]!, ["\(_1)"]) } - public var Notification_PassportValuePersonalDetails: String { return self._s[146]! } - public var Channel_AdminLog_MessagePreviousLink: String { return self._s[147]! } - public var ChangePhoneNumberNumber_NewNumber: String { return self._s[148]! } - public var ApplyLanguage_LanguageNotSupportedError: String { return self._s[149]! } - public var TwoStepAuth_ChangePasswordDescription: String { return self._s[150]! } - public var PhotoEditor_BlurToolLinear: String { return self._s[151]! } - public var Contacts_PermissionsAllowInSettings: String { return self._s[152]! } - public var Weekday_ShortMonday: String { return self._s[153]! } - public var Cache_KeepMedia: String { return self._s[154]! } - public var Passport_FieldIdentitySelfieHelp: String { return self._s[155]! } + public var Notification_PassportValuePersonalDetails: String { return self._s[155]! } + public var Channel_AdminLog_MessagePreviousLink: String { return self._s[156]! } + public var ChangePhoneNumberNumber_NewNumber: String { return self._s[157]! } + public var ApplyLanguage_LanguageNotSupportedError: String { return self._s[158]! } + public var TwoStepAuth_ChangePasswordDescription: String { return self._s[159]! } + public var PhotoEditor_BlurToolLinear: String { return self._s[160]! } + public var Contacts_PermissionsAllowInSettings: String { return self._s[161]! } + public var Weekday_ShortMonday: String { return self._s[162]! } + public var Cache_KeepMedia: String { return self._s[163]! } + public var Passport_FieldIdentitySelfieHelp: String { return self._s[164]! } public func PUSH_PINNED_STICKER(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[156]!, self._r[156]!, [_1, _2]) + return formatWithArgumentRanges(self._s[165]!, self._r[165]!, [_1, _2]) } public func Chat_SlowmodeTooltip(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[157]!, self._r[157]!, [_0]) + return formatWithArgumentRanges(self._s[166]!, self._r[166]!, [_0]) } - public var Wallet_Receive_ShareUrlInfo: String { return self._s[158]! } - public var Conversation_ClousStorageInfo_Description4: String { return self._s[159]! } - public var Wallet_RestoreFailed_Title: String { return self._s[160]! } - public var Passport_Language_ru: String { return self._s[161]! } + public var Wallet_Receive_ShareUrlInfo: String { return self._s[167]! } + public var Conversation_ClousStorageInfo_Description4: String { return self._s[168]! } + public var Wallet_RestoreFailed_Title: String { return self._s[169]! } + public var Passport_Language_ru: String { return self._s[170]! } public func Notification_CreatedChatWithTitle(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[162]!, self._r[162]!, [_0, _1]) + return formatWithArgumentRanges(self._s[171]!, self._r[171]!, [_0, _1]) } - public var WallpaperPreview_PatternIntensity: String { return self._s[163]! } - public var WebBrowser_InAppSafari: String { return self._s[166]! } - public var TwoStepAuth_RecoveryUnavailable: String { return self._s[167]! } - public var EnterPasscode_TouchId: String { return self._s[168]! } - public var PhotoEditor_QualityVeryHigh: String { return self._s[171]! } - public var Checkout_NewCard_SaveInfo: String { return self._s[173]! } - public var Gif_NoGifsPlaceholder: String { return self._s[175]! } - public var Conversation_OpenBotLinkTitle: String { return self._s[177]! } - public var ChatSettings_AutoDownloadEnabled: String { return self._s[178]! } - public var NetworkUsageSettings_BytesSent: String { return self._s[179]! } - public var Checkout_PasswordEntry_Pay: String { return self._s[180]! } - public var AuthSessions_TerminateSession: String { return self._s[181]! } - public var Message_File: String { return self._s[182]! } - public var MediaPicker_VideoMuteDescription: String { return self._s[183]! } - public var SocksProxySetup_ProxyStatusConnected: String { return self._s[184]! } - public var TwoStepAuth_RecoveryCode: String { return self._s[185]! } - public var EnterPasscode_EnterCurrentPasscode: String { return self._s[186]! } + public var WallpaperPreview_PatternIntensity: String { return self._s[172]! } + public var WebBrowser_InAppSafari: String { return self._s[175]! } + public var TwoStepAuth_RecoveryUnavailable: String { return self._s[176]! } + public var EnterPasscode_TouchId: String { return self._s[177]! } + public var PhotoEditor_QualityVeryHigh: String { return self._s[180]! } + public var Checkout_NewCard_SaveInfo: String { return self._s[182]! } + public var Gif_NoGifsPlaceholder: String { return self._s[184]! } + public func Notification_InvitedMultiple(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[186]!, self._r[186]!, [_0, _1]) + } + public var ChatSettings_AutoDownloadEnabled: String { return self._s[187]! } + public var NetworkUsageSettings_BytesSent: String { return self._s[188]! } + public var Checkout_PasswordEntry_Pay: String { return self._s[189]! } + public var AuthSessions_TerminateSession: String { return self._s[190]! } + public var Message_File: String { return self._s[191]! } + public var MediaPicker_VideoMuteDescription: String { return self._s[192]! } + public var SocksProxySetup_ProxyStatusConnected: String { return self._s[193]! } + public var TwoStepAuth_RecoveryCode: String { return self._s[194]! } + public var EnterPasscode_EnterCurrentPasscode: String { return self._s[195]! } public func TwoStepAuth_EnterPasswordHint(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[187]!, self._r[187]!, [_0]) + return formatWithArgumentRanges(self._s[196]!, self._r[196]!, [_0]) } - public var Conversation_Moderate_Report: String { return self._s[189]! } - public var TwoStepAuth_EmailInvalid: String { return self._s[190]! } - public var Passport_Language_ms: String { return self._s[191]! } - public var Channel_Edit_AboutItem: String { return self._s[193]! } - public var DialogList_SearchSectionGlobal: String { return self._s[197]! } - public var AttachmentMenu_WebSearch: String { return self._s[198]! } - public var PasscodeSettings_TurnPasscodeOn: String { return self._s[199]! } - public var Channel_BanUser_Title: String { return self._s[200]! } - public var WallpaperPreview_SwipeTopText: String { return self._s[201]! } - public var ChatList_DeleteSavedMessagesConfirmationText: String { return self._s[202]! } - public var ArchivedChats_IntroText2: String { return self._s[203]! } - public var Notification_Exceptions_DeleteAll: String { return self._s[204]! } - public var ChatSearch_SearchPlaceholder: String { return self._s[206]! } - public func Channel_AdminLog_MessageTransferedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[207]!, self._r[207]!, [_1, _2]) - } - public var Passport_FieldAddressTranslationHelp: String { return self._s[208]! } - public var NotificationsSound_Aurora: String { return self._s[209]! } + public var Conversation_Moderate_Report: String { return self._s[198]! } + public var TwoStepAuth_EmailInvalid: String { return self._s[199]! } + public var Passport_Language_ms: String { return self._s[200]! } + public var Channel_Edit_AboutItem: String { return self._s[202]! } + public var DialogList_SearchSectionGlobal: String { return self._s[206]! } + public var AttachmentMenu_WebSearch: String { return self._s[207]! } + public var ChatState_WaitingForNetwork: String { return self._s[208]! } + public var Channel_BanUser_Title: String { return self._s[209]! } + public var PasscodeSettings_TurnPasscodeOn: String { return self._s[210]! } + public var WallpaperPreview_SwipeTopText: String { return self._s[211]! } + public var ChatList_DeleteSavedMessagesConfirmationText: String { return self._s[212]! } + public var ArchivedChats_IntroText2: String { return self._s[213]! } + public var ChatSearch_SearchPlaceholder: String { return self._s[215]! } + public var Conversation_OpenBotLinkTitle: String { return self._s[216]! } + public var Passport_FieldAddressTranslationHelp: String { return self._s[217]! } + public var NotificationsSound_Aurora: String { return self._s[218]! } + public var Notification_Exceptions_DeleteAll: String { return self._s[219]! } public func FileSize_GB(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[210]!, self._r[210]!, [_0]) + return formatWithArgumentRanges(self._s[220]!, self._r[220]!, [_0]) } - public var AuthSessions_LoggedInWithTelegram: String { return self._s[213]! } + public var AuthSessions_LoggedInWithTelegram: String { return self._s[223]! } public func Privacy_GroupsAndChannels_InviteToGroupError(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[214]!, self._r[214]!, [_0, _1]) + return formatWithArgumentRanges(self._s[224]!, self._r[224]!, [_0, _1]) } - public var Passport_PasswordNext: String { return self._s[215]! } - public var Bot_GroupStatusReadsHistory: String { return self._s[216]! } - public var EmptyGroupInfo_Line2: String { return self._s[217]! } - public var VoiceOver_Chat_SeenByRecipients: String { return self._s[218]! } - public var Settings_FAQ_Intro: String { return self._s[221]! } - public var PrivacySettings_PasscodeAndTouchId: String { return self._s[223]! } - public var FeaturedStickerPacks_Title: String { return self._s[224]! } - public var TwoStepAuth_PasswordRemoveConfirmation: String { return self._s[226]! } - public var Username_Title: String { return self._s[227]! } + public var Passport_PasswordNext: String { return self._s[225]! } + public var Bot_GroupStatusReadsHistory: String { return self._s[226]! } + public var EmptyGroupInfo_Line2: String { return self._s[227]! } + public func Channel_AdminLog_MessageTransferedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[228]!, self._r[228]!, [_1, _2]) + } + public var VoiceOver_Chat_SeenByRecipients: String { return self._s[229]! } + public var Settings_FAQ_Intro: String { return self._s[232]! } + public var PrivacySettings_PasscodeAndTouchId: String { return self._s[234]! } + public var FeaturedStickerPacks_Title: String { return self._s[235]! } + public var TwoStepAuth_PasswordRemoveConfirmation: String { return self._s[237]! } + public var Username_Title: String { return self._s[238]! } public func Message_StickerText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[228]!, self._r[228]!, [_0]) + return formatWithArgumentRanges(self._s[239]!, self._r[239]!, [_0]) } - public var PasscodeSettings_AlphanumericCode: String { return self._s[229]! } - public var Localization_LanguageOther: String { return self._s[230]! } - public var Stickers_SuggestStickers: String { return self._s[231]! } + public var PeerInfo_PaneFiles: String { return self._s[240]! } + public var PasscodeSettings_AlphanumericCode: String { return self._s[241]! } + public var Localization_LanguageOther: String { return self._s[242]! } + public var Stickers_SuggestStickers: String { return self._s[243]! } public func Channel_AdminLog_MessageRemovedGroupUsername(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[232]!, self._r[232]!, [_0]) + return formatWithArgumentRanges(self._s[244]!, self._r[244]!, [_0]) } - public var NotificationSettings_ShowNotificationsFromAccountsSection: String { return self._s[233]! } - public var Channel_AdminLogFilter_EventsAdmins: String { return self._s[234]! } - public var Conversation_DefaultRestrictedStickers: String { return self._s[235]! } + public var NotificationSettings_ShowNotificationsFromAccountsSection: String { return self._s[245]! } + public var Channel_AdminLogFilter_EventsAdmins: String { return self._s[246]! } + public var Conversation_DefaultRestrictedStickers: String { return self._s[247]! } public func Notification_PinnedDeletedMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[236]!, self._r[236]!, [_0]) + return formatWithArgumentRanges(self._s[248]!, self._r[248]!, [_0]) } - public var Wallet_TransactionInfo_CopyAddress: String { return self._s[238]! } - public var Group_UpgradeConfirmation: String { return self._s[239]! } - public var DialogList_Unpin: String { return self._s[240]! } - public var Passport_Identity_DateOfBirth: String { return self._s[241]! } - public var Month_ShortOctober: String { return self._s[242]! } - public var SettingsSearch_Synonyms_Privacy_Data_ContactsSync: String { return self._s[243]! } - public var TwoFactorSetup_Done_Text: String { return self._s[244]! } - public var Notification_CallCanceledShort: String { return self._s[245]! } - public var Passport_Phone_Help: String { return self._s[246]! } - public var Passport_Language_az: String { return self._s[248]! } - public var CreatePoll_TextPlaceholder: String { return self._s[250]! } - public var VoiceOver_Chat_AnonymousPoll: String { return self._s[251]! } - public var Passport_Identity_DocumentNumber: String { return self._s[252]! } - public var PhotoEditor_CurvesRed: String { return self._s[253]! } - public var PhoneNumberHelp_Alert: String { return self._s[255]! } - public var SocksProxySetup_Port: String { return self._s[256]! } - public var Checkout_PayNone: String { return self._s[257]! } - public var AutoDownloadSettings_WiFi: String { return self._s[258]! } - public var GroupInfo_GroupType: String { return self._s[259]! } - public var StickerSettings_ContextHide: String { return self._s[260]! } - public var Passport_Address_OneOfTypeTemporaryRegistration: String { return self._s[261]! } - public var Group_Setup_HistoryTitle: String { return self._s[263]! } - public var Passport_Identity_FilesUploadNew: String { return self._s[264]! } - public var PasscodeSettings_AutoLock: String { return self._s[265]! } - public var Passport_Title: String { return self._s[266]! } - public var VoiceOver_Chat_ContactPhoneNumber: String { return self._s[267]! } - public var Channel_AdminLogFilter_EventsNewSubscribers: String { return self._s[268]! } - public var GroupPermission_NoSendGifs: String { return self._s[269]! } - public var PrivacySettings_PasscodeOn: String { return self._s[270]! } + public var Wallet_TransactionInfo_CopyAddress: String { return self._s[250]! } + public var Group_UpgradeConfirmation: String { return self._s[251]! } + public var DialogList_Unpin: String { return self._s[252]! } + public var Passport_Identity_DateOfBirth: String { return self._s[253]! } + public var Month_ShortOctober: String { return self._s[254]! } + public var SettingsSearch_Synonyms_Privacy_Data_ContactsSync: String { return self._s[255]! } + public var TwoFactorSetup_Done_Text: String { return self._s[256]! } + public var Notification_CallCanceledShort: String { return self._s[257]! } + public var Conversation_StopQuiz: String { return self._s[258]! } + public var Passport_Phone_Help: String { return self._s[259]! } + public var Passport_Language_az: String { return self._s[261]! } + public var CreatePoll_TextPlaceholder: String { return self._s[263]! } + public var VoiceOver_Chat_AnonymousPoll: String { return self._s[264]! } + public var Passport_Identity_DocumentNumber: String { return self._s[265]! } + public var PhotoEditor_CurvesRed: String { return self._s[266]! } + public var PhoneNumberHelp_Alert: String { return self._s[268]! } + public var SocksProxySetup_Port: String { return self._s[269]! } + public var Checkout_PayNone: String { return self._s[270]! } + public var AutoDownloadSettings_WiFi: String { return self._s[271]! } + public var GroupInfo_GroupType: String { return self._s[272]! } + public var StickerSettings_ContextHide: String { return self._s[273]! } + public var Passport_Address_OneOfTypeTemporaryRegistration: String { return self._s[274]! } + public var Group_Setup_HistoryTitle: String { return self._s[276]! } + public var Passport_Identity_FilesUploadNew: String { return self._s[277]! } + public var PasscodeSettings_AutoLock: String { return self._s[278]! } + public var Passport_Title: String { return self._s[279]! } + public var VoiceOver_Chat_ContactPhoneNumber: String { return self._s[280]! } + public var Channel_AdminLogFilter_EventsNewSubscribers: String { return self._s[281]! } + public var GroupPermission_NoSendGifs: String { return self._s[282]! } + public var PrivacySettings_PasscodeOn: String { return self._s[283]! } public func Conversation_ScheduleMessage_SendTomorrow(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[271]!, self._r[271]!, [_0]) + return formatWithArgumentRanges(self._s[284]!, self._r[284]!, [_0]) } - public var State_WaitingForNetwork: String { return self._s[273]! } + public var State_WaitingForNetwork: String { return self._s[287]! } public func Notification_Invited(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[274]!, self._r[274]!, [_0, _1]) + return formatWithArgumentRanges(self._s[288]!, self._r[288]!, [_0, _1]) } - public var Calls_NotNow: String { return self._s[276]! } + public var Calls_NotNow: String { return self._s[290]! } public func Channel_DiscussionGroup_HeaderSet(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[277]!, self._r[277]!, [_0]) + return formatWithArgumentRanges(self._s[291]!, self._r[291]!, [_0]) } - public var UserInfo_SendMessage: String { return self._s[278]! } - public var TwoStepAuth_PasswordSet: String { return self._s[279]! } - public var Passport_DeleteDocument: String { return self._s[280]! } - public var SocksProxySetup_AddProxyTitle: String { return self._s[281]! } + public var UserInfo_SendMessage: String { return self._s[292]! } + public var TwoStepAuth_PasswordSet: String { return self._s[293]! } + public var Passport_DeleteDocument: String { return self._s[294]! } + public var SocksProxySetup_AddProxyTitle: String { return self._s[295]! } public func PUSH_MESSAGE_VIDEO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[282]!, self._r[282]!, [_1]) + return formatWithArgumentRanges(self._s[296]!, self._r[296]!, [_1]) } - public var GroupRemoved_Remove: String { return self._s[283]! } - public var Passport_FieldIdentity: String { return self._s[284]! } - public var Group_Setup_TypePrivateHelp: String { return self._s[285]! } - public var Conversation_Processing: String { return self._s[288]! } - public var Wallet_Settings_BackupWallet: String { return self._s[290]! } - public var ChatSettings_AutoPlayAnimations: String { return self._s[291]! } - public var AuthSessions_LogOutApplicationsHelp: String { return self._s[294]! } - public var Month_GenFebruary: String { return self._s[295]! } - public var Wallet_Send_NetworkErrorTitle: String { return self._s[296]! } + public var AuthSessions_AddedDeviceTitle: String { return self._s[297]! } + public var GroupRemoved_Remove: String { return self._s[298]! } + public var Passport_FieldIdentity: String { return self._s[299]! } + public var Group_Setup_TypePrivateHelp: String { return self._s[300]! } + public var Conversation_Processing: String { return self._s[303]! } + public var Wallet_Settings_BackupWallet: String { return self._s[305]! } + public var ChatSettings_AutoPlayAnimations: String { return self._s[306]! } + public var AuthSessions_LogOutApplicationsHelp: String { return self._s[309]! } + public var Forward_ErrorPublicQuizDisabledInChannels: String { return self._s[310]! } + public var Month_GenFebruary: String { return self._s[311]! } + public var Wallet_Send_NetworkErrorTitle: String { return self._s[312]! } public func Login_InvalidPhoneEmailBody(_ _1: String, _ _2: String, _ _3: String, _ _4: String, _ _5: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[298]!, self._r[298]!, [_1, _2, _3, _4, _5]) + return formatWithArgumentRanges(self._s[314]!, self._r[314]!, [_1, _2, _3, _4, _5]) } - public var Passport_Identity_TypeIdentityCard: String { return self._s[299]! } - public var Wallet_Month_ShortJune: String { return self._s[301]! } - public var AutoDownloadSettings_DataUsageMedium: String { return self._s[302]! } - public var GroupInfo_AddParticipant: String { return self._s[303]! } - public var KeyCommand_SendMessage: String { return self._s[304]! } - public var VoiceOver_Chat_YourContact: String { return self._s[306]! } - public var Map_LiveLocationShowAll: String { return self._s[307]! } - public var WallpaperSearch_ColorOrange: String { return self._s[309]! } - public var Appearance_AppIconDefaultX: String { return self._s[310]! } - public var Checkout_Receipt_Title: String { return self._s[311]! } - public var Group_OwnershipTransfer_ErrorPrivacyRestricted: String { return self._s[312]! } - public var WallpaperPreview_PreviewTopText: String { return self._s[313]! } - public var Message_Contact: String { return self._s[314]! } - public var Call_StatusIncoming: String { return self._s[315]! } - public var Wallet_TransactionInfo_StorageFeeInfo: String { return self._s[316]! } + public var Passport_Identity_TypeIdentityCard: String { return self._s[315]! } + public var Wallet_Month_ShortJune: String { return self._s[317]! } + public var AutoDownloadSettings_DataUsageMedium: String { return self._s[318]! } + public var GroupInfo_AddParticipant: String { return self._s[319]! } + public var KeyCommand_SendMessage: String { return self._s[320]! } + public var VoiceOver_Chat_YourContact: String { return self._s[322]! } + public var Map_LiveLocationShowAll: String { return self._s[323]! } + public var WallpaperSearch_ColorOrange: String { return self._s[325]! } + public var Appearance_AppIconDefaultX: String { return self._s[326]! } + public var Checkout_Receipt_Title: String { return self._s[327]! } + public var Group_OwnershipTransfer_ErrorPrivacyRestricted: String { return self._s[328]! } + public var WallpaperPreview_PreviewTopText: String { return self._s[329]! } + public var Message_Contact: String { return self._s[330]! } + public var Call_StatusIncoming: String { return self._s[331]! } + public var Wallet_TransactionInfo_StorageFeeInfo: String { return self._s[332]! } public func Channel_AdminLog_MessageKickedName(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[317]!, self._r[317]!, [_1]) + return formatWithArgumentRanges(self._s[333]!, self._r[333]!, [_1]) } public func PUSH_ENCRYPTED_MESSAGE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[319]!, self._r[319]!, [_1]) + return formatWithArgumentRanges(self._s[335]!, self._r[335]!, [_1]) } - public var VoiceOver_Media_PlaybackRate: String { return self._s[320]! } - public var Passport_FieldIdentityDetailsHelp: String { return self._s[321]! } - public var Conversation_ViewChannel: String { return self._s[322]! } + public var VoiceOver_Media_PlaybackRate: String { return self._s[336]! } + public var Passport_FieldIdentityDetailsHelp: String { return self._s[337]! } + public var Conversation_ViewChannel: String { return self._s[338]! } public func Time_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[323]!, self._r[323]!, [_0]) + return formatWithArgumentRanges(self._s[339]!, self._r[339]!, [_0]) } - public var Passport_Language_nl: String { return self._s[325]! } - public var Camera_Retake: String { return self._s[326]! } + public var Theme_Colors_Accent: String { return self._s[340]! } + public var Passport_Language_nl: String { return self._s[342]! } + public var Camera_Retake: String { return self._s[343]! } public func UserInfo_BlockActionTitle(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[327]!, self._r[327]!, [_0]) - } - public var AuthSessions_LogOutApplications: String { return self._s[328]! } - public var ApplyLanguage_ApplySuccess: String { return self._s[329]! } - public var Tour_Title6: String { return self._s[330]! } - public var Map_ChooseAPlace: String { return self._s[331]! } - public var CallSettings_Never: String { return self._s[333]! } - public func Notification_ChangedGroupPhoto(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[334]!, self._r[334]!, [_0]) - } - public var ChannelRemoved_RemoveInfo: String { return self._s[335]! } - public func AutoDownloadSettings_PreloadVideoInfo(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[336]!, self._r[336]!, [_0]) - } - public var SettingsSearch_Synonyms_Notifications_MessageNotificationsExceptions: String { return self._s[337]! } - public func Conversation_ClearChatConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[338]!, self._r[338]!, [_0]) - } - public var GroupInfo_InviteLink_Title: String { return self._s[339]! } - public func Channel_AdminLog_MessageUnkickedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[340]!, self._r[340]!, [_1, _2]) - } - public var KeyCommand_ScrollUp: String { return self._s[341]! } - public var ContactInfo_URLLabelHomepage: String { return self._s[342]! } - public var Channel_OwnershipTransfer_ChangeOwner: String { return self._s[343]! } - public func Channel_AdminLog_DisabledSlowmode(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[344]!, self._r[344]!, [_0]) } - public var TwoFactorSetup_Done_Title: String { return self._s[345]! } - public func Conversation_EncryptedPlaceholderTitleOutgoing(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[346]!, self._r[346]!, [_0]) + public var AuthSessions_LogOutApplications: String { return self._s[345]! } + public var ApplyLanguage_ApplySuccess: String { return self._s[346]! } + public var Tour_Title6: String { return self._s[347]! } + public var Map_ChooseAPlace: String { return self._s[348]! } + public var CallSettings_Never: String { return self._s[350]! } + public func Notification_ChangedGroupPhoto(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[351]!, self._r[351]!, [_0]) } - public var CallFeedback_ReasonDistortedSpeech: String { return self._s[347]! } - public var Watch_LastSeen_WithinAWeek: String { return self._s[348]! } - public var ContactList_Context_SendMessage: String { return self._s[350]! } - public var Weekday_Tuesday: String { return self._s[351]! } - public var Wallet_Created_Title: String { return self._s[353]! } - public var ScheduledMessages_Delete: String { return self._s[354]! } - public var UserInfo_StartSecretChat: String { return self._s[355]! } - public var Passport_Identity_FilesTitle: String { return self._s[356]! } - public var Permissions_NotificationsAllow_v0: String { return self._s[357]! } - public var DialogList_DeleteConversationConfirmation: String { return self._s[359]! } - public var ChatList_UndoArchiveRevealedTitle: String { return self._s[360]! } - public func Wallet_Configuration_ApplyErrorTextURLUnreachable(_ _0: String) -> (String, [(Int, NSRange)]) { + public var ChannelRemoved_RemoveInfo: String { return self._s[352]! } + public func AutoDownloadSettings_PreloadVideoInfo(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[353]!, self._r[353]!, [_0]) + } + public var SettingsSearch_Synonyms_Notifications_MessageNotificationsExceptions: String { return self._s[354]! } + public func Conversation_ClearChatConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[355]!, self._r[355]!, [_0]) + } + public var GroupInfo_InviteLink_Title: String { return self._s[356]! } + public func Channel_AdminLog_MessageUnkickedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[357]!, self._r[357]!, [_1, _2]) + } + public var KeyCommand_ScrollUp: String { return self._s[358]! } + public var ContactInfo_URLLabelHomepage: String { return self._s[359]! } + public var Channel_OwnershipTransfer_ChangeOwner: String { return self._s[360]! } + public func Channel_AdminLog_DisabledSlowmode(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[361]!, self._r[361]!, [_0]) } - public var AuthSessions_Sessions: String { return self._s[362]! } + public var TwoFactorSetup_Done_Title: String { return self._s[362]! } + public func Conversation_EncryptedPlaceholderTitleOutgoing(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[363]!, self._r[363]!, [_0]) + } + public var CallFeedback_ReasonDistortedSpeech: String { return self._s[364]! } + public var Watch_LastSeen_WithinAWeek: String { return self._s[365]! } + public var ContactList_Context_SendMessage: String { return self._s[367]! } + public var Weekday_Tuesday: String { return self._s[368]! } + public var Wallet_Created_Title: String { return self._s[370]! } + public var ScheduledMessages_Delete: String { return self._s[371]! } + public var UserInfo_StartSecretChat: String { return self._s[372]! } + public var Passport_Identity_FilesTitle: String { return self._s[373]! } + public var Permissions_NotificationsAllow_v0: String { return self._s[374]! } + public var DialogList_DeleteConversationConfirmation: String { return self._s[376]! } + public var ChatList_UndoArchiveRevealedTitle: String { return self._s[377]! } + public func Wallet_Configuration_ApplyErrorTextURLUnreachable(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[378]!, self._r[378]!, [_0]) + } + public var AuthSessions_Sessions: String { return self._s[379]! } public func Settings_KeepPhoneNumber(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[364]!, self._r[364]!, [_0]) + return formatWithArgumentRanges(self._s[381]!, self._r[381]!, [_0]) } - public var TwoStepAuth_RecoveryEmailChangeDescription: String { return self._s[365]! } - public var Call_StatusWaiting: String { return self._s[366]! } - public var CreateGroup_SoftUserLimitAlert: String { return self._s[367]! } - public var FastTwoStepSetup_HintHelp: String { return self._s[368]! } - public var WallpaperPreview_CustomColorBottomText: String { return self._s[369]! } - public var EditTheme_Expand_Preview_OutgoingText: String { return self._s[370]! } - public var LogoutOptions_AddAccountText: String { return self._s[371]! } - public var PasscodeSettings_6DigitCode: String { return self._s[372]! } - public var Settings_LogoutConfirmationText: String { return self._s[373]! } - public var Passport_Identity_TypePassport: String { return self._s[375]! } + public var TwoStepAuth_RecoveryEmailChangeDescription: String { return self._s[382]! } + public var Call_StatusWaiting: String { return self._s[383]! } + public var CreateGroup_SoftUserLimitAlert: String { return self._s[384]! } + public var FastTwoStepSetup_HintHelp: String { return self._s[385]! } + public var WallpaperPreview_CustomColorBottomText: String { return self._s[386]! } + public var EditTheme_Expand_Preview_OutgoingText: String { return self._s[387]! } + public var LogoutOptions_AddAccountText: String { return self._s[388]! } + public var PasscodeSettings_6DigitCode: String { return self._s[389]! } + public var Settings_LogoutConfirmationText: String { return self._s[390]! } + public var Passport_Identity_TypePassport: String { return self._s[392]! } + public var Map_Work: String { return self._s[395]! } public func PUSH_MESSAGE_VIDEOS(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[378]!, self._r[378]!, [_1, _2]) + return formatWithArgumentRanges(self._s[396]!, self._r[396]!, [_1, _2]) } - public var SocksProxySetup_SaveProxy: String { return self._s[379]! } - public var AccessDenied_SaveMedia: String { return self._s[380]! } - public var Checkout_ErrorInvoiceAlreadyPaid: String { return self._s[382]! } - public var Settings_Title: String { return self._s[384]! } - public var VoiceOver_Chat_RecordModeVideoMessageInfo: String { return self._s[385]! } - public var Contacts_InviteSearchLabel: String { return self._s[387]! } - public var ConvertToSupergroup_Title: String { return self._s[388]! } + public var SocksProxySetup_SaveProxy: String { return self._s[397]! } + public var AccessDenied_SaveMedia: String { return self._s[398]! } + public var Checkout_ErrorInvoiceAlreadyPaid: String { return self._s[400]! } + public var CreatePoll_MultipleChoice: String { return self._s[401]! } + public var Settings_Title: String { return self._s[403]! } + public var VoiceOver_Chat_RecordModeVideoMessageInfo: String { return self._s[404]! } + public var Contacts_InviteSearchLabel: String { return self._s[406]! } + public var PrivacySettings_WebSessions: String { return self._s[407]! } + public var ConvertToSupergroup_Title: String { return self._s[408]! } public func Channel_AdminLog_CaptionEdited(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[389]!, self._r[389]!, [_0]) + return formatWithArgumentRanges(self._s[409]!, self._r[409]!, [_0]) } - public var TwoFactorSetup_Hint_Text: String { return self._s[390]! } - public var InfoPlist_NSSiriUsageDescription: String { return self._s[391]! } + public var TwoFactorSetup_Hint_Text: String { return self._s[410]! } + public var InfoPlist_NSSiriUsageDescription: String { return self._s[411]! } public func PUSH_MESSAGE_CHANNEL_MESSAGE_GAME_SCORE(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[392]!, self._r[392]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[412]!, self._r[412]!, [_1, _2, _3]) } - public var ChatSettings_AutomaticPhotoDownload: String { return self._s[393]! } - public var UserInfo_BotHelp: String { return self._s[394]! } - public var PrivacySettings_LastSeenEverybody: String { return self._s[395]! } - public var Checkout_Name: String { return self._s[396]! } - public var AutoDownloadSettings_DataUsage: String { return self._s[397]! } - public var Channel_BanUser_BlockFor: String { return self._s[398]! } - public var Checkout_ShippingAddress: String { return self._s[399]! } - public var AutoDownloadSettings_MaxVideoSize: String { return self._s[400]! } - public var Privacy_PaymentsClearInfoDoneHelp: String { return self._s[401]! } - public var Privacy_Forwards: String { return self._s[402]! } - public var Channel_BanUser_PermissionSendPolls: String { return self._s[403]! } - public var Appearance_ThemeCarouselNewNight: String { return self._s[404]! } + public var ChatSettings_AutomaticPhotoDownload: String { return self._s[413]! } + public var UserInfo_BotHelp: String { return self._s[414]! } + public var PrivacySettings_LastSeenEverybody: String { return self._s[415]! } + public var Checkout_Name: String { return self._s[416]! } + public var AutoDownloadSettings_DataUsage: String { return self._s[417]! } + public var Channel_BanUser_BlockFor: String { return self._s[418]! } + public var Checkout_ShippingAddress: String { return self._s[419]! } + public var AutoDownloadSettings_MaxVideoSize: String { return self._s[420]! } + public var Privacy_PaymentsClearInfoDoneHelp: String { return self._s[421]! } + public var Privacy_Forwards: String { return self._s[422]! } + public var Channel_BanUser_PermissionSendPolls: String { return self._s[423]! } + public var Appearance_ThemeCarouselNewNight: String { return self._s[424]! } public func SecretVideo_NotViewedYet(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[407]!, self._r[407]!, [_0]) + return formatWithArgumentRanges(self._s[427]!, self._r[427]!, [_0]) } - public var Contacts_SortedByName: String { return self._s[408]! } - public var Group_OwnershipTransfer_Title: String { return self._s[409]! } - public var VoiceOver_Chat_OpenHint: String { return self._s[410]! } - public var Group_LeaveGroup: String { return self._s[411]! } - public var Settings_UsernameEmpty: String { return self._s[412]! } + public var Contacts_SortedByName: String { return self._s[428]! } + public var Group_OwnershipTransfer_Title: String { return self._s[429]! } + public var PeerInfo_BioExpand: String { return self._s[431]! } + public var VoiceOver_Chat_OpenHint: String { return self._s[432]! } + public var Group_LeaveGroup: String { return self._s[433]! } + public var Settings_UsernameEmpty: String { return self._s[434]! } public func Notification_PinnedPollMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[413]!, self._r[413]!, [_0]) + return formatWithArgumentRanges(self._s[435]!, self._r[435]!, [_0]) } public func TwoStepAuth_ConfirmEmailDescription(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[414]!, self._r[414]!, [_1]) + return formatWithArgumentRanges(self._s[436]!, self._r[436]!, [_1]) } public func Channel_OwnershipTransfer_DescriptionInfo(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[415]!, self._r[415]!, [_1, _2]) + return formatWithArgumentRanges(self._s[437]!, self._r[437]!, [_1, _2]) } - public var Message_ImageExpired: String { return self._s[416]! } - public var TwoStepAuth_RecoveryFailed: String { return self._s[418]! } - public var EditTheme_Edit_Preview_OutgoingText: String { return self._s[419]! } - public var UserInfo_AddToExisting: String { return self._s[420]! } - public var TwoStepAuth_EnabledSuccess: String { return self._s[421]! } - public var Wallet_Send_SyncInProgress: String { return self._s[422]! } - public var SettingsSearch_Synonyms_Appearance_ChatBackground_SetColor: String { return self._s[423]! } + public var Message_ImageExpired: String { return self._s[438]! } + public var TwoStepAuth_RecoveryFailed: String { return self._s[440]! } + public var EditTheme_Edit_Preview_OutgoingText: String { return self._s[441]! } + public var UserInfo_AddToExisting: String { return self._s[442]! } + public var TwoStepAuth_EnabledSuccess: String { return self._s[443]! } + public var Wallet_Send_SyncInProgress: String { return self._s[444]! } + public var SettingsSearch_Synonyms_Appearance_ChatBackground_SetColor: String { return self._s[445]! } public func PUSH_CHANNEL_MESSAGE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[424]!, self._r[424]!, [_1]) + return formatWithArgumentRanges(self._s[446]!, self._r[446]!, [_1]) } - public var Notifications_GroupNotificationsAlert: String { return self._s[425]! } - public var Passport_Language_km: String { return self._s[426]! } - public var SocksProxySetup_AdNoticeHelp: String { return self._s[428]! } - public var VoiceOver_Media_PlaybackPlay: String { return self._s[429]! } - public var Notification_CallMissedShort: String { return self._s[430]! } - public var Wallet_Info_YourBalance: String { return self._s[431]! } - public var ReportPeer_ReasonOther_Send: String { return self._s[432]! } - public var Watch_Compose_Send: String { return self._s[433]! } - public var Passport_Identity_TypeInternalPassportUploadScan: String { return self._s[436]! } - public var TwoFactorSetup_Email_Action: String { return self._s[437]! } - public var Conversation_HoldForVideo: String { return self._s[438]! } - public var Wallet_Configuration_ApplyErrorTextURLInvalidData: String { return self._s[439]! } - public var Wallet_TransactionInfo_CommentHeader: String { return self._s[440]! } - public var CheckoutInfo_ErrorCityInvalid: String { return self._s[442]! } - public var Appearance_AutoNightThemeDisabled: String { return self._s[444]! } - public var Channel_LinkItem: String { return self._s[445]! } + public var Notifications_GroupNotificationsAlert: String { return self._s[447]! } + public var Passport_Language_km: String { return self._s[448]! } + public var SocksProxySetup_AdNoticeHelp: String { return self._s[450]! } + public var VoiceOver_Media_PlaybackPlay: String { return self._s[451]! } + public var Notification_CallMissedShort: String { return self._s[452]! } + public var Wallet_Info_YourBalance: String { return self._s[453]! } + public var ReportPeer_ReasonOther_Send: String { return self._s[455]! } + public var Watch_Compose_Send: String { return self._s[456]! } + public var Passport_Identity_TypeInternalPassportUploadScan: String { return self._s[459]! } + public var TwoFactorSetup_Email_Action: String { return self._s[460]! } + public var Conversation_HoldForVideo: String { return self._s[461]! } + public var Wallet_Configuration_ApplyErrorTextURLInvalidData: String { return self._s[462]! } + public var AuthSessions_OtherDevices: String { return self._s[463]! } + public var Wallet_TransactionInfo_CommentHeader: String { return self._s[464]! } + public var CheckoutInfo_ErrorCityInvalid: String { return self._s[466]! } + public var Appearance_AutoNightThemeDisabled: String { return self._s[468]! } + public var Channel_LinkItem: String { return self._s[469]! } public func PrivacySettings_LastSeenContactsMinusPlus(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[446]!, self._r[446]!, [_0, _1]) + return formatWithArgumentRanges(self._s[470]!, self._r[470]!, [_0, _1]) } public func Passport_Identity_NativeNameTitle(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[449]!, self._r[449]!, [_0]) + return formatWithArgumentRanges(self._s[473]!, self._r[473]!, [_0]) } - public var VoiceOver_Recording_StopAndPreview: String { return self._s[450]! } - public var Passport_Language_dv: String { return self._s[451]! } - public var Undo_LeftChannel: String { return self._s[452]! } - public var Notifications_ExceptionsMuted: String { return self._s[453]! } - public var ChatList_UnhideAction: String { return self._s[454]! } - public var Conversation_ContextMenuShare: String { return self._s[455]! } - public var Conversation_ContextMenuStickerPackInfo: String { return self._s[456]! } - public var ShareFileTip_Title: String { return self._s[457]! } - public var NotificationsSound_Chord: String { return self._s[458]! } - public var Wallet_TransactionInfo_OtherFeeHeader: String { return self._s[459]! } + public var VoiceOver_Recording_StopAndPreview: String { return self._s[474]! } + public var Passport_Language_dv: String { return self._s[475]! } + public var Undo_LeftChannel: String { return self._s[476]! } + public var Notifications_ExceptionsMuted: String { return self._s[477]! } + public var ChatList_UnhideAction: String { return self._s[478]! } + public var Conversation_ContextMenuShare: String { return self._s[479]! } + public var Conversation_ContextMenuStickerPackInfo: String { return self._s[480]! } + public var ShareFileTip_Title: String { return self._s[481]! } + public var NotificationsSound_Chord: String { return self._s[482]! } + public var Wallet_TransactionInfo_OtherFeeHeader: String { return self._s[483]! } public func PUSH_CHAT_RETURNED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[460]!, self._r[460]!, [_1, _2]) + return formatWithArgumentRanges(self._s[484]!, self._r[484]!, [_1, _2]) } - public var Passport_Address_EditTemporaryRegistration: String { return self._s[461]! } + public var Passport_Address_EditTemporaryRegistration: String { return self._s[485]! } public func Notification_Joined(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[462]!, self._r[462]!, [_0]) + return formatWithArgumentRanges(self._s[486]!, self._r[486]!, [_0]) } public func Wallet_Time_PreciseDate_m3(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[463]!, self._r[463]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[487]!, self._r[487]!, [_1, _2, _3]) } - public var Wallet_Settings_ConfigurationInfo: String { return self._s[464]! } - public var Wallpaper_ErrorNotFound: String { return self._s[465]! } - public var Notification_CallOutgoingShort: String { return self._s[467]! } - public var Wallet_WordImport_IncorrectText: String { return self._s[468]! } + public var Wallet_Settings_ConfigurationInfo: String { return self._s[488]! } + public var Wallpaper_ErrorNotFound: String { return self._s[489]! } + public var Notification_CallOutgoingShort: String { return self._s[491]! } + public var Wallet_WordImport_IncorrectText: String { return self._s[492]! } public func Watch_Time_ShortFullAt(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[469]!, self._r[469]!, [_1, _2]) + return formatWithArgumentRanges(self._s[493]!, self._r[493]!, [_1, _2]) } - public var Passport_Address_TypeUtilityBill: String { return self._s[470]! } - public var Privacy_Forwards_LinkIfAllowed: String { return self._s[471]! } - public var ReportPeer_Report: String { return self._s[472]! } - public var SettingsSearch_Synonyms_Proxy_Title: String { return self._s[473]! } - public var GroupInfo_DeactivatedStatus: String { return self._s[474]! } + public var Passport_Address_TypeUtilityBill: String { return self._s[494]! } + public var Privacy_Forwards_LinkIfAllowed: String { return self._s[495]! } + public var ReportPeer_Report: String { return self._s[496]! } + public var SettingsSearch_Synonyms_Proxy_Title: String { return self._s[497]! } + public var GroupInfo_DeactivatedStatus: String { return self._s[498]! } public func VoiceOver_Chat_MusicTitle(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[475]!, self._r[475]!, [_1, _2]) + return formatWithArgumentRanges(self._s[499]!, self._r[499]!, [_1, _2]) } - public var StickerPack_Send: String { return self._s[476]! } - public var Login_CodeSentInternal: String { return self._s[477]! } - public var Wallet_Month_GenJanuary: String { return self._s[478]! } - public var GroupInfo_InviteLink_LinkSection: String { return self._s[479]! } + public var StickerPack_Send: String { return self._s[500]! } + public var Login_CodeSentInternal: String { return self._s[501]! } + public var Wallet_Month_GenJanuary: String { return self._s[502]! } + public var GroupInfo_InviteLink_LinkSection: String { return self._s[503]! } public func Channel_AdminLog_MessageDeleted(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[480]!, self._r[480]!, [_0]) + return formatWithArgumentRanges(self._s[504]!, self._r[504]!, [_0]) } public func Conversation_EncryptionWaiting(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[482]!, self._r[482]!, [_0]) + return formatWithArgumentRanges(self._s[506]!, self._r[506]!, [_0]) } - public var Channel_BanUser_PermissionSendStickersAndGifs: String { return self._s[483]! } + public var Channel_BanUser_PermissionSendStickersAndGifs: String { return self._s[507]! } public func PUSH_PINNED_GAME(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[484]!, self._r[484]!, [_1]) + return formatWithArgumentRanges(self._s[508]!, self._r[508]!, [_1]) } - public var ReportPeer_ReasonViolence: String { return self._s[486]! } - public var Map_Locating: String { return self._s[487]! } + public var ReportPeer_ReasonViolence: String { return self._s[510]! } + public var Appearance_ShareThemeColor: String { return self._s[511]! } + public var Map_Locating: String { return self._s[512]! } public func VoiceOver_Chat_VideoFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[488]!, self._r[488]!, [_0]) + return formatWithArgumentRanges(self._s[513]!, self._r[513]!, [_0]) } public func PUSH_ALBUM(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[489]!, self._r[489]!, [_1]) + return formatWithArgumentRanges(self._s[514]!, self._r[514]!, [_1]) } - public var AutoDownloadSettings_GroupChats: String { return self._s[491]! } - public var CheckoutInfo_SaveInfo: String { return self._s[492]! } - public var SharedMedia_EmptyLinksText: String { return self._s[494]! } - public var Passport_Address_CityPlaceholder: String { return self._s[495]! } - public var CheckoutInfo_ErrorStateInvalid: String { return self._s[496]! } - public var Privacy_ProfilePhoto_CustomHelp: String { return self._s[497]! } - public var Wallet_Send_OwnAddressAlertTitle: String { return self._s[499]! } - public var Channel_AdminLog_CanAddAdmins: String { return self._s[500]! } + public var AutoDownloadSettings_GroupChats: String { return self._s[516]! } + public var CheckoutInfo_SaveInfo: String { return self._s[517]! } + public var SharedMedia_EmptyLinksText: String { return self._s[519]! } + public var Passport_Address_CityPlaceholder: String { return self._s[520]! } + public var CheckoutInfo_ErrorStateInvalid: String { return self._s[521]! } + public var Privacy_ProfilePhoto_CustomHelp: String { return self._s[522]! } + public var Wallet_Send_OwnAddressAlertTitle: String { return self._s[524]! } + public var Channel_AdminLog_CanAddAdmins: String { return self._s[525]! } public func PUSH_CHANNEL_MESSAGE_FWD(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[501]!, self._r[501]!, [_1]) + return formatWithArgumentRanges(self._s[526]!, self._r[526]!, [_1]) } public func Time_MonthOfYear_m8(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[502]!, self._r[502]!, [_0]) + return formatWithArgumentRanges(self._s[527]!, self._r[527]!, [_0]) } - public var InfoPlist_NSLocationWhenInUseUsageDescription: String { return self._s[503]! } - public var GroupInfo_InviteLink_RevokeAlert_Success: String { return self._s[504]! } - public var ChangePhoneNumberCode_Code: String { return self._s[505]! } - public var Appearance_CreateTheme: String { return self._s[506]! } + public var InfoPlist_NSLocationWhenInUseUsageDescription: String { return self._s[528]! } + public var GroupInfo_InviteLink_RevokeAlert_Success: String { return self._s[529]! } + public var ChangePhoneNumberCode_Code: String { return self._s[530]! } + public var Appearance_CreateTheme: String { return self._s[531]! } public func UserInfo_NotificationsDefaultSound(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[507]!, self._r[507]!, [_0]) + return formatWithArgumentRanges(self._s[532]!, self._r[532]!, [_0]) } - public var TwoStepAuth_SetupEmail: String { return self._s[508]! } - public var HashtagSearch_AllChats: String { return self._s[509]! } - public var SettingsSearch_Synonyms_Data_AutoDownloadUsingCellular: String { return self._s[511]! } + public var TwoStepAuth_SetupEmail: String { return self._s[533]! } + public var HashtagSearch_AllChats: String { return self._s[534]! } + public var MediaPlayer_UnknownTrack: String { return self._s[535]! } + public var SettingsSearch_Synonyms_Data_AutoDownloadUsingCellular: String { return self._s[537]! } public func ChatList_DeleteForEveryone(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[512]!, self._r[512]!, [_0]) + return formatWithArgumentRanges(self._s[538]!, self._r[538]!, [_0]) } - public var PhotoEditor_QualityHigh: String { return self._s[514]! } + public var PhotoEditor_QualityHigh: String { return self._s[540]! } public func Passport_Phone_UseTelegramNumber(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[515]!, self._r[515]!, [_0]) + return formatWithArgumentRanges(self._s[541]!, self._r[541]!, [_0]) } - public var ApplyLanguage_ApplyLanguageAction: String { return self._s[516]! } - public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsPreview: String { return self._s[517]! } - public var Message_LiveLocation: String { return self._s[518]! } - public var Cache_LowDiskSpaceText: String { return self._s[519]! } - public var Wallet_Receive_ShareAddress: String { return self._s[520]! } - public var EditTheme_ErrorLinkTaken: String { return self._s[521]! } - public var Conversation_SendMessage: String { return self._s[522]! } - public var AuthSessions_EmptyTitle: String { return self._s[523]! } - public var Privacy_PhoneNumber: String { return self._s[524]! } - public var PeopleNearby_CreateGroup: String { return self._s[525]! } - public var CallSettings_UseLessData: String { return self._s[526]! } - public var NetworkUsageSettings_MediaDocumentDataSection: String { return self._s[527]! } - public var Stickers_AddToFavorites: String { return self._s[528]! } - public var Wallet_WordImport_Title: String { return self._s[529]! } - public var PhotoEditor_QualityLow: String { return self._s[530]! } - public var Watch_UserInfo_Unblock: String { return self._s[531]! } - public var Settings_Logout: String { return self._s[532]! } + public var ApplyLanguage_ApplyLanguageAction: String { return self._s[542]! } + public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsPreview: String { return self._s[543]! } + public var Message_LiveLocation: String { return self._s[544]! } + public var Cache_LowDiskSpaceText: String { return self._s[545]! } + public var Wallet_Receive_ShareAddress: String { return self._s[546]! } + public var EditTheme_ErrorLinkTaken: String { return self._s[547]! } + public var Conversation_SendMessage: String { return self._s[548]! } + public var AuthSessions_EmptyTitle: String { return self._s[549]! } + public var Privacy_PhoneNumber: String { return self._s[550]! } + public var PeopleNearby_CreateGroup: String { return self._s[551]! } + public var CallSettings_UseLessData: String { return self._s[553]! } + public var NetworkUsageSettings_MediaDocumentDataSection: String { return self._s[554]! } + public var Stickers_AddToFavorites: String { return self._s[555]! } + public var Wallet_WordImport_Title: String { return self._s[556]! } + public var PhotoEditor_QualityLow: String { return self._s[557]! } + public var Watch_UserInfo_Unblock: String { return self._s[558]! } + public var Settings_Logout: String { return self._s[559]! } public func PUSH_MESSAGE_ROUND(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[533]!, self._r[533]!, [_1]) + return formatWithArgumentRanges(self._s[560]!, self._r[560]!, [_1]) } - public var ContactInfo_PhoneLabelWork: String { return self._s[534]! } - public var ChannelInfo_Stats: String { return self._s[535]! } - public var TextFormat_Link: String { return self._s[536]! } + public var ContactInfo_PhoneLabelWork: String { return self._s[561]! } + public var ChannelInfo_Stats: String { return self._s[562]! } + public var TextFormat_Link: String { return self._s[563]! } public func Date_ChatDateHeader(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[537]!, self._r[537]!, [_1, _2]) + return formatWithArgumentRanges(self._s[564]!, self._r[564]!, [_1, _2]) } - public var Wallet_TransactionInfo_Title: String { return self._s[538]! } + public var Wallet_TransactionInfo_Title: String { return self._s[565]! } public func Message_ForwardedMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[539]!, self._r[539]!, [_0]) + return formatWithArgumentRanges(self._s[566]!, self._r[566]!, [_0]) } - public var Watch_Notification_Joined: String { return self._s[540]! } - public var Group_Setup_TypePublicHelp: String { return self._s[541]! } - public var Passport_Scans_UploadNew: String { return self._s[542]! } - public var Checkout_LiabilityAlertTitle: String { return self._s[543]! } - public var DialogList_Title: String { return self._s[546]! } - public var NotificationSettings_ContactJoined: String { return self._s[547]! } - public var GroupInfo_LabelAdmin: String { return self._s[548]! } - public var KeyCommand_ChatInfo: String { return self._s[549]! } - public var Conversation_EditingCaptionPanelTitle: String { return self._s[550]! } - public var Call_ReportIncludeLog: String { return self._s[551]! } + public var Watch_Notification_Joined: String { return self._s[567]! } + public var Group_Setup_TypePublicHelp: String { return self._s[568]! } + public var Passport_Scans_UploadNew: String { return self._s[569]! } + public var Checkout_LiabilityAlertTitle: String { return self._s[570]! } + public var DialogList_Title: String { return self._s[573]! } + public var NotificationSettings_ContactJoined: String { return self._s[574]! } + public var GroupInfo_LabelAdmin: String { return self._s[575]! } + public var KeyCommand_ChatInfo: String { return self._s[576]! } + public var Conversation_EditingCaptionPanelTitle: String { return self._s[577]! } + public var Call_ReportIncludeLog: String { return self._s[578]! } public func Notifications_ExceptionsChangeSound(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[554]!, self._r[554]!, [_0]) + return formatWithArgumentRanges(self._s[581]!, self._r[581]!, [_0]) } - public var LocalGroup_IrrelevantWarning: String { return self._s[555]! } - public var ChatAdmins_AllMembersAreAdmins: String { return self._s[556]! } - public var Conversation_DefaultRestrictedInline: String { return self._s[557]! } - public var Message_Sticker: String { return self._s[558]! } - public var LastSeen_JustNow: String { return self._s[560]! } - public var Passport_Email_EmailPlaceholder: String { return self._s[562]! } - public var SettingsSearch_Synonyms_AppLanguage: String { return self._s[563]! } - public var Channel_AdminLogFilter_EventsEditedMessages: String { return self._s[564]! } - public var Channel_EditAdmin_PermissionsHeader: String { return self._s[565]! } - public var TwoStepAuth_Email: String { return self._s[566]! } - public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsSound: String { return self._s[567]! } - public var PhotoEditor_BlurToolOff: String { return self._s[568]! } - public var Message_PinnedStickerMessage: String { return self._s[569]! } - public var ContactInfo_PhoneLabelPager: String { return self._s[570]! } - public var SettingsSearch_Synonyms_Appearance_TextSize: String { return self._s[571]! } - public var Passport_DiscardMessageTitle: String { return self._s[572]! } - public var Privacy_PaymentsTitle: String { return self._s[573]! } - public var EditTheme_Edit_Preview_IncomingReplyName: String { return self._s[574]! } - public var ClearCache_StorageCache: String { return self._s[575]! } - public var Channel_DiscussionGroup_Header: String { return self._s[577]! } - public var VoiceOver_Chat_OptionSelected: String { return self._s[578]! } - public var Appearance_ColorTheme: String { return self._s[579]! } - public var UserInfo_ShareContact: String { return self._s[580]! } - public var Passport_Address_TypePassportRegistration: String { return self._s[581]! } - public var Common_More: String { return self._s[582]! } - public var Watch_Message_Call: String { return self._s[583]! } - public var Profile_EncryptionKey: String { return self._s[586]! } - public var Privacy_TopPeers: String { return self._s[587]! } - public var Conversation_StopPollConfirmation: String { return self._s[588]! } - public var Wallet_Words_NotDoneText: String { return self._s[590]! } - public var Privacy_TopPeersWarning: String { return self._s[592]! } - public var SettingsSearch_Synonyms_Data_DownloadInBackground: String { return self._s[593]! } - public var SettingsSearch_Synonyms_Data_Storage_KeepMedia: String { return self._s[594]! } - public var Wallet_RestoreFailed_EnterWords: String { return self._s[597]! } - public var DialogList_SearchSectionMessages: String { return self._s[598]! } - public var Notifications_ChannelNotifications: String { return self._s[599]! } - public var CheckoutInfo_ShippingInfoAddress1Placeholder: String { return self._s[600]! } - public var Passport_Language_sk: String { return self._s[601]! } - public var Notification_MessageLifetime1h: String { return self._s[602]! } - public var Wallpaper_ResetWallpapersInfo: String { return self._s[603]! } - public var Call_ReportSkip: String { return self._s[605]! } - public var Cache_ServiceFiles: String { return self._s[606]! } - public var Group_ErrorAddTooMuchAdmins: String { return self._s[607]! } - public var VoiceOver_Chat_YourFile: String { return self._s[608]! } - public var Map_Hybrid: String { return self._s[609]! } - public var Contacts_SearchUsersAndGroupsLabel: String { return self._s[611]! } - public var ChatSettings_AutoDownloadVideos: String { return self._s[613]! } - public var Channel_BanUser_PermissionEmbedLinks: String { return self._s[614]! } - public var InfoPlist_NSLocationAlwaysAndWhenInUseUsageDescription: String { return self._s[615]! } - public var SocksProxySetup_ProxyTelegram: String { return self._s[618]! } + public var Channel_AdminLog_InfoPanelChannelAlertText: String { return self._s[582]! } + public var ChatAdmins_AllMembersAreAdmins: String { return self._s[583]! } + public var LocalGroup_IrrelevantWarning: String { return self._s[584]! } + public var Conversation_DefaultRestrictedInline: String { return self._s[585]! } + public var Message_Sticker: String { return self._s[586]! } + public var LastSeen_JustNow: String { return self._s[588]! } + public var Passport_Email_EmailPlaceholder: String { return self._s[590]! } + public var SettingsSearch_Synonyms_AppLanguage: String { return self._s[591]! } + public var Channel_AdminLogFilter_EventsEditedMessages: String { return self._s[592]! } + public var Channel_EditAdmin_PermissionsHeader: String { return self._s[593]! } + public var TwoStepAuth_Email: String { return self._s[594]! } + public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsSound: String { return self._s[595]! } + public var PhotoEditor_BlurToolOff: String { return self._s[596]! } + public var Message_PinnedStickerMessage: String { return self._s[597]! } + public var ContactInfo_PhoneLabelPager: String { return self._s[598]! } + public var SettingsSearch_Synonyms_Appearance_TextSize: String { return self._s[599]! } + public var Passport_DiscardMessageTitle: String { return self._s[600]! } + public var Privacy_PaymentsTitle: String { return self._s[601]! } + public var EditTheme_Edit_Preview_IncomingReplyName: String { return self._s[602]! } + public var ClearCache_StorageCache: String { return self._s[603]! } + public var Appearance_TextSizeSetting: String { return self._s[604]! } + public var Channel_DiscussionGroup_Header: String { return self._s[606]! } + public var VoiceOver_Chat_OptionSelected: String { return self._s[607]! } + public var Appearance_ColorTheme: String { return self._s[608]! } + public var UserInfo_ShareContact: String { return self._s[609]! } + public var Passport_Address_TypePassportRegistration: String { return self._s[610]! } + public var Common_More: String { return self._s[611]! } + public var Watch_Message_Call: String { return self._s[612]! } + public var Profile_EncryptionKey: String { return self._s[615]! } + public var Privacy_TopPeers: String { return self._s[616]! } + public var Conversation_StopPollConfirmation: String { return self._s[617]! } + public var Wallet_Words_NotDoneText: String { return self._s[619]! } + public var Privacy_TopPeersWarning: String { return self._s[621]! } + public var SettingsSearch_Synonyms_Data_DownloadInBackground: String { return self._s[622]! } + public var SettingsSearch_Synonyms_Data_Storage_KeepMedia: String { return self._s[623]! } + public var Wallet_RestoreFailed_EnterWords: String { return self._s[626]! } + public var DialogList_SearchSectionMessages: String { return self._s[627]! } + public var Notifications_ChannelNotifications: String { return self._s[628]! } + public var CheckoutInfo_ShippingInfoAddress1Placeholder: String { return self._s[629]! } + public var Passport_Language_sk: String { return self._s[630]! } + public var Notification_MessageLifetime1h: String { return self._s[631]! } + public var Wallpaper_ResetWallpapersInfo: String { return self._s[632]! } + public var Appearance_ThemePreview_Chat_5_Text: String { return self._s[633]! } + public var Call_ReportSkip: String { return self._s[635]! } + public var Cache_ServiceFiles: String { return self._s[636]! } + public var Group_ErrorAddTooMuchAdmins: String { return self._s[637]! } + public var VoiceOver_Chat_YourFile: String { return self._s[638]! } + public var Map_Hybrid: String { return self._s[639]! } + public var Contacts_SearchUsersAndGroupsLabel: String { return self._s[641]! } + public func PUSH_MESSAGE_QUIZ(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[642]!, self._r[642]!, [_1]) + } + public var ChatSettings_AutoDownloadVideos: String { return self._s[644]! } + public var Channel_BanUser_PermissionEmbedLinks: String { return self._s[645]! } + public var InfoPlist_NSLocationAlwaysAndWhenInUseUsageDescription: String { return self._s[646]! } + public var SocksProxySetup_ProxyTelegram: String { return self._s[649]! } public func PUSH_MESSAGE_AUDIO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[619]!, self._r[619]!, [_1]) + return formatWithArgumentRanges(self._s[650]!, self._r[650]!, [_1]) } - public var Channel_Username_CreatePrivateLinkHelp: String { return self._s[621]! } - public var ScheduledMessages_ScheduledToday: String { return self._s[622]! } + public var Channel_Username_CreatePrivateLinkHelp: String { return self._s[652]! } + public var ScheduledMessages_ScheduledToday: String { return self._s[653]! } public func PUSH_CHAT_TITLE_EDITED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[623]!, self._r[623]!, [_1, _2]) + return formatWithArgumentRanges(self._s[654]!, self._r[654]!, [_1, _2]) } - public var Conversation_LiveLocationYou: String { return self._s[624]! } - public var SettingsSearch_Synonyms_Privacy_Calls: String { return self._s[625]! } - public var SettingsSearch_Synonyms_Notifications_MessageNotificationsPreview: String { return self._s[626]! } - public var UserInfo_ShareBot: String { return self._s[629]! } + public var Conversation_LiveLocationYou: String { return self._s[655]! } + public var SettingsSearch_Synonyms_Privacy_Calls: String { return self._s[656]! } + public var SettingsSearch_Synonyms_Notifications_MessageNotificationsPreview: String { return self._s[657]! } + public var UserInfo_ShareBot: String { return self._s[660]! } public func PUSH_AUTH_REGION(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[630]!, self._r[630]!, [_1, _2]) + return formatWithArgumentRanges(self._s[661]!, self._r[661]!, [_1, _2]) } - public var Conversation_ClearCache: String { return self._s[631]! } - public var PhotoEditor_ShadowsTint: String { return self._s[632]! } - public var Message_Audio: String { return self._s[633]! } - public var Passport_Language_lt: String { return self._s[634]! } + public var Conversation_ClearCache: String { return self._s[662]! } + public var PhotoEditor_ShadowsTint: String { return self._s[663]! } + public var Message_Audio: String { return self._s[664]! } + public var Passport_Language_lt: String { return self._s[665]! } public func Message_PinnedTextMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[635]!, self._r[635]!, [_0]) + return formatWithArgumentRanges(self._s[666]!, self._r[666]!, [_0]) } - public var Permissions_SiriText_v0: String { return self._s[636]! } - public var Conversation_FileICloudDrive: String { return self._s[637]! } - public var ChatList_DeleteForEveryoneConfirmationTitle: String { return self._s[638]! } - public var Notifications_Badge_IncludeMutedChats: String { return self._s[639]! } + public var Permissions_SiriText_v0: String { return self._s[667]! } + public var Conversation_FileICloudDrive: String { return self._s[668]! } + public var ChatList_DeleteForEveryoneConfirmationTitle: String { return self._s[669]! } + public var Notifications_Badge_IncludeMutedChats: String { return self._s[670]! } public func Notification_NewAuthDetected(_ _1: String, _ _2: String, _ _3: String, _ _4: String, _ _5: String, _ _6: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[640]!, self._r[640]!, [_1, _2, _3, _4, _5, _6]) + return formatWithArgumentRanges(self._s[671]!, self._r[671]!, [_1, _2, _3, _4, _5, _6]) } - public var DialogList_ProxyConnectionIssuesTooltip: String { return self._s[641]! } + public var DialogList_ProxyConnectionIssuesTooltip: String { return self._s[672]! } public func Time_MonthOfYear_m5(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[642]!, self._r[642]!, [_0]) + return formatWithArgumentRanges(self._s[673]!, self._r[673]!, [_0]) } - public var Channel_SignMessages: String { return self._s[643]! } + public var Channel_SignMessages: String { return self._s[674]! } public func PUSH_MESSAGE_NOTEXT(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[644]!, self._r[644]!, [_1]) + return formatWithArgumentRanges(self._s[675]!, self._r[675]!, [_1]) } - public var Compose_ChannelTokenListPlaceholder: String { return self._s[645]! } - public var Passport_ScanPassport: String { return self._s[646]! } - public var Watch_Suggestion_Thanks: String { return self._s[647]! } - public var BlockedUsers_AddNew: String { return self._s[648]! } + public var Compose_ChannelTokenListPlaceholder: String { return self._s[676]! } + public var Passport_ScanPassport: String { return self._s[677]! } + public var Watch_Suggestion_Thanks: String { return self._s[678]! } + public var BlockedUsers_AddNew: String { return self._s[679]! } public func PUSH_CHAT_MESSAGE(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[649]!, self._r[649]!, [_1, _2]) + return formatWithArgumentRanges(self._s[680]!, self._r[680]!, [_1, _2]) } - public var Watch_Message_Invoice: String { return self._s[650]! } - public var SettingsSearch_Synonyms_Privacy_LastSeen: String { return self._s[651]! } - public var Month_GenJuly: String { return self._s[652]! } - public var UserInfo_StartSecretChatStart: String { return self._s[653]! } - public var SocksProxySetup_ProxySocks5: String { return self._s[654]! } - public var Notification_Exceptions_DeleteAllConfirmation: String { return self._s[656]! } - public var Notification_ChannelInviterSelf: String { return self._s[657]! } - public var CheckoutInfo_ReceiverInfoEmail: String { return self._s[658]! } + public var Watch_Message_Invoice: String { return self._s[681]! } + public var SettingsSearch_Synonyms_Privacy_LastSeen: String { return self._s[682]! } + public var Month_GenJuly: String { return self._s[683]! } + public var CreatePoll_QuizInfo: String { return self._s[684]! } + public var UserInfo_StartSecretChatStart: String { return self._s[685]! } + public var SocksProxySetup_ProxySocks5: String { return self._s[686]! } + public var IntentsSettings_SuggestByShare: String { return self._s[688]! } + public var Notification_Exceptions_DeleteAllConfirmation: String { return self._s[689]! } + public var Notification_ChannelInviterSelf: String { return self._s[690]! } + public var CheckoutInfo_ReceiverInfoEmail: String { return self._s[691]! } public func ApplyLanguage_ChangeLanguageUnofficialText(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[659]!, self._r[659]!, [_1, _2]) + return formatWithArgumentRanges(self._s[692]!, self._r[692]!, [_1, _2]) } - public var CheckoutInfo_Title: String { return self._s[660]! } - public var Watch_Stickers_RecentPlaceholder: String { return self._s[661]! } + public var CheckoutInfo_Title: String { return self._s[693]! } + public var Watch_Stickers_RecentPlaceholder: String { return self._s[694]! } public func Map_DistanceAway(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[662]!, self._r[662]!, [_0]) + return formatWithArgumentRanges(self._s[695]!, self._r[695]!, [_0]) } - public var Passport_Identity_MainPage: String { return self._s[663]! } - public var TwoStepAuth_ConfirmEmailResendCode: String { return self._s[664]! } - public var Passport_Language_de: String { return self._s[665]! } - public var Update_Title: String { return self._s[666]! } - public var ContactInfo_PhoneLabelWorkFax: String { return self._s[667]! } - public var Channel_AdminLog_BanEmbedLinks: String { return self._s[668]! } - public var Passport_Email_UseTelegramEmailHelp: String { return self._s[669]! } - public var Notifications_ChannelNotificationsPreview: String { return self._s[670]! } - public var NotificationsSound_Telegraph: String { return self._s[671]! } - public var Watch_LastSeen_ALongTimeAgo: String { return self._s[672]! } - public var ChannelMembers_WhoCanAddMembers: String { return self._s[673]! } + public var Passport_Identity_MainPage: String { return self._s[696]! } + public var TwoStepAuth_ConfirmEmailResendCode: String { return self._s[697]! } + public var Passport_Language_de: String { return self._s[698]! } + public var PeerInfo_PaneVoice: String { return self._s[699]! } + public var Update_Title: String { return self._s[700]! } + public var ContactInfo_PhoneLabelWorkFax: String { return self._s[701]! } + public var Channel_AdminLog_BanEmbedLinks: String { return self._s[702]! } + public var Passport_Email_UseTelegramEmailHelp: String { return self._s[703]! } + public var Notifications_ChannelNotificationsPreview: String { return self._s[704]! } + public var NotificationsSound_Telegraph: String { return self._s[705]! } + public var Watch_LastSeen_ALongTimeAgo: String { return self._s[706]! } + public var ChannelMembers_WhoCanAddMembers: String { return self._s[707]! } public func AutoDownloadSettings_UpTo(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[674]!, self._r[674]!, [_0]) + return formatWithArgumentRanges(self._s[708]!, self._r[708]!, [_0]) } - public var ClearCache_Description: String { return self._s[675]! } - public var Stickers_SuggestAll: String { return self._s[676]! } - public var Conversation_ForwardTitle: String { return self._s[677]! } - public var Appearance_ThemePreview_ChatList_7_Name: String { return self._s[678]! } + public var ClearCache_Description: String { return self._s[709]! } + public var Stickers_SuggestAll: String { return self._s[710]! } + public var Conversation_ForwardTitle: String { return self._s[711]! } + public var Appearance_ThemePreview_ChatList_7_Name: String { return self._s[712]! } public func Notification_JoinedChannel(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[679]!, self._r[679]!, [_0]) + return formatWithArgumentRanges(self._s[713]!, self._r[713]!, [_0]) } - public var Calls_NewCall: String { return self._s[680]! } - public var Call_StatusEnded: String { return self._s[681]! } - public var AutoDownloadSettings_DataUsageLow: String { return self._s[682]! } - public var Settings_ProxyConnected: String { return self._s[683]! } - public var Channel_AdminLogFilter_EventsPinned: String { return self._s[684]! } - public var PhotoEditor_QualityVeryLow: String { return self._s[685]! } - public var Channel_AdminLogFilter_EventsDeletedMessages: String { return self._s[686]! } - public var Passport_PasswordPlaceholder: String { return self._s[687]! } - public var Message_PinnedInvoice: String { return self._s[688]! } - public var Passport_Identity_IssueDate: String { return self._s[689]! } - public var Passport_Language_pl: String { return self._s[690]! } + public var Calls_NewCall: String { return self._s[714]! } + public var Call_StatusEnded: String { return self._s[715]! } + public var AutoDownloadSettings_DataUsageLow: String { return self._s[716]! } + public var Settings_ProxyConnected: String { return self._s[717]! } + public var Channel_AdminLogFilter_EventsPinned: String { return self._s[718]! } + public var PhotoEditor_QualityVeryLow: String { return self._s[719]! } + public var Channel_AdminLogFilter_EventsDeletedMessages: String { return self._s[720]! } + public var Passport_PasswordPlaceholder: String { return self._s[721]! } + public var Message_PinnedInvoice: String { return self._s[722]! } + public var Passport_Identity_IssueDate: String { return self._s[723]! } + public var Passport_Language_pl: String { return self._s[724]! } public func ChannelInfo_ChannelForbidden(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[691]!, self._r[691]!, [_0]) + return formatWithArgumentRanges(self._s[725]!, self._r[725]!, [_0]) } - public var SocksProxySetup_PasteFromClipboard: String { return self._s[692]! } - public var Call_StatusConnecting: String { return self._s[693]! } + public var Call_StatusConnecting: String { return self._s[726]! } + public var SocksProxySetup_PasteFromClipboard: String { return self._s[727]! } public func Username_UsernameIsAvailable(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[694]!, self._r[694]!, [_0]) + return formatWithArgumentRanges(self._s[728]!, self._r[728]!, [_0]) } - public var ChatSettings_ConnectionType_UseProxy: String { return self._s[696]! } - public var Common_Edit: String { return self._s[697]! } - public var PrivacySettings_LastSeenNobody: String { return self._s[698]! } + public var ChatSettings_ConnectionType_UseProxy: String { return self._s[730]! } + public var Common_Edit: String { return self._s[731]! } + public var PrivacySettings_LastSeenNobody: String { return self._s[732]! } public func Notification_LeftChat(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[699]!, self._r[699]!, [_0]) + return formatWithArgumentRanges(self._s[733]!, self._r[733]!, [_0]) } - public var GroupInfo_ChatAdmins: String { return self._s[700]! } - public var PrivateDataSettings_Title: String { return self._s[701]! } - public var Login_CancelPhoneVerificationStop: String { return self._s[702]! } - public var ChatList_Read: String { return self._s[703]! } - public var Wallet_WordImport_Text: String { return self._s[704]! } - public var Undo_ChatClearedForBothSides: String { return self._s[705]! } - public var GroupPermission_SectionTitle: String { return self._s[706]! } - public var TwoFactorSetup_Intro_Title: String { return self._s[708]! } + public var GroupInfo_ChatAdmins: String { return self._s[734]! } + public var PrivateDataSettings_Title: String { return self._s[735]! } + public var Login_CancelPhoneVerificationStop: String { return self._s[736]! } + public var ChatList_Read: String { return self._s[737]! } + public var Wallet_WordImport_Text: String { return self._s[738]! } + public var Undo_ChatClearedForBothSides: String { return self._s[739]! } + public var GroupPermission_SectionTitle: String { return self._s[740]! } + public var TwoFactorSetup_Intro_Title: String { return self._s[742]! } public func PUSH_CHAT_LEFT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[709]!, self._r[709]!, [_1, _2]) + return formatWithArgumentRanges(self._s[743]!, self._r[743]!, [_1, _2]) } - public var Checkout_ErrorPaymentFailed: String { return self._s[710]! } - public var Update_UpdateApp: String { return self._s[711]! } - public var Group_Username_RevokeExistingUsernamesInfo: String { return self._s[712]! } - public var Settings_Appearance: String { return self._s[713]! } - public var SettingsSearch_Synonyms_Stickers_SuggestStickers: String { return self._s[717]! } - public var Watch_Location_Access: String { return self._s[718]! } - public var ShareMenu_CopyShareLink: String { return self._s[720]! } - public var TwoStepAuth_SetupHintTitle: String { return self._s[721]! } - public var Conversation_Theme: String { return self._s[723]! } + public var Checkout_ErrorPaymentFailed: String { return self._s[744]! } + public var Update_UpdateApp: String { return self._s[745]! } + public var Group_Username_RevokeExistingUsernamesInfo: String { return self._s[746]! } + public var Settings_Appearance: String { return self._s[747]! } + public var SettingsSearch_Synonyms_Stickers_SuggestStickers: String { return self._s[751]! } + public var Watch_Location_Access: String { return self._s[752]! } + public var ShareMenu_CopyShareLink: String { return self._s[754]! } + public var TwoStepAuth_SetupHintTitle: String { return self._s[755]! } + public var Conversation_Theme: String { return self._s[757]! } public func DialogList_SingleRecordingVideoMessageSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[724]!, self._r[724]!, [_0]) + return formatWithArgumentRanges(self._s[758]!, self._r[758]!, [_0]) } - public var Notifications_ClassicTones: String { return self._s[725]! } - public var Weekday_ShortWednesday: String { return self._s[726]! } - public var WallpaperPreview_SwipeColorsBottomText: String { return self._s[727]! } - public var Undo_LeftGroup: String { return self._s[730]! } - public var Wallet_RestoreFailed_Text: String { return self._s[731]! } - public var Conversation_LinkDialogCopy: String { return self._s[732]! } - public var Wallet_TransactionInfo_NoAddress: String { return self._s[734]! } - public var Wallet_Navigation_Back: String { return self._s[735]! } - public var KeyCommand_FocusOnInputField: String { return self._s[736]! } - public var Contacts_SelectAll: String { return self._s[737]! } - public var Preview_SaveToCameraRoll: String { return self._s[738]! } - public var PrivacySettings_PasscodeOff: String { return self._s[739]! } - public var Appearance_ThemePreview_ChatList_6_Name: String { return self._s[740]! } - public var Wallpaper_Title: String { return self._s[741]! } - public var Conversation_FilePhotoOrVideo: String { return self._s[742]! } - public var AccessDenied_Camera: String { return self._s[743]! } - public var Watch_Compose_CurrentLocation: String { return self._s[744]! } - public var Channel_DiscussionGroup_MakeHistoryPublicProceed: String { return self._s[746]! } + public var Notifications_ClassicTones: String { return self._s[759]! } + public var Weekday_ShortWednesday: String { return self._s[760]! } + public var WallpaperPreview_SwipeColorsBottomText: String { return self._s[761]! } + public var Undo_LeftGroup: String { return self._s[764]! } + public var Wallet_RestoreFailed_Text: String { return self._s[765]! } + public var Conversation_LinkDialogCopy: String { return self._s[766]! } + public var Wallet_TransactionInfo_NoAddress: String { return self._s[768]! } + public var Wallet_Navigation_Back: String { return self._s[769]! } + public var KeyCommand_FocusOnInputField: String { return self._s[770]! } + public var Contacts_SelectAll: String { return self._s[771]! } + public var Preview_SaveToCameraRoll: String { return self._s[772]! } + public var PrivacySettings_PasscodeOff: String { return self._s[773]! } + public var Appearance_ThemePreview_ChatList_6_Name: String { return self._s[774]! } + public func PUSH_CHANNEL_MESSAGE_QUIZ(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[775]!, self._r[775]!, [_1]) + } + public var Wallpaper_Title: String { return self._s[776]! } + public var Conversation_FilePhotoOrVideo: String { return self._s[777]! } + public var AccessDenied_Camera: String { return self._s[778]! } + public var Watch_Compose_CurrentLocation: String { return self._s[779]! } + public var PeerInfo_ButtonMessage: String { return self._s[781]! } + public var Channel_DiscussionGroup_MakeHistoryPublicProceed: String { return self._s[782]! } public func SecretImage_NotViewedYet(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[747]!, self._r[747]!, [_0]) + return formatWithArgumentRanges(self._s[783]!, self._r[783]!, [_0]) } - public var GroupInfo_InvitationLinkDoesNotExist: String { return self._s[748]! } - public var Passport_Language_ro: String { return self._s[749]! } - public var EditTheme_UploadNewTheme: String { return self._s[750]! } - public var CheckoutInfo_SaveInfoHelp: String { return self._s[751]! } - public var Wallet_Intro_Terms: String { return self._s[752]! } + public var GroupInfo_InvitationLinkDoesNotExist: String { return self._s[784]! } + public var Passport_Language_ro: String { return self._s[785]! } + public var EditTheme_UploadNewTheme: String { return self._s[786]! } + public var CheckoutInfo_SaveInfoHelp: String { return self._s[787]! } + public var Wallet_Intro_Terms: String { return self._s[788]! } public func Notification_SecretChatMessageScreenshot(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[753]!, self._r[753]!, [_0]) + return formatWithArgumentRanges(self._s[789]!, self._r[789]!, [_0]) } - public var Login_CancelPhoneVerification: String { return self._s[754]! } - public var State_ConnectingToProxy: String { return self._s[755]! } - public var Calls_RatingTitle: String { return self._s[756]! } - public var Generic_ErrorMoreInfo: String { return self._s[757]! } - public var Appearance_PreviewReplyText: String { return self._s[758]! } - public var CheckoutInfo_ShippingInfoPostcodePlaceholder: String { return self._s[759]! } + public var Login_CancelPhoneVerification: String { return self._s[790]! } + public var State_ConnectingToProxy: String { return self._s[791]! } + public var Calls_RatingTitle: String { return self._s[792]! } + public var Generic_ErrorMoreInfo: String { return self._s[793]! } + public var ChatList_Search_ShowMore: String { return self._s[794]! } + public var Appearance_PreviewReplyText: String { return self._s[795]! } + public var CheckoutInfo_ShippingInfoPostcodePlaceholder: String { return self._s[796]! } public func Wallet_Send_Balance(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[760]!, self._r[760]!, [_0]) + return formatWithArgumentRanges(self._s[797]!, self._r[797]!, [_0]) } - public var SharedMedia_CategoryLinks: String { return self._s[761]! } - public var Calls_Missed: String { return self._s[762]! } - public var Cache_Photos: String { return self._s[766]! } - public var GroupPermission_NoAddMembers: String { return self._s[767]! } - public var ScheduledMessages_Title: String { return self._s[768]! } + public var IntentsSettings_SuggestedChatsContacts: String { return self._s[798]! } + public var SharedMedia_CategoryLinks: String { return self._s[799]! } + public var Calls_Missed: String { return self._s[800]! } + public var Cache_Photos: String { return self._s[804]! } + public var GroupPermission_NoAddMembers: String { return self._s[805]! } + public var ScheduledMessages_Title: String { return self._s[806]! } public func Channel_AdminLog_MessageUnpinned(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[769]!, self._r[769]!, [_0]) - } - public var Conversation_ShareBotLocationConfirmationTitle: String { return self._s[770]! } - public var Settings_ProxyDisabled: String { return self._s[771]! } - public func Settings_ApplyProxyAlertCredentials(_ _1: String, _ _2: String, _ _3: String, _ _4: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[772]!, self._r[772]!, [_1, _2, _3, _4]) - } - public func Conversation_RestrictedMediaTimed(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[773]!, self._r[773]!, [_0]) - } - public var ChatList_Context_RemoveFromRecents: String { return self._s[775]! } - public var Appearance_Title: String { return self._s[776]! } - public func Time_MonthOfYear_m2(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[778]!, self._r[778]!, [_0]) - } - public var Conversation_WalletRequiredText: String { return self._s[779]! } - public var StickerPacksSettings_ShowStickersButtonHelp: String { return self._s[780]! } - public var Channel_EditMessageErrorGeneric: String { return self._s[781]! } - public var Privacy_Calls_IntegrationHelp: String { return self._s[782]! } - public var Preview_DeletePhoto: String { return self._s[783]! } - public var Appearance_AppIconFilledX: String { return self._s[784]! } - public var PrivacySettings_PrivacyTitle: String { return self._s[785]! } - public func Conversation_BotInteractiveUrlAlert(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[786]!, self._r[786]!, [_0]) - } - public var Coub_TapForSound: String { return self._s[788]! } - public var Map_LocatingError: String { return self._s[789]! } - public var TwoStepAuth_EmailChangeSuccess: String { return self._s[791]! } - public var Conversation_SendMessage_SendSilently: String { return self._s[792]! } - public var VoiceOver_MessageContextOpenMessageMenu: String { return self._s[793]! } - public func Wallet_Time_PreciseDate_m8(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[794]!, self._r[794]!, [_1, _2, _3]) - } - public var Passport_ForgottenPassword: String { return self._s[795]! } - public var GroupInfo_InviteLink_RevokeLink: String { return self._s[796]! } - public var StickerPacksSettings_ArchivedPacks: String { return self._s[797]! } - public var Login_TermsOfServiceSignupDecline: String { return self._s[799]! } - public var Channel_Moderator_AccessLevelRevoke: String { return self._s[800]! } - public var Message_Location: String { return self._s[801]! } - public var Passport_Identity_NamePlaceholder: String { return self._s[802]! } - public var Channel_Management_Title: String { return self._s[803]! } - public var DialogList_SearchSectionDialogs: String { return self._s[805]! } - public var Compose_NewChannel_Members: String { return self._s[806]! } - public func DialogList_SingleUploadingFileSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[807]!, self._r[807]!, [_0]) } - public var GroupInfo_Location: String { return self._s[808]! } - public var Appearance_ThemePreview_ChatList_5_Name: String { return self._s[809]! } - public var ClearCache_Clear: String { return self._s[810]! } - public var AutoNightTheme_ScheduledFrom: String { return self._s[811]! } - public var PhotoEditor_WarmthTool: String { return self._s[812]! } - public var Passport_Language_tr: String { return self._s[813]! } + public var Conversation_ShareBotLocationConfirmationTitle: String { return self._s[808]! } + public var Settings_ProxyDisabled: String { return self._s[809]! } + public func Settings_ApplyProxyAlertCredentials(_ _1: String, _ _2: String, _ _3: String, _ _4: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[810]!, self._r[810]!, [_1, _2, _3, _4]) + } + public func Conversation_RestrictedMediaTimed(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[811]!, self._r[811]!, [_0]) + } + public var ChatList_Context_RemoveFromRecents: String { return self._s[813]! } + public var Appearance_Title: String { return self._s[814]! } + public func Time_MonthOfYear_m2(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[816]!, self._r[816]!, [_0]) + } + public var Conversation_WalletRequiredText: String { return self._s[817]! } + public var StickerPacksSettings_ShowStickersButtonHelp: String { return self._s[818]! } + public var OldChannels_NoticeCreateText: String { return self._s[819]! } + public var Channel_EditMessageErrorGeneric: String { return self._s[820]! } + public var Privacy_Calls_IntegrationHelp: String { return self._s[821]! } + public var Preview_DeletePhoto: String { return self._s[822]! } + public var Appearance_AppIconFilledX: String { return self._s[823]! } + public var PrivacySettings_PrivacyTitle: String { return self._s[824]! } + public func Conversation_BotInteractiveUrlAlert(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[825]!, self._r[825]!, [_0]) + } + public var Coub_TapForSound: String { return self._s[828]! } + public var Map_LocatingError: String { return self._s[829]! } + public var TwoStepAuth_EmailChangeSuccess: String { return self._s[831]! } + public var Conversation_SendMessage_SendSilently: String { return self._s[832]! } + public var VoiceOver_MessageContextOpenMessageMenu: String { return self._s[833]! } + public func Wallet_Time_PreciseDate_m8(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[834]!, self._r[834]!, [_1, _2, _3]) + } + public var Passport_ForgottenPassword: String { return self._s[835]! } + public var GroupInfo_InviteLink_RevokeLink: String { return self._s[836]! } + public var StickerPacksSettings_ArchivedPacks: String { return self._s[837]! } + public var Login_TermsOfServiceSignupDecline: String { return self._s[839]! } + public var Channel_Moderator_AccessLevelRevoke: String { return self._s[840]! } + public var Message_Location: String { return self._s[841]! } + public var Passport_Identity_NamePlaceholder: String { return self._s[842]! } + public var Channel_Management_Title: String { return self._s[843]! } + public var DialogList_SearchSectionDialogs: String { return self._s[845]! } + public var Compose_NewChannel_Members: String { return self._s[846]! } + public func DialogList_SingleUploadingFileSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[847]!, self._r[847]!, [_0]) + } + public var GroupInfo_Location: String { return self._s[848]! } + public var Appearance_ThemePreview_ChatList_5_Name: String { return self._s[849]! } + public var ClearCache_Clear: String { return self._s[850]! } + public var AutoNightTheme_ScheduledFrom: String { return self._s[851]! } + public var PhotoEditor_WarmthTool: String { return self._s[852]! } + public var Passport_Language_tr: String { return self._s[853]! } public func PUSH_MESSAGE_GAME_SCORE(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[814]!, self._r[814]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[854]!, self._r[854]!, [_1, _2, _3]) } - public var Login_ResetAccountProtected_Reset: String { return self._s[816]! } - public var Watch_PhotoView_Title: String { return self._s[817]! } - public var Passport_Phone_Delete: String { return self._s[818]! } - public var Undo_ChatDeletedForBothSides: String { return self._s[819]! } - public var Conversation_EditingMessageMediaEditCurrentPhoto: String { return self._s[820]! } - public var GroupInfo_Permissions: String { return self._s[821]! } - public var PasscodeSettings_TurnPasscodeOff: String { return self._s[822]! } - public var Profile_ShareContactButton: String { return self._s[823]! } - public var ChatSettings_Other: String { return self._s[824]! } - public var UserInfo_NotificationsDisabled: String { return self._s[825]! } - public var CheckoutInfo_ShippingInfoCity: String { return self._s[826]! } - public var LastSeen_WithinAMonth: String { return self._s[827]! } - public var VoiceOver_Chat_PlayHint: String { return self._s[828]! } - public var Conversation_ReportGroupLocation: String { return self._s[829]! } - public var Conversation_EncryptionCanceled: String { return self._s[830]! } - public var MediaPicker_GroupDescription: String { return self._s[831]! } - public var WebSearch_Images: String { return self._s[832]! } + public var OldChannels_NoticeUpgradeText: String { return self._s[855]! } + public var Login_ResetAccountProtected_Reset: String { return self._s[857]! } + public var Watch_PhotoView_Title: String { return self._s[858]! } + public var Passport_Phone_Delete: String { return self._s[859]! } + public var Undo_ChatDeletedForBothSides: String { return self._s[860]! } + public var Conversation_EditingMessageMediaEditCurrentPhoto: String { return self._s[861]! } + public var GroupInfo_Permissions: String { return self._s[862]! } + public var PasscodeSettings_TurnPasscodeOff: String { return self._s[863]! } + public var Profile_ShareContactButton: String { return self._s[864]! } + public var ChatSettings_Other: String { return self._s[865]! } + public var UserInfo_NotificationsDisabled: String { return self._s[866]! } + public var CheckoutInfo_ShippingInfoCity: String { return self._s[867]! } + public var LastSeen_WithinAMonth: String { return self._s[868]! } + public var VoiceOver_Chat_PlayHint: String { return self._s[869]! } + public var Conversation_ReportGroupLocation: String { return self._s[870]! } + public var Conversation_EncryptionCanceled: String { return self._s[871]! } + public var MediaPicker_GroupDescription: String { return self._s[872]! } + public var WebSearch_Images: String { return self._s[873]! } public func Channel_Management_PromotedBy(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[833]!, self._r[833]!, [_0]) + return formatWithArgumentRanges(self._s[874]!, self._r[874]!, [_0]) } - public var Message_Photo: String { return self._s[834]! } - public var PasscodeSettings_HelpBottom: String { return self._s[835]! } - public var AutoDownloadSettings_VideosTitle: String { return self._s[836]! } - public var VoiceOver_Media_PlaybackRateChange: String { return self._s[837]! } - public var Passport_Identity_AddDriversLicense: String { return self._s[838]! } - public var TwoStepAuth_EnterPasswordPassword: String { return self._s[839]! } - public var NotificationsSound_Calypso: String { return self._s[840]! } - public var Map_Map: String { return self._s[841]! } - public var CheckoutInfo_ReceiverInfoTitle: String { return self._s[843]! } - public var ChatSettings_TextSizeUnits: String { return self._s[844]! } + public var Message_Photo: String { return self._s[875]! } + public var PasscodeSettings_HelpBottom: String { return self._s[876]! } + public var AutoDownloadSettings_VideosTitle: String { return self._s[877]! } + public var VoiceOver_Media_PlaybackRateChange: String { return self._s[878]! } + public var Passport_Identity_AddDriversLicense: String { return self._s[879]! } + public var TwoStepAuth_EnterPasswordPassword: String { return self._s[880]! } + public var NotificationsSound_Calypso: String { return self._s[881]! } + public var Map_Map: String { return self._s[882]! } + public func Conversation_LiveLocationYouAndOther(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[883]!, self._r[883]!, [_0]) + } + public var CheckoutInfo_ReceiverInfoTitle: String { return self._s[885]! } + public var ChatSettings_TextSizeUnits: String { return self._s[886]! } public func VoiceOver_Chat_FileFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[845]!, self._r[845]!, [_0]) + return formatWithArgumentRanges(self._s[887]!, self._r[887]!, [_0]) } - public var Common_of: String { return self._s[846]! } - public var Conversation_ForwardContacts: String { return self._s[849]! } + public var Common_of: String { return self._s[888]! } + public var Conversation_ForwardContacts: String { return self._s[891]! } + public var IntentsSettings_SuggestByAll: String { return self._s[893]! } public func Call_AnsweringWithAccount(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[851]!, self._r[851]!, [_0]) + return formatWithArgumentRanges(self._s[894]!, self._r[894]!, [_0]) } - public var Passport_Language_hy: String { return self._s[852]! } - public var Notifications_MessageNotificationsHelp: String { return self._s[853]! } - public var AutoDownloadSettings_Reset: String { return self._s[854]! } - public var Wallet_TransactionInfo_AddressCopied: String { return self._s[855]! } - public var Paint_ClearConfirm: String { return self._s[856]! } - public var Camera_VideoMode: String { return self._s[857]! } + public var Passport_Language_hy: String { return self._s[895]! } + public var Notifications_MessageNotificationsHelp: String { return self._s[896]! } + public var AutoDownloadSettings_Reset: String { return self._s[897]! } + public var Wallet_TransactionInfo_AddressCopied: String { return self._s[898]! } + public var Paint_ClearConfirm: String { return self._s[899]! } + public var Camera_VideoMode: String { return self._s[900]! } public func Conversation_RestrictedStickersTimed(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[858]!, self._r[858]!, [_0]) + return formatWithArgumentRanges(self._s[901]!, self._r[901]!, [_0]) } - public var Privacy_Calls_AlwaysAllow_Placeholder: String { return self._s[859]! } - public var Conversation_ViewBackground: String { return self._s[860]! } + public var Privacy_Calls_AlwaysAllow_Placeholder: String { return self._s[902]! } + public var Conversation_ViewBackground: String { return self._s[903]! } public func Wallet_Info_TransactionDateHeaderYear(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[861]!, self._r[861]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[904]!, self._r[904]!, [_1, _2, _3]) } - public var Passport_Language_el: String { return self._s[862]! } - public var PhotoEditor_Original: String { return self._s[863]! } - public var Settings_FAQ_Button: String { return self._s[865]! } - public var Channel_Setup_PublicNoLink: String { return self._s[867]! } - public var Conversation_UnsupportedMedia: String { return self._s[868]! } - public var Conversation_SlideToCancel: String { return self._s[869]! } - public var Appearance_ThemePreview_ChatList_4_Name: String { return self._s[870]! } - public var Passport_Identity_OneOfTypeInternalPassport: String { return self._s[871]! } - public var CheckoutInfo_ShippingInfoPostcode: String { return self._s[872]! } - public var Conversation_ReportSpamChannelConfirmation: String { return self._s[873]! } - public var AutoNightTheme_NotAvailable: String { return self._s[874]! } - public var Conversation_Owner: String { return self._s[875]! } - public var Common_Create: String { return self._s[876]! } - public var Settings_ApplyProxyAlertEnable: String { return self._s[877]! } - public var ContactList_Context_Call: String { return self._s[878]! } - public var Localization_ChooseLanguage: String { return self._s[880]! } - public var ChatList_Context_AddToContacts: String { return self._s[882]! } - public var Settings_Proxy: String { return self._s[884]! } - public var Privacy_TopPeersHelp: String { return self._s[885]! } - public var CheckoutInfo_ShippingInfoCountryPlaceholder: String { return self._s[886]! } - public var Chat_UnsendMyMessages: String { return self._s[887]! } + public var Passport_Language_el: String { return self._s[905]! } + public var PhotoEditor_Original: String { return self._s[906]! } + public var Settings_FAQ_Button: String { return self._s[908]! } + public var Channel_Setup_PublicNoLink: String { return self._s[910]! } + public var Conversation_UnsupportedMedia: String { return self._s[911]! } + public var Conversation_SlideToCancel: String { return self._s[912]! } + public var Appearance_ThemePreview_ChatList_4_Name: String { return self._s[913]! } + public var Passport_Identity_OneOfTypeInternalPassport: String { return self._s[914]! } + public var CheckoutInfo_ShippingInfoPostcode: String { return self._s[915]! } + public var Conversation_ReportSpamChannelConfirmation: String { return self._s[916]! } + public var AutoNightTheme_NotAvailable: String { return self._s[917]! } + public var Conversation_Owner: String { return self._s[918]! } + public var Common_Create: String { return self._s[919]! } + public var Settings_ApplyProxyAlertEnable: String { return self._s[920]! } + public var ContactList_Context_Call: String { return self._s[921]! } + public var Localization_ChooseLanguage: String { return self._s[923]! } + public var ChatList_Context_AddToContacts: String { return self._s[925]! } + public var OldChannels_NoticeTitle: String { return self._s[926]! } + public var Settings_Proxy: String { return self._s[928]! } + public var Privacy_TopPeersHelp: String { return self._s[929]! } + public var CheckoutInfo_ShippingInfoCountryPlaceholder: String { return self._s[930]! } + public var Chat_UnsendMyMessages: String { return self._s[931]! } public func VoiceOver_Chat_Duration(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[888]!, self._r[888]!, [_0]) + return formatWithArgumentRanges(self._s[932]!, self._r[932]!, [_0]) } - public var TwoStepAuth_ConfirmationAbort: String { return self._s[889]! } + public var TwoStepAuth_ConfirmationAbort: String { return self._s[933]! } public func Contacts_AccessDeniedHelpPortrait(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[891]!, self._r[891]!, [_0]) + return formatWithArgumentRanges(self._s[935]!, self._r[935]!, [_0]) } - public var Contacts_SortedByPresence: String { return self._s[892]! } - public var Passport_Identity_SurnamePlaceholder: String { return self._s[893]! } - public var Cache_Title: String { return self._s[894]! } + public var Contacts_SortedByPresence: String { return self._s[936]! } + public var Passport_Identity_SurnamePlaceholder: String { return self._s[937]! } + public var Cache_Title: String { return self._s[938]! } public func Login_PhoneBannedEmailSubject(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[895]!, self._r[895]!, [_0]) + return formatWithArgumentRanges(self._s[939]!, self._r[939]!, [_0]) } - public var TwoStepAuth_EmailCodeExpired: String { return self._s[896]! } - public var Channel_Moderator_Title: String { return self._s[897]! } - public var InstantPage_AutoNightTheme: String { return self._s[899]! } + public var TwoStepAuth_EmailCodeExpired: String { return self._s[940]! } + public var Channel_Moderator_Title: String { return self._s[941]! } + public var InstantPage_AutoNightTheme: String { return self._s[943]! } public func PUSH_MESSAGE_POLL(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[902]!, self._r[902]!, [_1]) + return formatWithArgumentRanges(self._s[946]!, self._r[946]!, [_1]) } - public var Passport_Scans_Upload: String { return self._s[903]! } - public var Undo_Undo: String { return self._s[905]! } - public var Contacts_AccessDeniedHelpON: String { return self._s[906]! } - public var TwoStepAuth_RemovePassword: String { return self._s[907]! } - public var Common_Delete: String { return self._s[908]! } - public var Contacts_AddPeopleNearby: String { return self._s[910]! } - public var Conversation_ContextMenuDelete: String { return self._s[911]! } - public var SocksProxySetup_Credentials: String { return self._s[912]! } - public var Appearance_EditTheme: String { return self._s[914]! } - public var ClearCache_StorageOtherApps: String { return self._s[915]! } - public var PasscodeSettings_AutoLock_Disabled: String { return self._s[916]! } - public var Wallet_Send_NetworkErrorText: String { return self._s[917]! } - public var Passport_Address_OneOfTypeRentalAgreement: String { return self._s[920]! } - public var Conversation_ShareBotContactConfirmationTitle: String { return self._s[921]! } - public var Passport_Language_id: String { return self._s[923]! } - public var WallpaperSearch_ColorTeal: String { return self._s[924]! } - public var ChannelIntro_Title: String { return self._s[925]! } + public var Passport_Scans_Upload: String { return self._s[947]! } + public var Undo_Undo: String { return self._s[949]! } + public var Contacts_AccessDeniedHelpON: String { return self._s[950]! } + public var TwoStepAuth_RemovePassword: String { return self._s[951]! } + public var Common_Delete: String { return self._s[952]! } + public var Contacts_AddPeopleNearby: String { return self._s[954]! } + public var Conversation_ContextMenuDelete: String { return self._s[955]! } + public var SocksProxySetup_Credentials: String { return self._s[956]! } + public var Appearance_EditTheme: String { return self._s[958]! } + public var ClearCache_StorageOtherApps: String { return self._s[959]! } + public var PasscodeSettings_AutoLock_Disabled: String { return self._s[960]! } + public var Wallet_Send_NetworkErrorText: String { return self._s[961]! } + public var AuthSessions_DevicesTitle: String { return self._s[963]! } + public var Passport_Address_OneOfTypeRentalAgreement: String { return self._s[965]! } + public var Conversation_ShareBotContactConfirmationTitle: String { return self._s[966]! } + public var Passport_Language_id: String { return self._s[968]! } + public var WallpaperSearch_ColorTeal: String { return self._s[969]! } + public var ChannelIntro_Title: String { return self._s[970]! } public func Channel_AdminLog_MessageToggleSignaturesOff(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[926]!, self._r[926]!, [_0]) + return formatWithArgumentRanges(self._s[971]!, self._r[971]!, [_0]) } - public var VoiceOver_Chat_OpenLinkHint: String { return self._s[928]! } - public var VoiceOver_Chat_Reply: String { return self._s[929]! } - public var ScheduledMessages_BotActionUnavailable: String { return self._s[930]! } - public var Channel_Info_Description: String { return self._s[931]! } - public var Stickers_FavoriteStickers: String { return self._s[932]! } - public var Channel_BanUser_PermissionAddMembers: String { return self._s[933]! } - public var Notifications_DisplayNamesOnLockScreen: String { return self._s[934]! } - public var ChatSearch_ResultsTooltip: String { return self._s[935]! } - public var Wallet_VoiceOver_Editing_ClearText: String { return self._s[936]! } - public var Calls_NoMissedCallsPlacehoder: String { return self._s[937]! } - public var Group_PublicLink_Placeholder: String { return self._s[938]! } - public var Notifications_ExceptionsDefaultSound: String { return self._s[939]! } + public var VoiceOver_Chat_OpenLinkHint: String { return self._s[973]! } + public var VoiceOver_Chat_Reply: String { return self._s[974]! } + public var ScheduledMessages_BotActionUnavailable: String { return self._s[975]! } + public var Channel_Info_Description: String { return self._s[976]! } + public var Stickers_FavoriteStickers: String { return self._s[977]! } + public var Channel_BanUser_PermissionAddMembers: String { return self._s[978]! } + public var Notifications_DisplayNamesOnLockScreen: String { return self._s[979]! } + public var ChatSearch_ResultsTooltip: String { return self._s[980]! } + public var Wallet_VoiceOver_Editing_ClearText: String { return self._s[981]! } + public var Calls_NoMissedCallsPlacehoder: String { return self._s[982]! } + public var Group_PublicLink_Placeholder: String { return self._s[983]! } + public var Notifications_ExceptionsDefaultSound: String { return self._s[984]! } public func PUSH_CHANNEL_MESSAGE_POLL(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[940]!, self._r[940]!, [_1]) + return formatWithArgumentRanges(self._s[985]!, self._r[985]!, [_1]) } - public var TextFormat_Underline: String { return self._s[941]! } + public var TextFormat_Underline: String { return self._s[986]! } public func DialogList_SearchSubtitleFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[942]!, self._r[942]!, [_1, _2]) + return formatWithArgumentRanges(self._s[988]!, self._r[988]!, [_1, _2]) } public func Channel_AdminLog_MessageRemovedGroupStickerPack(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[943]!, self._r[943]!, [_0]) + return formatWithArgumentRanges(self._s[989]!, self._r[989]!, [_0]) } - public var Appearance_ThemePreview_ChatList_3_Name: String { return self._s[944]! } + public var Appearance_ThemePreview_ChatList_3_Name: String { return self._s[990]! } public func Channel_OwnershipTransfer_TransferCompleted(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[945]!, self._r[945]!, [_1, _2]) + return formatWithArgumentRanges(self._s[991]!, self._r[991]!, [_1, _2]) } - public var Wallet_Intro_ImportExisting: String { return self._s[946]! } - public var GroupPermission_Delete: String { return self._s[947]! } - public var Passport_Language_uk: String { return self._s[948]! } - public var StickerPack_HideStickers: String { return self._s[950]! } - public var ChangePhoneNumberNumber_NumberPlaceholder: String { return self._s[951]! } + public var Wallet_Intro_ImportExisting: String { return self._s[992]! } + public var GroupPermission_Delete: String { return self._s[993]! } + public var Passport_Language_uk: String { return self._s[994]! } + public var StickerPack_HideStickers: String { return self._s[996]! } + public var ChangePhoneNumberNumber_NumberPlaceholder: String { return self._s[997]! } public func PUSH_CHAT_MESSAGE_PHOTO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[952]!, self._r[952]!, [_1, _2]) + return formatWithArgumentRanges(self._s[998]!, self._r[998]!, [_1, _2]) } - public var Activity_UploadingVideoMessage: String { return self._s[953]! } + public var Activity_UploadingVideoMessage: String { return self._s[999]! } public func GroupPermission_ApplyAlertText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[954]!, self._r[954]!, [_0]) + return formatWithArgumentRanges(self._s[1000]!, self._r[1000]!, [_0]) } - public var Channel_TitleInfo: String { return self._s[955]! } - public var StickerPacksSettings_ArchivedPacks_Info: String { return self._s[956]! } - public var Settings_CallSettings: String { return self._s[957]! } - public var Camera_SquareMode: String { return self._s[958]! } - public var Conversation_SendMessage_ScheduleMessage: String { return self._s[959]! } - public var GroupInfo_SharedMediaNone: String { return self._s[960]! } + public var Channel_TitleInfo: String { return self._s[1001]! } + public var StickerPacksSettings_ArchivedPacks_Info: String { return self._s[1002]! } + public var Settings_CallSettings: String { return self._s[1003]! } + public var Camera_SquareMode: String { return self._s[1004]! } + public var Conversation_SendMessage_ScheduleMessage: String { return self._s[1005]! } + public var GroupInfo_SharedMediaNone: String { return self._s[1006]! } public func PUSH_MESSAGE_VIDEO_SECRET(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[961]!, self._r[961]!, [_1]) + return formatWithArgumentRanges(self._s[1007]!, self._r[1007]!, [_1]) } - public var Bot_GenericBotStatus: String { return self._s[962]! } - public var Application_Update: String { return self._s[964]! } - public var Month_ShortJanuary: String { return self._s[965]! } - public var Contacts_PermissionsKeepDisabled: String { return self._s[966]! } - public var Channel_AdminLog_BanReadMessages: String { return self._s[967]! } - public var Settings_AppLanguage_Unofficial: String { return self._s[968]! } - public var Passport_Address_Street2Placeholder: String { return self._s[969]! } + public var Bot_GenericBotStatus: String { return self._s[1008]! } + public var Application_Update: String { return self._s[1010]! } + public var Month_ShortJanuary: String { return self._s[1011]! } + public var Contacts_PermissionsKeepDisabled: String { return self._s[1012]! } + public var Channel_AdminLog_BanReadMessages: String { return self._s[1013]! } + public var Settings_AppLanguage_Unofficial: String { return self._s[1014]! } + public var Passport_Address_Street2Placeholder: String { return self._s[1015]! } public func Map_LiveLocationShortHour(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[970]!, self._r[970]!, [_0]) + return formatWithArgumentRanges(self._s[1016]!, self._r[1016]!, [_0]) } - public var NetworkUsageSettings_Cellular: String { return self._s[971]! } - public var Appearance_PreviewOutgoingText: String { return self._s[972]! } - public var Notifications_PermissionsAllowInSettings: String { return self._s[973]! } - public var AutoDownloadSettings_OnForAll: String { return self._s[975]! } - public var Map_Directions: String { return self._s[976]! } - public var Passport_FieldIdentityTranslationHelp: String { return self._s[978]! } - public var Appearance_ThemeDay: String { return self._s[979]! } - public var LogoutOptions_LogOut: String { return self._s[980]! } - public var Group_PublicLink_Title: String { return self._s[982]! } - public var Channel_AddBotErrorNoRights: String { return self._s[983]! } - public var Passport_Identity_AddPassport: String { return self._s[984]! } - public var LocalGroup_ButtonTitle: String { return self._s[985]! } - public var Call_Message: String { return self._s[986]! } - public var PhotoEditor_ExposureTool: String { return self._s[987]! } - public var Wallet_Receive_CommentInfo: String { return self._s[989]! } - public var Passport_FieldOneOf_Delimeter: String { return self._s[990]! } - public var Channel_AdminLog_CanBanUsers: String { return self._s[992]! } - public var Appearance_ThemePreview_ChatList_2_Name: String { return self._s[993]! } - public var Appearance_Preview: String { return self._s[994]! } - public var Compose_ChannelMembers: String { return self._s[995]! } - public var Conversation_DeleteManyMessages: String { return self._s[996]! } - public var ReportPeer_ReasonOther_Title: String { return self._s[997]! } - public var Checkout_ErrorProviderAccountTimeout: String { return self._s[998]! } - public var TwoStepAuth_ResetAccountConfirmation: String { return self._s[999]! } - public var Channel_Stickers_CreateYourOwn: String { return self._s[1002]! } - public var Conversation_UpdateTelegram: String { return self._s[1003]! } - public var EditTheme_Create_TopInfo: String { return self._s[1004]! } + public var NetworkUsageSettings_Cellular: String { return self._s[1017]! } + public var Appearance_PreviewOutgoingText: String { return self._s[1018]! } + public func StickerPackActionInfo_RemovedText(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1019]!, self._r[1019]!, [_0]) + } + public var Notifications_PermissionsAllowInSettings: String { return self._s[1020]! } + public var AutoDownloadSettings_OnForAll: String { return self._s[1022]! } + public var Map_Directions: String { return self._s[1023]! } + public var Passport_FieldIdentityTranslationHelp: String { return self._s[1025]! } + public var Appearance_ThemeDay: String { return self._s[1026]! } + public var LogoutOptions_LogOut: String { return self._s[1027]! } + public var Group_PublicLink_Title: String { return self._s[1029]! } + public var Channel_AddBotErrorNoRights: String { return self._s[1030]! } + public var ChatList_Search_ShowLess: String { return self._s[1031]! } + public var Passport_Identity_AddPassport: String { return self._s[1032]! } + public var LocalGroup_ButtonTitle: String { return self._s[1033]! } + public var Call_Message: String { return self._s[1034]! } + public var PhotoEditor_ExposureTool: String { return self._s[1035]! } + public var Wallet_Receive_CommentInfo: String { return self._s[1037]! } + public var Passport_FieldOneOf_Delimeter: String { return self._s[1038]! } + public var Channel_AdminLog_CanBanUsers: String { return self._s[1040]! } + public var Appearance_ThemePreview_ChatList_2_Name: String { return self._s[1041]! } + public var Appearance_Preview: String { return self._s[1042]! } + public var Compose_ChannelMembers: String { return self._s[1043]! } + public var Conversation_DeleteManyMessages: String { return self._s[1044]! } + public var ReportPeer_ReasonOther_Title: String { return self._s[1045]! } + public var Checkout_ErrorProviderAccountTimeout: String { return self._s[1046]! } + public var TwoStepAuth_ResetAccountConfirmation: String { return self._s[1047]! } + public var Channel_Stickers_CreateYourOwn: String { return self._s[1050]! } + public var Conversation_UpdateTelegram: String { return self._s[1051]! } + public var EditTheme_Create_TopInfo: String { return self._s[1052]! } public func Notification_PinnedPhotoMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1005]!, self._r[1005]!, [_0]) + return formatWithArgumentRanges(self._s[1053]!, self._r[1053]!, [_0]) } - public var Wallet_WordCheck_Continue: String { return self._s[1006]! } - public var TwoFactorSetup_Hint_Action: String { return self._s[1007]! } + public var Wallet_WordCheck_Continue: String { return self._s[1054]! } + public var TwoFactorSetup_Hint_Action: String { return self._s[1055]! } + public var IntentsSettings_ResetAll: String { return self._s[1056]! } public func PUSH_PINNED_GIF(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1008]!, self._r[1008]!, [_1]) + return formatWithArgumentRanges(self._s[1057]!, self._r[1057]!, [_1]) } - public var GroupInfo_Administrators_Title: String { return self._s[1009]! } - public var Privacy_Forwards_PreviewMessageText: String { return self._s[1010]! } + public var GroupInfo_Administrators_Title: String { return self._s[1058]! } + public var Privacy_Forwards_PreviewMessageText: String { return self._s[1059]! } public func PrivacySettings_LastSeenNobodyPlus(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1011]!, self._r[1011]!, [_0]) + return formatWithArgumentRanges(self._s[1060]!, self._r[1060]!, [_0]) } - public var Tour_Title3: String { return self._s[1012]! } - public var Channel_EditAdmin_PermissionInviteSubscribers: String { return self._s[1013]! } - public var Clipboard_SendPhoto: String { return self._s[1017]! } - public var MediaPicker_Videos: String { return self._s[1018]! } - public var Passport_Email_Title: String { return self._s[1019]! } + public var Tour_Title3: String { return self._s[1061]! } + public var Channel_EditAdmin_PermissionInviteSubscribers: String { return self._s[1062]! } + public var Clipboard_SendPhoto: String { return self._s[1066]! } + public var MediaPicker_Videos: String { return self._s[1067]! } + public var Passport_Email_Title: String { return self._s[1068]! } public func PrivacySettings_LastSeenEverybodyMinus(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1020]!, self._r[1020]!, [_0]) + return formatWithArgumentRanges(self._s[1069]!, self._r[1069]!, [_0]) } - public var StickerPacksSettings_Title: String { return self._s[1021]! } - public var Conversation_MessageDialogDelete: String { return self._s[1022]! } - public var Privacy_Calls_CustomHelp: String { return self._s[1024]! } - public var Message_Wallpaper: String { return self._s[1025]! } - public var MemberSearch_BotSection: String { return self._s[1026]! } - public var GroupInfo_SetSound: String { return self._s[1027]! } - public var Core_ServiceUserStatus: String { return self._s[1028]! } - public var LiveLocationUpdated_JustNow: String { return self._s[1029]! } - public var Call_StatusFailed: String { return self._s[1030]! } - public var TwoFactorSetup_Email_Placeholder: String { return self._s[1031]! } - public var TwoStepAuth_SetupPasswordDescription: String { return self._s[1032]! } - public var TwoStepAuth_SetPassword: String { return self._s[1033]! } - public var Permissions_PeopleNearbyText_v0: String { return self._s[1034]! } + public var StickerPacksSettings_Title: String { return self._s[1070]! } + public var Conversation_MessageDialogDelete: String { return self._s[1071]! } + public var Privacy_Calls_CustomHelp: String { return self._s[1073]! } + public var Message_Wallpaper: String { return self._s[1074]! } + public var MemberSearch_BotSection: String { return self._s[1075]! } + public var GroupInfo_SetSound: String { return self._s[1076]! } + public func Time_TomorrowAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1077]!, self._r[1077]!, [_0]) + } + public var Core_ServiceUserStatus: String { return self._s[1078]! } + public var LiveLocationUpdated_JustNow: String { return self._s[1079]! } + public var Call_StatusFailed: String { return self._s[1080]! } + public var TwoFactorSetup_Email_Placeholder: String { return self._s[1081]! } + public var TwoStepAuth_SetupPasswordDescription: String { return self._s[1082]! } + public var TwoStepAuth_SetPassword: String { return self._s[1083]! } + public var Permissions_PeopleNearbyText_v0: String { return self._s[1084]! } public func SocksProxySetup_ProxyStatusPing(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1036]!, self._r[1036]!, [_0]) + return formatWithArgumentRanges(self._s[1086]!, self._r[1086]!, [_0]) } - public var Calls_SubmitRating: String { return self._s[1037]! } - public var Profile_Username: String { return self._s[1038]! } - public var Bot_DescriptionTitle: String { return self._s[1039]! } - public var MaskStickerSettings_Title: String { return self._s[1040]! } - public var SharedMedia_CategoryOther: String { return self._s[1041]! } - public var GroupInfo_SetGroupPhoto: String { return self._s[1042]! } - public var Common_NotNow: String { return self._s[1043]! } - public var CallFeedback_IncludeLogsInfo: String { return self._s[1044]! } - public var Conversation_ShareMyPhoneNumber: String { return self._s[1045]! } - public var Map_Location: String { return self._s[1046]! } - public var Invitation_JoinGroup: String { return self._s[1047]! } - public var AutoDownloadSettings_Title: String { return self._s[1049]! } - public var Conversation_DiscardVoiceMessageDescription: String { return self._s[1050]! } - public var Channel_ErrorAddBlocked: String { return self._s[1051]! } - public var Conversation_UnblockUser: String { return self._s[1052]! } - public var EditTheme_Edit_TopInfo: String { return self._s[1053]! } - public var Watch_Bot_Restart: String { return self._s[1054]! } - public var TwoStepAuth_Title: String { return self._s[1055]! } - public var Channel_AdminLog_BanSendMessages: String { return self._s[1056]! } - public var Checkout_ShippingMethod: String { return self._s[1057]! } - public var Passport_Identity_OneOfTypeIdentityCard: String { return self._s[1058]! } + public var Calls_SubmitRating: String { return self._s[1087]! } + public var Map_NoPlacesNearby: String { return self._s[1088]! } + public var Profile_Username: String { return self._s[1089]! } + public var Bot_DescriptionTitle: String { return self._s[1090]! } + public var MaskStickerSettings_Title: String { return self._s[1091]! } + public var SharedMedia_CategoryOther: String { return self._s[1092]! } + public var GroupInfo_SetGroupPhoto: String { return self._s[1093]! } + public var Common_NotNow: String { return self._s[1094]! } + public var CallFeedback_IncludeLogsInfo: String { return self._s[1095]! } + public var Conversation_ShareMyPhoneNumber: String { return self._s[1096]! } + public var Map_Location: String { return self._s[1097]! } + public var Invitation_JoinGroup: String { return self._s[1098]! } + public var AutoDownloadSettings_Title: String { return self._s[1100]! } + public var Conversation_DiscardVoiceMessageDescription: String { return self._s[1101]! } + public var Channel_ErrorAddBlocked: String { return self._s[1102]! } + public var Conversation_UnblockUser: String { return self._s[1103]! } + public var EditTheme_Edit_TopInfo: String { return self._s[1104]! } + public var Watch_Bot_Restart: String { return self._s[1105]! } + public var TwoStepAuth_Title: String { return self._s[1106]! } + public var Channel_AdminLog_BanSendMessages: String { return self._s[1107]! } + public var Checkout_ShippingMethod: String { return self._s[1108]! } + public var Passport_Identity_OneOfTypeIdentityCard: String { return self._s[1109]! } public func PUSH_CHAT_MESSAGE_STICKER(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1059]!, self._r[1059]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[1110]!, self._r[1110]!, [_1, _2, _3]) } + public var PeerInfo_ButtonDiscuss: String { return self._s[1111]! } + public var EditTheme_ChangeColors: String { return self._s[1113]! } public func Chat_UnsendMyMessagesAlertTitle(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1061]!, self._r[1061]!, [_0]) + return formatWithArgumentRanges(self._s[1114]!, self._r[1114]!, [_0]) } public func Channel_Username_LinkHint(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1062]!, self._r[1062]!, [_0]) + return formatWithArgumentRanges(self._s[1115]!, self._r[1115]!, [_0]) } - public var Appearance_ThemePreview_ChatList_1_Name: String { return self._s[1063]! } - public var SettingsSearch_Synonyms_Data_AutoplayGifs: String { return self._s[1064]! } - public var AuthSessions_TerminateOtherSessions: String { return self._s[1065]! } - public var Contacts_FailedToSendInvitesMessage: String { return self._s[1066]! } - public var PrivacySettings_TwoStepAuth: String { return self._s[1067]! } - public var Notification_Exceptions_PreviewAlwaysOn: String { return self._s[1068]! } - public var SettingsSearch_Synonyms_Privacy_Passcode: String { return self._s[1069]! } - public var Conversation_EditingMessagePanelMedia: String { return self._s[1070]! } - public var Checkout_PaymentMethod_Title: String { return self._s[1071]! } - public var SocksProxySetup_Connection: String { return self._s[1072]! } - public var Group_MessagePhotoRemoved: String { return self._s[1073]! } - public var Channel_Stickers_NotFound: String { return self._s[1076]! } - public var Group_About_Help: String { return self._s[1077]! } - public var Notification_PassportValueProofOfIdentity: String { return self._s[1078]! } - public var PeopleNearby_Title: String { return self._s[1080]! } + public var Appearance_ThemePreview_ChatList_1_Name: String { return self._s[1116]! } + public var SettingsSearch_Synonyms_Data_AutoplayGifs: String { return self._s[1117]! } + public var AuthSessions_TerminateOtherSessions: String { return self._s[1118]! } + public var Contacts_FailedToSendInvitesMessage: String { return self._s[1119]! } + public var PrivacySettings_TwoStepAuth: String { return self._s[1120]! } + public var Notification_Exceptions_PreviewAlwaysOn: String { return self._s[1121]! } + public var SettingsSearch_Synonyms_Privacy_Passcode: String { return self._s[1122]! } + public var Conversation_EditingMessagePanelMedia: String { return self._s[1123]! } + public var Checkout_PaymentMethod_Title: String { return self._s[1124]! } + public var SocksProxySetup_Connection: String { return self._s[1125]! } + public var Group_MessagePhotoRemoved: String { return self._s[1126]! } + public var PeopleNearby_MakeInvisible: String { return self._s[1128]! } + public var Channel_Stickers_NotFound: String { return self._s[1130]! } + public var Group_About_Help: String { return self._s[1131]! } + public var Notification_PassportValueProofOfIdentity: String { return self._s[1132]! } + public var PeopleNearby_Title: String { return self._s[1134]! } public func ApplyLanguage_ChangeLanguageOfficialText(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1081]!, self._r[1081]!, [_1]) + return formatWithArgumentRanges(self._s[1135]!, self._r[1135]!, [_1]) } - public var CheckoutInfo_ShippingInfoStatePlaceholder: String { return self._s[1083]! } - public var Notifications_GroupNotificationsExceptionsHelp: String { return self._s[1084]! } - public var SocksProxySetup_Password: String { return self._s[1085]! } - public var Notifications_PermissionsEnable: String { return self._s[1086]! } - public var TwoStepAuth_ChangeEmail: String { return self._s[1088]! } + public var Map_Home: String { return self._s[1136]! } + public var CheckoutInfo_ShippingInfoStatePlaceholder: String { return self._s[1138]! } + public var Notifications_GroupNotificationsExceptionsHelp: String { return self._s[1139]! } + public var SocksProxySetup_Password: String { return self._s[1140]! } + public var Notifications_PermissionsEnable: String { return self._s[1141]! } + public var TwoStepAuth_ChangeEmail: String { return self._s[1143]! } public func Channel_AdminLog_MessageInvitedName(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1089]!, self._r[1089]!, [_1]) + return formatWithArgumentRanges(self._s[1144]!, self._r[1144]!, [_1]) } public func Time_MonthOfYear_m10(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1091]!, self._r[1091]!, [_0]) + return formatWithArgumentRanges(self._s[1146]!, self._r[1146]!, [_0]) } - public var Passport_Identity_TypeDriversLicense: String { return self._s[1092]! } - public var ArchivedPacksAlert_Title: String { return self._s[1093]! } - public var Wallet_Receive_InvoiceUrlCopied: String { return self._s[1094]! } + public var Passport_Identity_TypeDriversLicense: String { return self._s[1147]! } + public var ArchivedPacksAlert_Title: String { return self._s[1148]! } + public var Wallet_Receive_InvoiceUrlCopied: String { return self._s[1149]! } + public var Map_PlacesNearby: String { return self._s[1150]! } public func Time_PreciseDate_m7(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1095]!, self._r[1095]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[1151]!, self._r[1151]!, [_1, _2, _3]) } - public var PrivacyLastSeenSettings_GroupsAndChannelsHelp: String { return self._s[1096]! } - public var Privacy_Calls_NeverAllow_Placeholder: String { return self._s[1098]! } - public var Conversation_StatusTyping: String { return self._s[1099]! } - public var Broadcast_AdminLog_EmptyText: String { return self._s[1100]! } - public var Notification_PassportValueProofOfAddress: String { return self._s[1101]! } - public var UserInfo_CreateNewContact: String { return self._s[1102]! } - public var Passport_Identity_FrontSide: String { return self._s[1103]! } - public var Login_PhoneNumberAlreadyAuthorizedSwitch: String { return self._s[1104]! } - public var Calls_CallTabTitle: String { return self._s[1105]! } - public var Channel_AdminLog_ChannelEmptyText: String { return self._s[1106]! } + public var PrivacyLastSeenSettings_GroupsAndChannelsHelp: String { return self._s[1152]! } + public var Privacy_Calls_NeverAllow_Placeholder: String { return self._s[1154]! } + public var Conversation_StatusTyping: String { return self._s[1155]! } + public var Broadcast_AdminLog_EmptyText: String { return self._s[1156]! } + public var Notification_PassportValueProofOfAddress: String { return self._s[1157]! } + public var UserInfo_CreateNewContact: String { return self._s[1158]! } + public var Passport_Identity_FrontSide: String { return self._s[1159]! } + public var Login_PhoneNumberAlreadyAuthorizedSwitch: String { return self._s[1160]! } + public var Calls_CallTabTitle: String { return self._s[1161]! } + public var Channel_AdminLog_ChannelEmptyText: String { return self._s[1162]! } public func Login_BannedPhoneBody(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1108]!, self._r[1108]!, [_0]) + return formatWithArgumentRanges(self._s[1164]!, self._r[1164]!, [_0]) } - public var Watch_UserInfo_MuteTitle: String { return self._s[1109]! } - public var Group_EditAdmin_RankAdminPlaceholder: String { return self._s[1110]! } - public var SharedMedia_EmptyMusicText: String { return self._s[1111]! } - public var Wallet_Completed_Text: String { return self._s[1112]! } - public var PasscodeSettings_AutoLock_IfAwayFor_1minute: String { return self._s[1113]! } - public var Paint_Stickers: String { return self._s[1114]! } - public var Privacy_GroupsAndChannels: String { return self._s[1115]! } - public var ChatList_Context_Delete: String { return self._s[1117]! } - public var UserInfo_AddContact: String { return self._s[1118]! } + public var Watch_UserInfo_MuteTitle: String { return self._s[1165]! } + public var Group_EditAdmin_RankAdminPlaceholder: String { return self._s[1166]! } + public var SharedMedia_EmptyMusicText: String { return self._s[1167]! } + public var Wallet_Completed_Text: String { return self._s[1168]! } + public var PasscodeSettings_AutoLock_IfAwayFor_1minute: String { return self._s[1169]! } + public var Paint_Stickers: String { return self._s[1170]! } + public var Privacy_GroupsAndChannels: String { return self._s[1171]! } + public var ChatList_Context_Delete: String { return self._s[1173]! } + public var UserInfo_AddContact: String { return self._s[1174]! } public func Conversation_MessageViaUser(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1119]!, self._r[1119]!, [_0]) + return formatWithArgumentRanges(self._s[1175]!, self._r[1175]!, [_0]) } - public var PhoneNumberHelp_ChangeNumber: String { return self._s[1121]! } + public var PhoneNumberHelp_ChangeNumber: String { return self._s[1177]! } public func ChatList_ClearChatConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1123]!, self._r[1123]!, [_0]) + return formatWithArgumentRanges(self._s[1179]!, self._r[1179]!, [_0]) } - public var DialogList_NoMessagesTitle: String { return self._s[1124]! } - public var EditProfile_NameAndPhotoHelp: String { return self._s[1125]! } - public var BlockedUsers_BlockUser: String { return self._s[1126]! } - public var Notifications_PermissionsOpenSettings: String { return self._s[1127]! } - public var MediaPicker_UngroupDescription: String { return self._s[1128]! } - public var Watch_NoConnection: String { return self._s[1129]! } - public var Month_GenSeptember: String { return self._s[1130]! } - public var Conversation_ViewGroup: String { return self._s[1132]! } - public var Channel_AdminLogFilter_EventsLeavingSubscribers: String { return self._s[1135]! } - public var Privacy_Forwards_AlwaysLink: String { return self._s[1136]! } - public var Channel_OwnershipTransfer_ErrorAdminsTooMuch: String { return self._s[1137]! } - public var Passport_FieldOneOf_FinalDelimeter: String { return self._s[1138]! } - public var Wallet_WordCheck_IncorrectHeader: String { return self._s[1139]! } - public var MediaPicker_CameraRoll: String { return self._s[1141]! } - public var Month_GenAugust: String { return self._s[1142]! } - public var Wallet_Configuration_SourceHeader: String { return self._s[1143]! } - public var AccessDenied_VideoMessageMicrophone: String { return self._s[1144]! } - public var SharedMedia_EmptyText: String { return self._s[1145]! } - public var Map_ShareLiveLocation: String { return self._s[1146]! } - public var Calls_All: String { return self._s[1147]! } - public var Appearance_ThemeNight: String { return self._s[1150]! } - public var Conversation_HoldForAudio: String { return self._s[1151]! } - public var SettingsSearch_Synonyms_Support: String { return self._s[1154]! } - public var GroupInfo_GroupHistoryHidden: String { return self._s[1155]! } - public var SocksProxySetup_Secret: String { return self._s[1156]! } + public var DialogList_NoMessagesTitle: String { return self._s[1180]! } + public var EditProfile_NameAndPhotoHelp: String { return self._s[1181]! } + public var BlockedUsers_BlockUser: String { return self._s[1182]! } + public var Notifications_PermissionsOpenSettings: String { return self._s[1183]! } + public var MediaPicker_UngroupDescription: String { return self._s[1185]! } + public var Watch_NoConnection: String { return self._s[1186]! } + public var Month_GenSeptember: String { return self._s[1187]! } + public var Conversation_ViewGroup: String { return self._s[1189]! } + public var Channel_AdminLogFilter_EventsLeavingSubscribers: String { return self._s[1192]! } + public var Privacy_Forwards_AlwaysLink: String { return self._s[1193]! } + public var Channel_OwnershipTransfer_ErrorAdminsTooMuch: String { return self._s[1194]! } + public var Passport_FieldOneOf_FinalDelimeter: String { return self._s[1195]! } + public var Wallet_WordCheck_IncorrectHeader: String { return self._s[1196]! } + public var MediaPicker_CameraRoll: String { return self._s[1198]! } + public var Month_GenAugust: String { return self._s[1199]! } + public var Wallet_Configuration_SourceHeader: String { return self._s[1200]! } + public var AccessDenied_VideoMessageMicrophone: String { return self._s[1201]! } + public var SharedMedia_EmptyText: String { return self._s[1202]! } + public var Map_ShareLiveLocation: String { return self._s[1203]! } + public var Calls_All: String { return self._s[1204]! } + public var Map_SendThisPlace: String { return self._s[1206]! } + public var Appearance_ThemeNight: String { return self._s[1208]! } + public var Conversation_HoldForAudio: String { return self._s[1209]! } + public var SettingsSearch_Synonyms_Support: String { return self._s[1212]! } + public var GroupInfo_GroupHistoryHidden: String { return self._s[1213]! } + public var SocksProxySetup_Secret: String { return self._s[1214]! } public func Activity_RemindAboutChannel(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1157]!, self._r[1157]!, [_0]) + return formatWithArgumentRanges(self._s[1215]!, self._r[1215]!, [_0]) } - public var Channel_BanList_RestrictedTitle: String { return self._s[1159]! } - public var Conversation_Location: String { return self._s[1160]! } + public var Channel_BanList_RestrictedTitle: String { return self._s[1217]! } + public var Conversation_Location: String { return self._s[1218]! } public func AutoDownloadSettings_UpToFor(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1161]!, self._r[1161]!, [_1, _2]) + return formatWithArgumentRanges(self._s[1219]!, self._r[1219]!, [_1, _2]) } - public var ChatSettings_AutoDownloadPhotos: String { return self._s[1163]! } - public var SettingsSearch_Synonyms_Privacy_Title: String { return self._s[1164]! } - public var Notifications_PermissionsText: String { return self._s[1165]! } - public var SettingsSearch_Synonyms_Data_SaveIncomingPhotos: String { return self._s[1166]! } - public var Call_Flip: String { return self._s[1167]! } - public var Channel_AdminLog_CanDeleteMessagesOfOthers: String { return self._s[1169]! } - public var SocksProxySetup_ProxyStatusConnecting: String { return self._s[1170]! } - public var Wallet_TransactionInfo_StorageFeeInfoUrl: String { return self._s[1171]! } - public var PrivacyPhoneNumberSettings_DiscoveryHeader: String { return self._s[1172]! } - public var Channel_EditAdmin_PermissionPinMessages: String { return self._s[1174]! } - public var TwoStepAuth_ReEnterPasswordDescription: String { return self._s[1176]! } - public var Channel_TooMuchBots: String { return self._s[1178]! } - public var Passport_DeletePassportConfirmation: String { return self._s[1179]! } - public var Login_InvalidCodeError: String { return self._s[1180]! } - public var StickerPacksSettings_FeaturedPacks: String { return self._s[1181]! } + public var ChatSettings_AutoDownloadPhotos: String { return self._s[1221]! } + public var SettingsSearch_Synonyms_Privacy_Title: String { return self._s[1222]! } + public var Notifications_PermissionsText: String { return self._s[1223]! } + public var SettingsSearch_Synonyms_Data_SaveIncomingPhotos: String { return self._s[1224]! } + public var Call_Flip: String { return self._s[1225]! } + public var Channel_AdminLog_CanDeleteMessagesOfOthers: String { return self._s[1227]! } + public var SocksProxySetup_ProxyStatusConnecting: String { return self._s[1228]! } + public var Wallet_TransactionInfo_StorageFeeInfoUrl: String { return self._s[1229]! } + public var PrivacyPhoneNumberSettings_DiscoveryHeader: String { return self._s[1230]! } + public var Channel_EditAdmin_PermissionPinMessages: String { return self._s[1232]! } + public var TwoStepAuth_ReEnterPasswordDescription: String { return self._s[1234]! } + public var Channel_TooMuchBots: String { return self._s[1236]! } + public var Passport_DeletePassportConfirmation: String { return self._s[1237]! } + public var Login_InvalidCodeError: String { return self._s[1238]! } + public var StickerPacksSettings_FeaturedPacks: String { return self._s[1239]! } public func ChatList_DeleteSecretChatConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1182]!, self._r[1182]!, [_0]) + return formatWithArgumentRanges(self._s[1240]!, self._r[1240]!, [_0]) } public func GroupInfo_InvitationLinkAcceptChannel(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1183]!, self._r[1183]!, [_0]) + return formatWithArgumentRanges(self._s[1241]!, self._r[1241]!, [_0]) } - public var VoiceOver_Navigation_ProxySettings: String { return self._s[1184]! } - public var Call_CallInProgressTitle: String { return self._s[1185]! } - public var Month_ShortSeptember: String { return self._s[1186]! } - public var Watch_ChannelInfo_Title: String { return self._s[1187]! } - public var ChatList_DeleteSavedMessagesConfirmation: String { return self._s[1190]! } - public var DialogList_PasscodeLockHelp: String { return self._s[1191]! } - public var Chat_MultipleTextMessagesDisabled: String { return self._s[1192]! } - public var Wallet_Receive_Title: String { return self._s[1193]! } - public var Notifications_Badge_IncludePublicGroups: String { return self._s[1194]! } - public var Channel_AdminLogFilter_EventsTitle: String { return self._s[1195]! } - public var PhotoEditor_CropReset: String { return self._s[1196]! } - public var Group_Username_CreatePrivateLinkHelp: String { return self._s[1198]! } - public var Channel_Management_LabelEditor: String { return self._s[1199]! } - public var Passport_Identity_LatinNameHelp: String { return self._s[1201]! } - public var PhotoEditor_HighlightsTool: String { return self._s[1202]! } - public var Wallet_Info_WalletCreated: String { return self._s[1203]! } - public var UserInfo_Title: String { return self._s[1204]! } - public var ChatList_HideAction: String { return self._s[1205]! } - public var AccessDenied_Title: String { return self._s[1206]! } - public var DialogList_SearchLabel: String { return self._s[1207]! } - public var Group_Setup_HistoryHidden: String { return self._s[1208]! } - public var TwoStepAuth_PasswordChangeSuccess: String { return self._s[1209]! } - public var State_Updating: String { return self._s[1211]! } - public var Contacts_TabTitle: String { return self._s[1212]! } - public var Notifications_Badge_CountUnreadMessages: String { return self._s[1214]! } - public var GroupInfo_GroupHistory: String { return self._s[1215]! } - public var Conversation_UnsupportedMediaPlaceholder: String { return self._s[1216]! } - public var Wallpaper_SetColor: String { return self._s[1217]! } - public var CheckoutInfo_ShippingInfoCountry: String { return self._s[1218]! } - public var SettingsSearch_Synonyms_SavedMessages: String { return self._s[1219]! } - public var Chat_AttachmentLimitReached: String { return self._s[1220]! } - public var Passport_Identity_OneOfTypeDriversLicense: String { return self._s[1221]! } - public var Contacts_NotRegisteredSection: String { return self._s[1222]! } + public var VoiceOver_Navigation_ProxySettings: String { return self._s[1242]! } + public var Call_CallInProgressTitle: String { return self._s[1243]! } + public var Month_ShortSeptember: String { return self._s[1244]! } + public var Watch_ChannelInfo_Title: String { return self._s[1245]! } + public var ChatList_DeleteSavedMessagesConfirmation: String { return self._s[1248]! } + public var DialogList_PasscodeLockHelp: String { return self._s[1249]! } + public var Chat_MultipleTextMessagesDisabled: String { return self._s[1250]! } + public var Wallet_Receive_Title: String { return self._s[1251]! } + public var Notifications_Badge_IncludePublicGroups: String { return self._s[1252]! } + public var Channel_AdminLogFilter_EventsTitle: String { return self._s[1253]! } + public var PhotoEditor_CropReset: String { return self._s[1254]! } + public var Group_Username_CreatePrivateLinkHelp: String { return self._s[1256]! } + public var Channel_Management_LabelEditor: String { return self._s[1257]! } + public var Passport_Identity_LatinNameHelp: String { return self._s[1259]! } + public var PhotoEditor_HighlightsTool: String { return self._s[1260]! } + public var Wallet_Info_WalletCreated: String { return self._s[1261]! } + public var UserInfo_Title: String { return self._s[1262]! } + public var ChatList_HideAction: String { return self._s[1263]! } + public var AccessDenied_Title: String { return self._s[1264]! } + public var DialogList_SearchLabel: String { return self._s[1265]! } + public var Group_Setup_HistoryHidden: String { return self._s[1266]! } + public var TwoStepAuth_PasswordChangeSuccess: String { return self._s[1267]! } + public var State_Updating: String { return self._s[1269]! } + public var Contacts_TabTitle: String { return self._s[1270]! } + public var Notifications_Badge_CountUnreadMessages: String { return self._s[1272]! } + public var GroupInfo_GroupHistory: String { return self._s[1273]! } + public var Conversation_UnsupportedMediaPlaceholder: String { return self._s[1274]! } + public var Wallpaper_SetColor: String { return self._s[1275]! } + public var CheckoutInfo_ShippingInfoCountry: String { return self._s[1276]! } + public var SettingsSearch_Synonyms_SavedMessages: String { return self._s[1277]! } + public var Chat_AttachmentLimitReached: String { return self._s[1278]! } + public var Passport_Identity_OneOfTypeDriversLicense: String { return self._s[1279]! } + public var Contacts_NotRegisteredSection: String { return self._s[1280]! } public func Time_PreciseDate_m4(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1223]!, self._r[1223]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[1281]!, self._r[1281]!, [_1, _2, _3]) } - public var Paint_Clear: String { return self._s[1224]! } - public var StickerPacksSettings_ArchivedMasks: String { return self._s[1225]! } - public var SocksProxySetup_Connecting: String { return self._s[1226]! } - public var ExplicitContent_AlertChannel: String { return self._s[1227]! } - public var CreatePoll_AllOptionsAdded: String { return self._s[1228]! } - public var Conversation_Contact: String { return self._s[1229]! } - public var Login_CodeExpired: String { return self._s[1230]! } - public var Passport_DiscardMessageAction: String { return self._s[1231]! } - public var ChatList_Context_Unpin: String { return self._s[1232]! } - public var Channel_AdminLog_MessagePreviousDescription: String { return self._s[1233]! } + public var Paint_Clear: String { return self._s[1282]! } + public var StickerPacksSettings_ArchivedMasks: String { return self._s[1283]! } + public var SocksProxySetup_Connecting: String { return self._s[1284]! } + public var ExplicitContent_AlertChannel: String { return self._s[1285]! } + public var CreatePoll_AllOptionsAdded: String { return self._s[1286]! } + public var Conversation_Contact: String { return self._s[1287]! } + public var Login_CodeExpired: String { return self._s[1288]! } + public var Passport_DiscardMessageAction: String { return self._s[1289]! } + public var ChatList_Context_Unpin: String { return self._s[1290]! } + public var Channel_AdminLog_MessagePreviousDescription: String { return self._s[1291]! } public func VoiceOver_Chat_MusicFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1234]!, self._r[1234]!, [_0]) + return formatWithArgumentRanges(self._s[1292]!, self._r[1292]!, [_0]) } - public var Channel_AdminLog_EmptyMessageText: String { return self._s[1235]! } - public var SettingsSearch_Synonyms_Data_NetworkUsage: String { return self._s[1236]! } + public var Channel_AdminLog_EmptyMessageText: String { return self._s[1293]! } + public var SettingsSearch_Synonyms_Data_NetworkUsage: String { return self._s[1294]! } public func Group_EditAdmin_RankInfo(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1237]!, self._r[1237]!, [_0]) + return formatWithArgumentRanges(self._s[1295]!, self._r[1295]!, [_0]) } - public var Month_ShortApril: String { return self._s[1238]! } - public var AuthSessions_CurrentSession: String { return self._s[1239]! } - public var Chat_AttachmentMultipleFilesDisabled: String { return self._s[1242]! } - public var Wallet_Navigation_Cancel: String { return self._s[1244]! } - public var WallpaperPreview_CropTopText: String { return self._s[1245]! } - public var PrivacySettings_DeleteAccountIfAwayFor: String { return self._s[1246]! } - public var CheckoutInfo_ShippingInfoTitle: String { return self._s[1247]! } + public var Month_ShortApril: String { return self._s[1296]! } + public var AuthSessions_CurrentSession: String { return self._s[1297]! } + public var Chat_AttachmentMultipleFilesDisabled: String { return self._s[1300]! } + public var Wallet_Navigation_Cancel: String { return self._s[1302]! } + public var WallpaperPreview_CropTopText: String { return self._s[1303]! } + public var PrivacySettings_DeleteAccountIfAwayFor: String { return self._s[1304]! } + public var CheckoutInfo_ShippingInfoTitle: String { return self._s[1305]! } public func Conversation_ScheduleMessage_SendOn(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1248]!, self._r[1248]!, [_0, _1]) + return formatWithArgumentRanges(self._s[1306]!, self._r[1306]!, [_0, _1]) } - public var Appearance_ThemePreview_Chat_2_Text: String { return self._s[1249]! } - public var Channel_Setup_TypePrivate: String { return self._s[1251]! } - public var Forward_ChannelReadOnly: String { return self._s[1254]! } - public var PhotoEditor_CurvesBlue: String { return self._s[1255]! } - public var AddContact_SharedContactException: String { return self._s[1256]! } - public var UserInfo_BotPrivacy: String { return self._s[1258]! } - public var Wallet_CreateInvoice_Title: String { return self._s[1259]! } - public var Notification_PassportValueEmail: String { return self._s[1260]! } - public var EmptyGroupInfo_Subtitle: String { return self._s[1261]! } - public var GroupPermission_NewTitle: String { return self._s[1262]! } - public var CallFeedback_ReasonDropped: String { return self._s[1263]! } - public var GroupInfo_Permissions_AddException: String { return self._s[1264]! } - public var Channel_SignMessages_Help: String { return self._s[1266]! } - public var Undo_ChatDeleted: String { return self._s[1268]! } - public var Conversation_ChatBackground: String { return self._s[1269]! } + public var Appearance_ThemePreview_Chat_2_Text: String { return self._s[1307]! } + public var Channel_Setup_TypePrivate: String { return self._s[1309]! } + public var Forward_ChannelReadOnly: String { return self._s[1312]! } + public var PhotoEditor_CurvesBlue: String { return self._s[1313]! } + public var AddContact_SharedContactException: String { return self._s[1314]! } + public var UserInfo_BotPrivacy: String { return self._s[1316]! } + public var Wallet_CreateInvoice_Title: String { return self._s[1317]! } + public var Notification_PassportValueEmail: String { return self._s[1318]! } + public var EmptyGroupInfo_Subtitle: String { return self._s[1319]! } + public var GroupPermission_NewTitle: String { return self._s[1320]! } + public var CallFeedback_ReasonDropped: String { return self._s[1321]! } + public var GroupInfo_Permissions_AddException: String { return self._s[1322]! } + public var Channel_SignMessages_Help: String { return self._s[1324]! } + public var Undo_ChatDeleted: String { return self._s[1326]! } + public var Conversation_ChatBackground: String { return self._s[1327]! } public func Wallet_WordCheck_Text(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1270]!, self._r[1270]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[1328]!, self._r[1328]!, [_1, _2, _3]) } - public var ChannelMembers_WhoCanAddMembers_Admins: String { return self._s[1271]! } - public var FastTwoStepSetup_EmailPlaceholder: String { return self._s[1272]! } - public var Passport_Language_pt: String { return self._s[1273]! } - public var VoiceOver_Chat_YourVoiceMessage: String { return self._s[1274]! } - public var NotificationsSound_Popcorn: String { return self._s[1277]! } - public var AutoNightTheme_Disabled: String { return self._s[1278]! } - public var BlockedUsers_LeavePrefix: String { return self._s[1279]! } - public var WallpaperPreview_CustomColorTopText: String { return self._s[1280]! } - public var Contacts_PermissionsSuppressWarningText: String { return self._s[1281]! } - public var WallpaperSearch_ColorBlue: String { return self._s[1282]! } + public func PUSH_CHAT_MESSAGE_QUIZ(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1329]!, self._r[1329]!, [_1, _2, _3]) + } + public var ChannelMembers_WhoCanAddMembers_Admins: String { return self._s[1330]! } + public var FastTwoStepSetup_EmailPlaceholder: String { return self._s[1331]! } + public var Passport_Language_pt: String { return self._s[1332]! } + public var VoiceOver_Chat_YourVoiceMessage: String { return self._s[1333]! } + public var NotificationsSound_Popcorn: String { return self._s[1336]! } + public var AutoNightTheme_Disabled: String { return self._s[1337]! } + public var BlockedUsers_LeavePrefix: String { return self._s[1338]! } + public var WallpaperPreview_CustomColorTopText: String { return self._s[1339]! } + public var Contacts_PermissionsSuppressWarningText: String { return self._s[1340]! } + public var WallpaperSearch_ColorBlue: String { return self._s[1341]! } public func CancelResetAccount_TextSMS(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1283]!, self._r[1283]!, [_0]) + return formatWithArgumentRanges(self._s[1342]!, self._r[1342]!, [_0]) } - public var CheckoutInfo_ErrorNameInvalid: String { return self._s[1284]! } - public var SocksProxySetup_UseForCalls: String { return self._s[1285]! } - public var Passport_DeleteDocumentConfirmation: String { return self._s[1287]! } + public var CheckoutInfo_ErrorNameInvalid: String { return self._s[1343]! } + public var SocksProxySetup_UseForCalls: String { return self._s[1344]! } + public var Passport_DeleteDocumentConfirmation: String { return self._s[1346]! } + public var PeerInfo_PaneGroups: String { return self._s[1347]! } public func Conversation_Megabytes(_ _0: Float) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1288]!, self._r[1288]!, ["\(_0)"]) + return formatWithArgumentRanges(self._s[1348]!, self._r[1348]!, ["\(_0)"]) } - public var SocksProxySetup_Hostname: String { return self._s[1291]! } - public var ChatSettings_AutoDownloadSettings_OffForAll: String { return self._s[1292]! } - public var Compose_NewEncryptedChat: String { return self._s[1293]! } - public var Login_CodeFloodError: String { return self._s[1294]! } - public var Calls_TabTitle: String { return self._s[1295]! } - public var Privacy_ProfilePhoto: String { return self._s[1296]! } - public var Passport_Language_he: String { return self._s[1297]! } + public var SocksProxySetup_Hostname: String { return self._s[1351]! } + public var ChatSettings_AutoDownloadSettings_OffForAll: String { return self._s[1352]! } + public var Compose_NewEncryptedChat: String { return self._s[1353]! } + public var Login_CodeFloodError: String { return self._s[1354]! } + public var Calls_TabTitle: String { return self._s[1355]! } + public var Privacy_ProfilePhoto: String { return self._s[1356]! } + public var Passport_Language_he: String { return self._s[1357]! } public func Conversation_SetReminder_RemindToday(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1298]!, self._r[1298]!, [_0]) + return formatWithArgumentRanges(self._s[1358]!, self._r[1358]!, [_0]) } - public var GroupPermission_Title: String { return self._s[1299]! } + public var GroupPermission_Title: String { return self._s[1359]! } public func Channel_AdminLog_MessageGroupPreHistoryHidden(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1300]!, self._r[1300]!, [_0]) + return formatWithArgumentRanges(self._s[1360]!, self._r[1360]!, [_0]) } - public var Wallet_TransactionInfo_SenderHeader: String { return self._s[1301]! } - public var GroupPermission_NoChangeInfo: String { return self._s[1302]! } - public var ChatList_DeleteForCurrentUser: String { return self._s[1303]! } - public var Tour_Text1: String { return self._s[1304]! } - public var Channel_EditAdmin_TransferOwnership: String { return self._s[1305]! } - public var Month_ShortFebruary: String { return self._s[1306]! } - public var TwoStepAuth_EmailSkip: String { return self._s[1307]! } + public var Wallet_TransactionInfo_SenderHeader: String { return self._s[1361]! } + public var GroupPermission_NoChangeInfo: String { return self._s[1362]! } + public var ChatList_DeleteForCurrentUser: String { return self._s[1363]! } + public var Tour_Text1: String { return self._s[1364]! } + public var Channel_EditAdmin_TransferOwnership: String { return self._s[1365]! } + public var Month_ShortFebruary: String { return self._s[1366]! } + public var TwoStepAuth_EmailSkip: String { return self._s[1367]! } public func Wallet_Time_PreciseDate_m4(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1308]!, self._r[1308]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[1368]!, self._r[1368]!, [_1, _2, _3]) } - public var NotificationsSound_Glass: String { return self._s[1309]! } - public var Appearance_ThemeNightBlue: String { return self._s[1310]! } - public var CheckoutInfo_Pay: String { return self._s[1311]! } - public var Invite_LargeRecipientsCountWarning: String { return self._s[1313]! } - public var Call_CallAgain: String { return self._s[1315]! } - public var AttachmentMenu_SendAsFile: String { return self._s[1316]! } - public var AccessDenied_MicrophoneRestricted: String { return self._s[1317]! } - public var Passport_InvalidPasswordError: String { return self._s[1318]! } - public var Watch_Message_Game: String { return self._s[1319]! } - public var Stickers_Install: String { return self._s[1320]! } - public var VoiceOver_Chat_Message: String { return self._s[1321]! } - public var PrivacyLastSeenSettings_NeverShareWith: String { return self._s[1322]! } - public var Passport_Identity_ResidenceCountry: String { return self._s[1324]! } - public var Notifications_GroupNotificationsHelp: String { return self._s[1325]! } - public var AuthSessions_OtherSessions: String { return self._s[1326]! } - public var Channel_Username_Help: String { return self._s[1327]! } - public var Camera_Title: String { return self._s[1328]! } - public var GroupInfo_SetGroupPhotoDelete: String { return self._s[1330]! } - public var Privacy_ProfilePhoto_NeverShareWith_Title: String { return self._s[1331]! } - public var Channel_AdminLog_SendPolls: String { return self._s[1332]! } - public var Channel_AdminLog_TitleAllEvents: String { return self._s[1333]! } - public var Channel_EditAdmin_PermissionInviteMembers: String { return self._s[1334]! } - public var Contacts_MemberSearchSectionTitleGroup: String { return self._s[1335]! } - public var ScheduledMessages_DeleteMany: String { return self._s[1336]! } - public var Conversation_RestrictedStickers: String { return self._s[1337]! } - public var Notifications_ExceptionsResetToDefaults: String { return self._s[1339]! } - public var UserInfo_TelegramCall: String { return self._s[1341]! } - public var TwoStepAuth_SetupResendEmailCode: String { return self._s[1342]! } - public var CreatePoll_OptionsHeader: String { return self._s[1343]! } - public var SettingsSearch_Synonyms_Data_CallsUseLessData: String { return self._s[1344]! } - public var ArchivedChats_IntroTitle1: String { return self._s[1345]! } - public var Privacy_GroupsAndChannels_AlwaysAllow_Title: String { return self._s[1346]! } - public var Passport_Identity_EditPersonalDetails: String { return self._s[1347]! } + public var NotificationsSound_Glass: String { return self._s[1369]! } + public var Appearance_ThemeNightBlue: String { return self._s[1370]! } + public var CheckoutInfo_Pay: String { return self._s[1371]! } + public var PeerInfo_ButtonLeave: String { return self._s[1373]! } + public var Invite_LargeRecipientsCountWarning: String { return self._s[1374]! } + public var Call_CallAgain: String { return self._s[1376]! } + public var AttachmentMenu_SendAsFile: String { return self._s[1377]! } + public var AccessDenied_MicrophoneRestricted: String { return self._s[1378]! } + public var Passport_InvalidPasswordError: String { return self._s[1379]! } + public var Watch_Message_Game: String { return self._s[1380]! } + public var Stickers_Install: String { return self._s[1381]! } + public var VoiceOver_Chat_Message: String { return self._s[1382]! } + public var PrivacyLastSeenSettings_NeverShareWith: String { return self._s[1383]! } + public var Passport_Identity_ResidenceCountry: String { return self._s[1385]! } + public var Notifications_GroupNotificationsHelp: String { return self._s[1386]! } + public var AuthSessions_OtherSessions: String { return self._s[1387]! } + public var Channel_Username_Help: String { return self._s[1388]! } + public var Camera_Title: String { return self._s[1389]! } + public var IntentsSettings_Title: String { return self._s[1390]! } + public var GroupInfo_SetGroupPhotoDelete: String { return self._s[1392]! } + public var Privacy_ProfilePhoto_NeverShareWith_Title: String { return self._s[1393]! } + public var Channel_AdminLog_SendPolls: String { return self._s[1394]! } + public var Channel_AdminLog_TitleAllEvents: String { return self._s[1395]! } + public var Channel_EditAdmin_PermissionInviteMembers: String { return self._s[1396]! } + public var Contacts_MemberSearchSectionTitleGroup: String { return self._s[1397]! } + public var ScheduledMessages_DeleteMany: String { return self._s[1398]! } + public var Conversation_RestrictedStickers: String { return self._s[1399]! } + public var Notifications_ExceptionsResetToDefaults: String { return self._s[1401]! } + public var UserInfo_TelegramCall: String { return self._s[1403]! } + public var TwoStepAuth_SetupResendEmailCode: String { return self._s[1404]! } + public var CreatePoll_OptionsHeader: String { return self._s[1405]! } + public var SettingsSearch_Synonyms_Data_CallsUseLessData: String { return self._s[1406]! } + public var ArchivedChats_IntroTitle1: String { return self._s[1407]! } + public var Privacy_GroupsAndChannels_AlwaysAllow_Title: String { return self._s[1408]! } + public var Theme_Colors_Proceed: String { return self._s[1409]! } + public var Passport_Identity_EditPersonalDetails: String { return self._s[1410]! } public func Time_PreciseDate_m1(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1348]!, self._r[1348]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[1411]!, self._r[1411]!, [_1, _2, _3]) } - public var Wallet_Month_GenAugust: String { return self._s[1349]! } - public var Settings_SaveEditedPhotos: String { return self._s[1350]! } - public var TwoStepAuth_ConfirmationTitle: String { return self._s[1351]! } - public var Privacy_GroupsAndChannels_NeverAllow_Title: String { return self._s[1352]! } - public var Conversation_MessageDialogRetry: String { return self._s[1353]! } - public var ChatList_Context_MarkAsUnread: String { return self._s[1354]! } - public var Conversation_DiscardVoiceMessageAction: String { return self._s[1355]! } - public var Permissions_PeopleNearbyTitle_v0: String { return self._s[1356]! } - public var Group_Setup_TypeHeader: String { return self._s[1357]! } - public var Paint_RecentStickers: String { return self._s[1358]! } - public var PhotoEditor_GrainTool: String { return self._s[1359]! } - public var CheckoutInfo_ShippingInfoState: String { return self._s[1360]! } - public var EmptyGroupInfo_Line4: String { return self._s[1361]! } - public var Watch_AuthRequired: String { return self._s[1363]! } + public var Wallet_Month_GenAugust: String { return self._s[1412]! } + public var Settings_SaveEditedPhotos: String { return self._s[1413]! } + public var TwoStepAuth_ConfirmationTitle: String { return self._s[1414]! } + public var Privacy_GroupsAndChannels_NeverAllow_Title: String { return self._s[1415]! } + public var Conversation_MessageDialogRetry: String { return self._s[1416]! } + public var ChatList_Context_MarkAsUnread: String { return self._s[1417]! } + public var MessagePoll_SubmitVote: String { return self._s[1418]! } + public var Conversation_DiscardVoiceMessageAction: String { return self._s[1419]! } + public var Permissions_PeopleNearbyTitle_v0: String { return self._s[1420]! } + public var Group_Setup_TypeHeader: String { return self._s[1421]! } + public var Paint_RecentStickers: String { return self._s[1422]! } + public var PhotoEditor_GrainTool: String { return self._s[1423]! } + public var CheckoutInfo_ShippingInfoState: String { return self._s[1424]! } + public var EmptyGroupInfo_Line4: String { return self._s[1425]! } + public var Watch_AuthRequired: String { return self._s[1427]! } public func Passport_Email_UseTelegramEmail(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1364]!, self._r[1364]!, [_0]) + return formatWithArgumentRanges(self._s[1428]!, self._r[1428]!, [_0]) } - public var Conversation_EncryptedDescriptionTitle: String { return self._s[1365]! } - public var ChannelIntro_Text: String { return self._s[1366]! } - public var DialogList_DeleteBotConfirmation: String { return self._s[1367]! } - public var GroupPermission_NoSendMedia: String { return self._s[1368]! } - public var Calls_AddTab: String { return self._s[1369]! } - public var Message_ReplyActionButtonShowReceipt: String { return self._s[1370]! } - public var Channel_AdminLog_EmptyFilterText: String { return self._s[1371]! } - public var Conversation_WalletRequiredSetup: String { return self._s[1372]! } - public var Notification_MessageLifetime1d: String { return self._s[1373]! } - public var Notifications_ChannelNotificationsExceptionsHelp: String { return self._s[1374]! } - public var Channel_BanUser_PermissionsHeader: String { return self._s[1375]! } - public var Passport_Identity_GenderFemale: String { return self._s[1376]! } - public var BlockedUsers_BlockTitle: String { return self._s[1377]! } + public var Conversation_EncryptedDescriptionTitle: String { return self._s[1429]! } + public var ChannelIntro_Text: String { return self._s[1430]! } + public var DialogList_DeleteBotConfirmation: String { return self._s[1431]! } + public var GroupPermission_NoSendMedia: String { return self._s[1432]! } + public var Calls_AddTab: String { return self._s[1433]! } + public var Message_ReplyActionButtonShowReceipt: String { return self._s[1434]! } + public var Channel_AdminLog_EmptyFilterText: String { return self._s[1435]! } + public var Conversation_WalletRequiredSetup: String { return self._s[1436]! } + public var Notification_MessageLifetime1d: String { return self._s[1437]! } + public var Notifications_ChannelNotificationsExceptionsHelp: String { return self._s[1438]! } + public var Channel_BanUser_PermissionsHeader: String { return self._s[1439]! } + public var Passport_Identity_GenderFemale: String { return self._s[1440]! } + public var BlockedUsers_BlockTitle: String { return self._s[1441]! } public func PUSH_CHANNEL_MESSAGE_GIF(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1378]!, self._r[1378]!, [_1]) + return formatWithArgumentRanges(self._s[1442]!, self._r[1442]!, [_1]) } - public var Weekday_Yesterday: String { return self._s[1379]! } - public var WallpaperSearch_ColorBlack: String { return self._s[1380]! } - public var Settings_Context_Logout: String { return self._s[1381]! } - public var Wallet_Info_UnknownTransaction: String { return self._s[1382]! } - public var ChatList_ArchiveAction: String { return self._s[1383]! } - public var AutoNightTheme_Scheduled: String { return self._s[1384]! } - public var TwoFactorSetup_Email_SkipAction: String { return self._s[1385]! } + public var Weekday_Yesterday: String { return self._s[1443]! } + public var WallpaperSearch_ColorBlack: String { return self._s[1444]! } + public var Settings_Context_Logout: String { return self._s[1445]! } + public var Wallet_Info_UnknownTransaction: String { return self._s[1446]! } + public var ChatList_ArchiveAction: String { return self._s[1447]! } + public var AutoNightTheme_Scheduled: String { return self._s[1448]! } + public var TwoFactorSetup_Email_SkipAction: String { return self._s[1449]! } + public var Settings_Devices: String { return self._s[1450]! } + public var ContactInfo_Note: String { return self._s[1451]! } public func Login_PhoneGenericEmailBody(_ _1: String, _ _2: String, _ _3: String, _ _4: String, _ _5: String, _ _6: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1386]!, self._r[1386]!, [_1, _2, _3, _4, _5, _6]) + return formatWithArgumentRanges(self._s[1452]!, self._r[1452]!, [_1, _2, _3, _4, _5, _6]) } - public var EditTheme_ThemeTemplateAlertTitle: String { return self._s[1387]! } - public var Wallet_Receive_CreateInvoice: String { return self._s[1388]! } - public var PrivacyPolicy_DeclineDeleteNow: String { return self._s[1389]! } + public var EditTheme_ThemeTemplateAlertTitle: String { return self._s[1453]! } + public var Wallet_Receive_CreateInvoice: String { return self._s[1454]! } + public var PrivacyPolicy_DeclineDeleteNow: String { return self._s[1455]! } + public var Theme_Colors_ColorWallpaperWarningProceed: String { return self._s[1456]! } public func PUSH_CHAT_JOINED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1390]!, self._r[1390]!, [_1, _2]) + return formatWithArgumentRanges(self._s[1457]!, self._r[1457]!, [_1, _2]) } - public var CreatePoll_Create: String { return self._s[1391]! } - public var Channel_Members_AddBannedErrorAdmin: String { return self._s[1392]! } + public var CreatePoll_Create: String { return self._s[1458]! } + public var Channel_Members_AddBannedErrorAdmin: String { return self._s[1459]! } public func Notification_CallFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1393]!, self._r[1393]!, [_1, _2]) + return formatWithArgumentRanges(self._s[1460]!, self._r[1460]!, [_1, _2]) } - public var ScheduledMessages_ClearAllConfirmation: String { return self._s[1394]! } - public var Checkout_ErrorProviderAccountInvalid: String { return self._s[1395]! } - public var Notifications_InAppNotificationsSounds: String { return self._s[1397]! } + public var ScheduledMessages_ClearAllConfirmation: String { return self._s[1461]! } + public var Checkout_ErrorProviderAccountInvalid: String { return self._s[1462]! } + public var Notifications_InAppNotificationsSounds: String { return self._s[1464]! } public func PUSH_PINNED_GAME_SCORE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1398]!, self._r[1398]!, [_1]) + return formatWithArgumentRanges(self._s[1465]!, self._r[1465]!, [_1]) } - public var Preview_OpenInInstagram: String { return self._s[1399]! } - public var Notification_MessageLifetimeRemovedOutgoing: String { return self._s[1400]! } + public var Preview_OpenInInstagram: String { return self._s[1466]! } + public var Notification_MessageLifetimeRemovedOutgoing: String { return self._s[1467]! } public func PUSH_CHAT_ADD_MEMBER(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1401]!, self._r[1401]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[1468]!, self._r[1468]!, [_1, _2, _3]) } public func Passport_PrivacyPolicy(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1402]!, self._r[1402]!, [_1, _2]) + return formatWithArgumentRanges(self._s[1469]!, self._r[1469]!, [_1, _2]) } - public var Channel_AdminLog_InfoPanelAlertTitle: String { return self._s[1403]! } - public var ArchivedChats_IntroText3: String { return self._s[1404]! } - public var ChatList_UndoArchiveHiddenText: String { return self._s[1405]! } - public var NetworkUsageSettings_TotalSection: String { return self._s[1406]! } - public var Wallet_Month_GenSeptember: String { return self._s[1407]! } - public var Channel_Setup_TypePrivateHelp: String { return self._s[1408]! } + public var Channel_AdminLog_InfoPanelAlertTitle: String { return self._s[1470]! } + public var ArchivedChats_IntroText3: String { return self._s[1471]! } + public var ChatList_UndoArchiveHiddenText: String { return self._s[1472]! } + public var NetworkUsageSettings_TotalSection: String { return self._s[1473]! } + public var Wallet_Month_GenSeptember: String { return self._s[1474]! } + public var Channel_Setup_TypePrivateHelp: String { return self._s[1475]! } public func PUSH_CHAT_MESSAGE_POLL(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1409]!, self._r[1409]!, [_1, _2, _3]) - } - public var Privacy_GroupsAndChannels_NeverAllow_Placeholder: String { return self._s[1411]! } - public var FastTwoStepSetup_HintSection: String { return self._s[1412]! } - public var Wallpaper_PhotoLibrary: String { return self._s[1413]! } - public var TwoStepAuth_SetupResendEmailCodeAlert: String { return self._s[1414]! } - public var Gif_NoGifsFound: String { return self._s[1415]! } - public var Watch_LastSeen_WithinAMonth: String { return self._s[1416]! } - public var VoiceOver_MessageContextDelete: String { return self._s[1417]! } - public var EditTheme_Preview: String { return self._s[1418]! } - public func ClearCache_StorageTitle(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1419]!, self._r[1419]!, [_0]) - } - public var GroupInfo_ActionPromote: String { return self._s[1420]! } - public var PasscodeSettings_SimplePasscode: String { return self._s[1421]! } - public var GroupInfo_Permissions_Title: String { return self._s[1422]! } - public var Permissions_ContactsText_v0: String { return self._s[1423]! } - public var PrivacyPhoneNumberSettings_CustomDisabledHelp: String { return self._s[1424]! } - public var SettingsSearch_Synonyms_Notifications_BadgeIncludeMutedPublicGroups: String { return self._s[1425]! } - public var PrivacySettings_DataSettingsHelp: String { return self._s[1428]! } - public var Passport_FieldEmailHelp: String { return self._s[1429]! } - public func Activity_RemindAboutUser(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1430]!, self._r[1430]!, [_0]) - } - public var Passport_Identity_GenderPlaceholder: String { return self._s[1431]! } - public var Weekday_ShortSaturday: String { return self._s[1432]! } - public var ContactInfo_PhoneLabelMain: String { return self._s[1433]! } - public var Watch_Conversation_UserInfo: String { return self._s[1434]! } - public var CheckoutInfo_ShippingInfoCityPlaceholder: String { return self._s[1435]! } - public var GroupPermission_PermissionDisabledByDefault: String { return self._s[1436]! } - public var PrivacyLastSeenSettings_Title: String { return self._s[1437]! } - public var Conversation_ShareBotLocationConfirmation: String { return self._s[1438]! } - public var PhotoEditor_VignetteTool: String { return self._s[1439]! } - public var Passport_Address_Street1Placeholder: String { return self._s[1440]! } - public var Passport_Language_et: String { return self._s[1441]! } - public var AppUpgrade_Running: String { return self._s[1442]! } - public var Channel_DiscussionGroup_Info: String { return self._s[1444]! } - public var EditTheme_Create_Preview_IncomingReplyName: String { return self._s[1445]! } - public var Passport_Language_bg: String { return self._s[1446]! } - public var Stickers_NoStickersFound: String { return self._s[1448]! } - public func PUSH_CHANNEL_MESSAGE_TEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1450]!, self._r[1450]!, [_1, _2]) - } - public func VoiceOver_Chat_ContactFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1451]!, self._r[1451]!, [_0]) - } - public var Wallet_Month_GenJuly: String { return self._s[1452]! } - public var Wallet_Receive_AddressHeader: String { return self._s[1453]! } - public var Wallet_Send_AmountText: String { return self._s[1454]! } - public var Settings_About: String { return self._s[1455]! } - public func Channel_AdminLog_MessageRestricted(_ _0: String, _ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1456]!, self._r[1456]!, [_0, _1, _2]) - } - public var ChatList_Context_MarkAsRead: String { return self._s[1458]! } - public var KeyCommand_NewMessage: String { return self._s[1459]! } - public var Group_ErrorAddBlocked: String { return self._s[1460]! } - public func Message_PaymentSent(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1461]!, self._r[1461]!, [_0]) - } - public var Map_LocationTitle: String { return self._s[1462]! } - public var ReportGroupLocation_Title: String { return self._s[1463]! } - public var CallSettings_UseLessDataLongDescription: String { return self._s[1464]! } - public var Cache_ClearProgress: String { return self._s[1465]! } - public func Channel_Management_ErrorNotMember(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1466]!, self._r[1466]!, [_0]) - } - public var GroupRemoved_AddToGroup: String { return self._s[1467]! } - public var Passport_UpdateRequiredError: String { return self._s[1468]! } - public var Wallet_SecureStorageNotAvailable_Text: String { return self._s[1469]! } - public func PUSH_MESSAGE_DOC(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1470]!, self._r[1470]!, [_1]) - } - public var Notifications_PermissionsSuppressWarningText: String { return self._s[1472]! } - public var Passport_Identity_MainPageHelp: String { return self._s[1473]! } - public var Conversation_StatusKickedFromGroup: String { return self._s[1474]! } - public var Passport_Language_ka: String { return self._s[1475]! } - public func Wallet_Time_PreciseDate_m12(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[1476]!, self._r[1476]!, [_1, _2, _3]) } - public var Call_Decline: String { return self._s[1477]! } - public var SocksProxySetup_ProxyEnabled: String { return self._s[1478]! } - public var TwoFactorSetup_Email_SkipConfirmationText: String { return self._s[1481]! } + public var Privacy_GroupsAndChannels_NeverAllow_Placeholder: String { return self._s[1478]! } + public var FastTwoStepSetup_HintSection: String { return self._s[1479]! } + public var Wallpaper_PhotoLibrary: String { return self._s[1480]! } + public var TwoStepAuth_SetupResendEmailCodeAlert: String { return self._s[1481]! } + public var Gif_NoGifsFound: String { return self._s[1482]! } + public var Watch_LastSeen_WithinAMonth: String { return self._s[1483]! } + public var VoiceOver_MessageContextDelete: String { return self._s[1484]! } + public var EditTheme_Preview: String { return self._s[1485]! } + public func ClearCache_StorageTitle(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1486]!, self._r[1486]!, [_0]) + } + public var GroupInfo_ActionPromote: String { return self._s[1487]! } + public var PasscodeSettings_SimplePasscode: String { return self._s[1488]! } + public var GroupInfo_Permissions_Title: String { return self._s[1489]! } + public var Permissions_ContactsText_v0: String { return self._s[1490]! } + public var PrivacyPhoneNumberSettings_CustomDisabledHelp: String { return self._s[1491]! } + public var SettingsSearch_Synonyms_Notifications_BadgeIncludeMutedPublicGroups: String { return self._s[1492]! } + public var PrivacySettings_DataSettingsHelp: String { return self._s[1495]! } + public var Passport_FieldEmailHelp: String { return self._s[1496]! } + public func Activity_RemindAboutUser(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1497]!, self._r[1497]!, [_0]) + } + public var Passport_Identity_GenderPlaceholder: String { return self._s[1498]! } + public var Weekday_ShortSaturday: String { return self._s[1499]! } + public var ContactInfo_PhoneLabelMain: String { return self._s[1500]! } + public var Watch_Conversation_UserInfo: String { return self._s[1501]! } + public var CheckoutInfo_ShippingInfoCityPlaceholder: String { return self._s[1502]! } + public var GroupPermission_PermissionDisabledByDefault: String { return self._s[1503]! } + public var PrivacyLastSeenSettings_Title: String { return self._s[1504]! } + public var Conversation_ShareBotLocationConfirmation: String { return self._s[1505]! } + public var PhotoEditor_VignetteTool: String { return self._s[1506]! } + public var Passport_Address_Street1Placeholder: String { return self._s[1507]! } + public var Passport_Language_et: String { return self._s[1508]! } + public var AppUpgrade_Running: String { return self._s[1509]! } + public var Channel_DiscussionGroup_Info: String { return self._s[1511]! } + public var EditTheme_Create_Preview_IncomingReplyName: String { return self._s[1512]! } + public var Passport_Language_bg: String { return self._s[1513]! } + public var Stickers_NoStickersFound: String { return self._s[1515]! } + public func PUSH_CHANNEL_MESSAGE_TEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1517]!, self._r[1517]!, [_1, _2]) + } + public func VoiceOver_Chat_ContactFrom(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1518]!, self._r[1518]!, [_0]) + } + public var Wallet_Month_GenJuly: String { return self._s[1519]! } + public var Wallet_Receive_AddressHeader: String { return self._s[1520]! } + public var Wallet_Send_AmountText: String { return self._s[1521]! } + public var Settings_About: String { return self._s[1522]! } + public func Channel_AdminLog_MessageRestricted(_ _0: String, _ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1523]!, self._r[1523]!, [_0, _1, _2]) + } + public var ChatList_Context_MarkAsRead: String { return self._s[1525]! } + public var KeyCommand_NewMessage: String { return self._s[1526]! } + public var Group_ErrorAddBlocked: String { return self._s[1527]! } + public func Message_PaymentSent(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1528]!, self._r[1528]!, [_0]) + } + public var Map_LocationTitle: String { return self._s[1529]! } + public var ReportGroupLocation_Title: String { return self._s[1530]! } + public var CallSettings_UseLessDataLongDescription: String { return self._s[1531]! } + public var Cache_ClearProgress: String { return self._s[1532]! } + public func Channel_Management_ErrorNotMember(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1533]!, self._r[1533]!, [_0]) + } + public var GroupRemoved_AddToGroup: String { return self._s[1534]! } + public var Passport_UpdateRequiredError: String { return self._s[1535]! } + public var Wallet_SecureStorageNotAvailable_Text: String { return self._s[1536]! } + public func PUSH_MESSAGE_DOC(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1537]!, self._r[1537]!, [_1]) + } + public var Notifications_PermissionsSuppressWarningText: String { return self._s[1539]! } + public var Passport_Identity_MainPageHelp: String { return self._s[1540]! } + public var PeerInfo_ButtonSearch: String { return self._s[1541]! } + public var Conversation_StatusKickedFromGroup: String { return self._s[1542]! } + public var Passport_Language_ka: String { return self._s[1543]! } + public func Wallet_Time_PreciseDate_m12(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1544]!, self._r[1544]!, [_1, _2, _3]) + } + public var Call_Decline: String { return self._s[1545]! } + public var SocksProxySetup_ProxyEnabled: String { return self._s[1546]! } + public var TwoFactorSetup_Email_SkipConfirmationText: String { return self._s[1549]! } public func AuthCode_Alert(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1482]!, self._r[1482]!, [_0]) + return formatWithArgumentRanges(self._s[1550]!, self._r[1550]!, [_0]) } - public var CallFeedback_Send: String { return self._s[1483]! } - public var EditTheme_EditTitle: String { return self._s[1484]! } + public var CallFeedback_Send: String { return self._s[1551]! } + public var EditTheme_EditTitle: String { return self._s[1552]! } public func Channel_AdminLog_MessagePromotedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1485]!, self._r[1485]!, [_1, _2]) + return formatWithArgumentRanges(self._s[1553]!, self._r[1553]!, [_1, _2]) } - public var Passport_Phone_UseTelegramNumberHelp: String { return self._s[1486]! } + public var Passport_Phone_UseTelegramNumberHelp: String { return self._s[1554]! } public func Wallet_Updated_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1488]!, self._r[1488]!, [_0]) + return formatWithArgumentRanges(self._s[1556]!, self._r[1556]!, [_0]) } - public var SettingsSearch_Synonyms_Data_Title: String { return self._s[1489]! } - public var Passport_DeletePassport: String { return self._s[1490]! } - public var Appearance_AppIconFilled: String { return self._s[1491]! } - public var Privacy_Calls_P2PAlways: String { return self._s[1492]! } - public var Month_ShortDecember: String { return self._s[1493]! } - public var Channel_AdminLog_CanEditMessages: String { return self._s[1495]! } + public var SettingsSearch_Synonyms_Data_Title: String { return self._s[1557]! } + public var Passport_DeletePassport: String { return self._s[1558]! } + public var Appearance_AppIconFilled: String { return self._s[1559]! } + public var Privacy_Calls_P2PAlways: String { return self._s[1560]! } + public var Month_ShortDecember: String { return self._s[1561]! } + public var Channel_AdminLog_CanEditMessages: String { return self._s[1563]! } public func Contacts_AccessDeniedHelpLandscape(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1496]!, self._r[1496]!, [_0]) + return formatWithArgumentRanges(self._s[1564]!, self._r[1564]!, [_0]) } - public var Channel_Stickers_Searching: String { return self._s[1497]! } - public var Conversation_EncryptedDescription1: String { return self._s[1498]! } - public var Conversation_EncryptedDescription2: String { return self._s[1499]! } - public var PasscodeSettings_PasscodeOptions: String { return self._s[1500]! } - public var Conversation_EncryptedDescription3: String { return self._s[1502]! } - public var PhotoEditor_SharpenTool: String { return self._s[1503]! } - public var Wallet_Configuration_Title: String { return self._s[1504]! } + public var Channel_Stickers_Searching: String { return self._s[1565]! } + public var Conversation_EncryptedDescription1: String { return self._s[1566]! } + public var Conversation_EncryptedDescription2: String { return self._s[1567]! } + public var PasscodeSettings_PasscodeOptions: String { return self._s[1568]! } + public var Conversation_EncryptedDescription3: String { return self._s[1570]! } + public var PhotoEditor_SharpenTool: String { return self._s[1571]! } + public var Wallet_Configuration_Title: String { return self._s[1572]! } public func Conversation_AddNameToContacts(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1505]!, self._r[1505]!, [_0]) + return formatWithArgumentRanges(self._s[1573]!, self._r[1573]!, [_0]) } - public var Conversation_EncryptedDescription4: String { return self._s[1507]! } - public var Channel_Members_AddMembers: String { return self._s[1508]! } - public var Wallpaper_Search: String { return self._s[1509]! } - public var Weekday_Friday: String { return self._s[1510]! } - public var Privacy_ContactsSync: String { return self._s[1511]! } - public var SettingsSearch_Synonyms_Privacy_Data_ContactsReset: String { return self._s[1512]! } - public var ApplyLanguage_ChangeLanguageAction: String { return self._s[1513]! } + public var Conversation_EncryptedDescription4: String { return self._s[1575]! } + public var Channel_Members_AddMembers: String { return self._s[1576]! } + public var Wallpaper_Search: String { return self._s[1577]! } + public var Weekday_Friday: String { return self._s[1579]! } + public var Privacy_ContactsSync: String { return self._s[1580]! } + public var SettingsSearch_Synonyms_Privacy_Data_ContactsReset: String { return self._s[1581]! } + public var ApplyLanguage_ChangeLanguageAction: String { return self._s[1582]! } public func Channel_Management_RestrictedBy(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1514]!, self._r[1514]!, [_0]) + return formatWithArgumentRanges(self._s[1583]!, self._r[1583]!, [_0]) } - public var Wallet_Configuration_BlockchainIdHeader: String { return self._s[1515]! } - public var GroupInfo_Permissions_Removed: String { return self._s[1516]! } - public var ScheduledMessages_ScheduledOnline: String { return self._s[1517]! } - public var Passport_Identity_GenderMale: String { return self._s[1518]! } + public var Wallet_Configuration_BlockchainIdHeader: String { return self._s[1584]! } + public var GroupInfo_Permissions_Removed: String { return self._s[1585]! } + public var ScheduledMessages_ScheduledOnline: String { return self._s[1586]! } + public var Passport_Identity_GenderMale: String { return self._s[1587]! } public func Call_StatusBar(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1519]!, self._r[1519]!, [_0]) + return formatWithArgumentRanges(self._s[1588]!, self._r[1588]!, [_0]) } - public var Notifications_PermissionsKeepDisabled: String { return self._s[1520]! } - public var Conversation_JumpToDate: String { return self._s[1521]! } - public var Contacts_GlobalSearch: String { return self._s[1522]! } - public var AutoDownloadSettings_ResetHelp: String { return self._s[1523]! } - public var SettingsSearch_Synonyms_FAQ: String { return self._s[1524]! } - public var Profile_MessageLifetime1d: String { return self._s[1525]! } + public var Notifications_PermissionsKeepDisabled: String { return self._s[1589]! } + public var Conversation_JumpToDate: String { return self._s[1590]! } + public var Contacts_GlobalSearch: String { return self._s[1591]! } + public var AutoDownloadSettings_ResetHelp: String { return self._s[1592]! } + public var SettingsSearch_Synonyms_FAQ: String { return self._s[1593]! } + public var Profile_MessageLifetime1d: String { return self._s[1594]! } public func MESSAGE_INVOICE(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1526]!, self._r[1526]!, [_1, _2]) + return formatWithArgumentRanges(self._s[1595]!, self._r[1595]!, [_1, _2]) } - public var StickerPack_BuiltinPackName: String { return self._s[1529]! } + public var StickerPack_BuiltinPackName: String { return self._s[1598]! } public func PUSH_CHAT_MESSAGE_AUDIO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1530]!, self._r[1530]!, [_1, _2]) + return formatWithArgumentRanges(self._s[1599]!, self._r[1599]!, [_1, _2]) } - public var VoiceOver_Chat_RecordModeVoiceMessageInfo: String { return self._s[1531]! } - public var Passport_InfoTitle: String { return self._s[1533]! } - public var Notifications_PermissionsUnreachableText: String { return self._s[1534]! } + public var VoiceOver_Chat_RecordModeVoiceMessageInfo: String { return self._s[1600]! } + public var Passport_InfoTitle: String { return self._s[1602]! } + public var Notifications_PermissionsUnreachableText: String { return self._s[1603]! } public func NetworkUsageSettings_CellularUsageSince(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1538]!, self._r[1538]!, [_0]) + return formatWithArgumentRanges(self._s[1607]!, self._r[1607]!, [_0]) } public func PUSH_CHAT_MESSAGE_GEO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1539]!, self._r[1539]!, [_1, _2]) + return formatWithArgumentRanges(self._s[1608]!, self._r[1608]!, [_1, _2]) } - public var Passport_Address_TypePassportRegistrationUploadScan: String { return self._s[1540]! } - public var Profile_BotInfo: String { return self._s[1541]! } - public var Watch_Compose_CreateMessage: String { return self._s[1542]! } - public var AutoDownloadSettings_VoiceMessagesInfo: String { return self._s[1543]! } - public var Month_ShortNovember: String { return self._s[1544]! } - public var Conversation_ScamWarning: String { return self._s[1545]! } - public var Wallpaper_SetCustomBackground: String { return self._s[1546]! } - public var Passport_Identity_TranslationsHelp: String { return self._s[1547]! } - public var NotificationsSound_Chime: String { return self._s[1548]! } - public var Passport_Language_ko: String { return self._s[1550]! } - public var InviteText_URL: String { return self._s[1551]! } - public var TextFormat_Monospace: String { return self._s[1552]! } + public var Passport_Address_TypePassportRegistrationUploadScan: String { return self._s[1609]! } + public var Profile_BotInfo: String { return self._s[1610]! } + public var Watch_Compose_CreateMessage: String { return self._s[1611]! } + public var AutoDownloadSettings_VoiceMessagesInfo: String { return self._s[1612]! } + public var Month_ShortNovember: String { return self._s[1613]! } + public var Conversation_ScamWarning: String { return self._s[1614]! } + public var Wallpaper_SetCustomBackground: String { return self._s[1615]! } + public var Appearance_TextSize_Title: String { return self._s[1616]! } + public var Passport_Identity_TranslationsHelp: String { return self._s[1617]! } + public var NotificationsSound_Chime: String { return self._s[1618]! } + public var Passport_Language_ko: String { return self._s[1620]! } + public var InviteText_URL: String { return self._s[1621]! } + public var TextFormat_Monospace: String { return self._s[1622]! } public func Time_PreciseDate_m11(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1553]!, self._r[1553]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[1623]!, self._r[1623]!, [_1, _2, _3]) } - public var EditTheme_Edit_BottomInfo: String { return self._s[1554]! } + public var EditTheme_Edit_BottomInfo: String { return self._s[1624]! } public func Login_WillSendSms(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1555]!, self._r[1555]!, [_0]) + return formatWithArgumentRanges(self._s[1625]!, self._r[1625]!, [_0]) } public func Watch_Time_ShortWeekdayAt(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1556]!, self._r[1556]!, [_1, _2]) + return formatWithArgumentRanges(self._s[1626]!, self._r[1626]!, [_1, _2]) } - public var Wallet_Words_Title: String { return self._s[1557]! } - public var Wallet_Month_ShortMay: String { return self._s[1558]! } - public var EditTheme_CreateTitle: String { return self._s[1560]! } - public var Passport_InfoLearnMore: String { return self._s[1561]! } - public var TwoStepAuth_EmailPlaceholder: String { return self._s[1562]! } - public var Passport_Identity_AddIdentityCard: String { return self._s[1563]! } - public var Your_card_has_expired: String { return self._s[1564]! } - public var StickerPacksSettings_StickerPacksSection: String { return self._s[1565]! } - public var GroupInfo_InviteLink_Help: String { return self._s[1566]! } - public var TwoFactorSetup_EmailVerification_ResendAction: String { return self._s[1570]! } - public var Conversation_Report: String { return self._s[1572]! } - public var Notifications_MessageNotificationsSound: String { return self._s[1573]! } - public var Notification_MessageLifetime1m: String { return self._s[1574]! } - public var Privacy_ContactsTitle: String { return self._s[1575]! } - public var Conversation_ShareMyContactInfo: String { return self._s[1576]! } - public var Wallet_WordCheck_Title: String { return self._s[1577]! } - public var ChannelMembers_WhoCanAddMembersAdminsHelp: String { return self._s[1578]! } - public var Channel_Members_Title: String { return self._s[1579]! } - public var Map_OpenInWaze: String { return self._s[1580]! } - public var Login_PhoneBannedError: String { return self._s[1581]! } + public var Wallet_Words_Title: String { return self._s[1627]! } + public var Wallet_Month_ShortMay: String { return self._s[1628]! } + public var EditTheme_CreateTitle: String { return self._s[1630]! } + public var Passport_InfoLearnMore: String { return self._s[1631]! } + public var TwoStepAuth_EmailPlaceholder: String { return self._s[1632]! } + public var Passport_Identity_AddIdentityCard: String { return self._s[1633]! } + public var Your_card_has_expired: String { return self._s[1634]! } + public var StickerPacksSettings_StickerPacksSection: String { return self._s[1635]! } + public var GroupInfo_InviteLink_Help: String { return self._s[1636]! } + public var TwoFactorSetup_EmailVerification_ResendAction: String { return self._s[1640]! } + public var Conversation_Report: String { return self._s[1642]! } + public var Notifications_MessageNotificationsSound: String { return self._s[1643]! } + public var Notification_MessageLifetime1m: String { return self._s[1644]! } + public var Privacy_ContactsTitle: String { return self._s[1645]! } + public var Conversation_ShareMyContactInfo: String { return self._s[1646]! } + public var Wallet_WordCheck_Title: String { return self._s[1647]! } + public var ChannelMembers_WhoCanAddMembersAdminsHelp: String { return self._s[1648]! } + public var Channel_Members_Title: String { return self._s[1649]! } + public var Map_OpenInWaze: String { return self._s[1650]! } + public var Appearance_RemoveThemeColorConfirmation: String { return self._s[1651]! } + public var Login_PhoneBannedError: String { return self._s[1652]! } public func LiveLocationUpdated_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1582]!, self._r[1582]!, [_0]) + return formatWithArgumentRanges(self._s[1653]!, self._r[1653]!, [_0]) } - public var Group_Management_AddModeratorHelp: String { return self._s[1583]! } - public var AutoDownloadSettings_WifiTitle: String { return self._s[1584]! } - public var Common_OK: String { return self._s[1585]! } - public var Passport_Address_TypeBankStatementUploadScan: String { return self._s[1586]! } - public var Wallet_Words_NotDoneResponse: String { return self._s[1587]! } - public var Cache_Music: String { return self._s[1588]! } - public var Wallet_Configuration_SourceURL: String { return self._s[1589]! } - public var SettingsSearch_Synonyms_EditProfile_PhoneNumber: String { return self._s[1590]! } - public var PasscodeSettings_UnlockWithTouchId: String { return self._s[1591]! } - public var TwoStepAuth_HintPlaceholder: String { return self._s[1592]! } + public var IntentsSettings_MainAccount: String { return self._s[1654]! } + public var Group_Management_AddModeratorHelp: String { return self._s[1655]! } + public var AutoDownloadSettings_WifiTitle: String { return self._s[1656]! } + public var Common_OK: String { return self._s[1657]! } + public var Passport_Address_TypeBankStatementUploadScan: String { return self._s[1658]! } + public var Wallet_Words_NotDoneResponse: String { return self._s[1659]! } + public var Cache_Music: String { return self._s[1660]! } + public var Wallet_Configuration_SourceURL: String { return self._s[1661]! } + public var SettingsSearch_Synonyms_EditProfile_PhoneNumber: String { return self._s[1662]! } + public var PasscodeSettings_UnlockWithTouchId: String { return self._s[1665]! } + public var TwoStepAuth_HintPlaceholder: String { return self._s[1666]! } public func PUSH_PINNED_INVOICE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1593]!, self._r[1593]!, [_1]) + return formatWithArgumentRanges(self._s[1667]!, self._r[1667]!, [_1]) } public func Passport_RequestHeader(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1594]!, self._r[1594]!, [_0]) + return formatWithArgumentRanges(self._s[1668]!, self._r[1668]!, [_0]) } - public var TwoFactorSetup_Done_Action: String { return self._s[1595]! } + public var TwoFactorSetup_Done_Action: String { return self._s[1669]! } public func VoiceOver_Chat_ContactOrganization(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1596]!, self._r[1596]!, [_0]) + return formatWithArgumentRanges(self._s[1670]!, self._r[1670]!, [_0]) } - public var Wallet_Send_ErrorNotEnoughFundsText: String { return self._s[1597]! } - public var Watch_MessageView_ViewOnPhone: String { return self._s[1599]! } - public var Privacy_Calls_CustomShareHelp: String { return self._s[1600]! } - public var Wallet_Receive_CreateInvoiceInfo: String { return self._s[1602]! } - public var ChangePhoneNumberNumber_Title: String { return self._s[1603]! } - public var State_ConnectingToProxyInfo: String { return self._s[1604]! } - public var Conversation_SwipeToReplyHintTitle: String { return self._s[1605]! } - public var Message_VideoMessage: String { return self._s[1607]! } - public var ChannelInfo_DeleteChannel: String { return self._s[1608]! } - public var ContactInfo_PhoneLabelOther: String { return self._s[1609]! } - public var Channel_EditAdmin_CannotEdit: String { return self._s[1610]! } - public var Passport_DeleteAddressConfirmation: String { return self._s[1611]! } + public var Wallet_Send_ErrorNotEnoughFundsText: String { return self._s[1671]! } + public var Watch_MessageView_ViewOnPhone: String { return self._s[1673]! } + public var Privacy_Calls_CustomShareHelp: String { return self._s[1674]! } + public var Wallet_Receive_CreateInvoiceInfo: String { return self._s[1676]! } + public var ChangePhoneNumberNumber_Title: String { return self._s[1677]! } + public var State_ConnectingToProxyInfo: String { return self._s[1678]! } + public var Conversation_SwipeToReplyHintTitle: String { return self._s[1679]! } + public var Message_VideoMessage: String { return self._s[1681]! } + public var ChannelInfo_DeleteChannel: String { return self._s[1682]! } + public var ContactInfo_PhoneLabelOther: String { return self._s[1683]! } + public var Channel_EditAdmin_CannotEdit: String { return self._s[1684]! } + public var Passport_DeleteAddressConfirmation: String { return self._s[1685]! } public func Wallet_Time_PreciseDate_m9(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1612]!, self._r[1612]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[1686]!, self._r[1686]!, [_1, _2, _3]) } - public var WallpaperPreview_SwipeBottomText: String { return self._s[1613]! } - public var Activity_RecordingAudio: String { return self._s[1614]! } - public var SettingsSearch_Synonyms_Watch: String { return self._s[1615]! } - public var PasscodeSettings_TryAgainIn1Minute: String { return self._s[1616]! } - public var Wallet_Info_Address: String { return self._s[1617]! } + public var WallpaperPreview_SwipeBottomText: String { return self._s[1687]! } + public var Activity_RecordingAudio: String { return self._s[1688]! } + public var SettingsSearch_Synonyms_Watch: String { return self._s[1689]! } + public var PasscodeSettings_TryAgainIn1Minute: String { return self._s[1690]! } + public var Wallet_Info_Address: String { return self._s[1691]! } public func Notification_ChangedGroupName(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1619]!, self._r[1619]!, [_0, _1]) + return formatWithArgumentRanges(self._s[1693]!, self._r[1693]!, [_0, _1]) } public func EmptyGroupInfo_Line1(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1623]!, self._r[1623]!, [_0]) + return formatWithArgumentRanges(self._s[1697]!, self._r[1697]!, [_0]) } - public var Conversation_ApplyLocalization: String { return self._s[1624]! } - public var TwoFactorSetup_Intro_Action: String { return self._s[1625]! } - public var UserInfo_AddPhone: String { return self._s[1626]! } - public var Map_ShareLiveLocationHelp: String { return self._s[1627]! } + public var Conversation_ApplyLocalization: String { return self._s[1698]! } + public var TwoFactorSetup_Intro_Action: String { return self._s[1699]! } + public var UserInfo_AddPhone: String { return self._s[1700]! } + public var Map_ShareLiveLocationHelp: String { return self._s[1701]! } public func Passport_Identity_NativeNameGenericHelp(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1628]!, self._r[1628]!, [_0]) + return formatWithArgumentRanges(self._s[1702]!, self._r[1702]!, [_0]) } - public var Passport_Scans: String { return self._s[1630]! } - public var BlockedUsers_Unblock: String { return self._s[1631]! } + public var Passport_Scans: String { return self._s[1704]! } + public var BlockedUsers_Unblock: String { return self._s[1705]! } public func PUSH_ENCRYPTION_REQUEST(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1632]!, self._r[1632]!, [_1]) + return formatWithArgumentRanges(self._s[1706]!, self._r[1706]!, [_1]) } - public var Channel_Management_LabelCreator: String { return self._s[1633]! } - public var Conversation_ReportSpamAndLeave: String { return self._s[1634]! } - public var SettingsSearch_Synonyms_EditProfile_Bio: String { return self._s[1635]! } - public var ChatList_UndoArchiveMultipleTitle: String { return self._s[1636]! } - public var Passport_Identity_NativeNameGenericTitle: String { return self._s[1637]! } + public var Channel_Management_LabelCreator: String { return self._s[1707]! } + public var Conversation_ReportSpamAndLeave: String { return self._s[1708]! } + public var SettingsSearch_Synonyms_EditProfile_Bio: String { return self._s[1709]! } + public var ChatList_UndoArchiveMultipleTitle: String { return self._s[1710]! } + public var Passport_Identity_NativeNameGenericTitle: String { return self._s[1711]! } public func Login_EmailPhoneBody(_ _0: String, _ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1638]!, self._r[1638]!, [_0, _1, _2]) + return formatWithArgumentRanges(self._s[1712]!, self._r[1712]!, [_0, _1, _2]) } - public var Login_PhoneNumberHelp: String { return self._s[1639]! } - public var LastSeen_ALongTimeAgo: String { return self._s[1640]! } - public var Channel_AdminLog_CanPinMessages: String { return self._s[1641]! } - public var ChannelIntro_CreateChannel: String { return self._s[1642]! } - public var Conversation_UnreadMessages: String { return self._s[1643]! } - public var SettingsSearch_Synonyms_Stickers_ArchivedPacks: String { return self._s[1644]! } - public var Channel_AdminLog_EmptyText: String { return self._s[1645]! } - public var Theme_Context_Apply: String { return self._s[1646]! } - public var Notification_GroupActivated: String { return self._s[1647]! } - public var NotificationSettings_ContactJoinedInfo: String { return self._s[1648]! } - public var Wallet_Intro_CreateWallet: String { return self._s[1649]! } + public var Login_PhoneNumberHelp: String { return self._s[1713]! } + public var LastSeen_ALongTimeAgo: String { return self._s[1714]! } + public var Channel_AdminLog_CanPinMessages: String { return self._s[1715]! } + public var ChannelIntro_CreateChannel: String { return self._s[1716]! } + public var Conversation_UnreadMessages: String { return self._s[1717]! } + public var SettingsSearch_Synonyms_Stickers_ArchivedPacks: String { return self._s[1718]! } + public var Channel_AdminLog_EmptyText: String { return self._s[1719]! } + public var Theme_Context_Apply: String { return self._s[1720]! } + public var Notification_GroupActivated: String { return self._s[1721]! } + public var NotificationSettings_ContactJoinedInfo: String { return self._s[1722]! } + public var Wallet_Intro_CreateWallet: String { return self._s[1723]! } public func Notification_PinnedContactMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1650]!, self._r[1650]!, [_0]) + return formatWithArgumentRanges(self._s[1724]!, self._r[1724]!, [_0]) } public func DownloadingStatus(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1651]!, self._r[1651]!, [_0, _1]) + return formatWithArgumentRanges(self._s[1725]!, self._r[1725]!, [_0, _1]) } - public var GroupInfo_ConvertToSupergroup: String { return self._s[1653]! } + public var GroupInfo_ConvertToSupergroup: String { return self._s[1727]! } public func PrivacyPolicy_AgeVerificationMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1654]!, self._r[1654]!, [_0]) + return formatWithArgumentRanges(self._s[1728]!, self._r[1728]!, [_0]) } - public var Undo_DeletedChannel: String { return self._s[1655]! } - public var CallFeedback_AddComment: String { return self._s[1656]! } + public var Undo_DeletedChannel: String { return self._s[1729]! } + public var CallFeedback_AddComment: String { return self._s[1730]! } public func Conversation_OpenBotLinkAllowMessages(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1657]!, self._r[1657]!, [_0]) + return formatWithArgumentRanges(self._s[1731]!, self._r[1731]!, [_0]) } - public var Document_TargetConfirmationFormat: String { return self._s[1658]! } + public var Document_TargetConfirmationFormat: String { return self._s[1732]! } public func Call_StatusOngoing(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1659]!, self._r[1659]!, [_0]) + return formatWithArgumentRanges(self._s[1733]!, self._r[1733]!, [_0]) } - public var LogoutOptions_SetPasscodeTitle: String { return self._s[1660]! } + public var LogoutOptions_SetPasscodeTitle: String { return self._s[1734]! } public func PUSH_CHAT_MESSAGE_GAME_SCORE(_ _1: String, _ _2: String, _ _3: String, _ _4: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1661]!, self._r[1661]!, [_1, _2, _3, _4]) + return formatWithArgumentRanges(self._s[1735]!, self._r[1735]!, [_1, _2, _3, _4]) } - public var Wallet_SecureStorageChanged_PasscodeText: String { return self._s[1662]! } - public var Theme_ErrorNotFound: String { return self._s[1663]! } - public var Contacts_SortByName: String { return self._s[1664]! } - public var SettingsSearch_Synonyms_Privacy_Forwards: String { return self._s[1665]! } + public var Wallet_SecureStorageChanged_PasscodeText: String { return self._s[1736]! } + public var Theme_ErrorNotFound: String { return self._s[1737]! } + public var Contacts_SortByName: String { return self._s[1738]! } + public var SettingsSearch_Synonyms_Privacy_Forwards: String { return self._s[1739]! } public func CHAT_MESSAGE_INVOICE(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1667]!, self._r[1667]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[1741]!, self._r[1741]!, [_1, _2, _3]) } - public var Notification_Exceptions_RemoveFromExceptions: String { return self._s[1668]! } - public var ScheduledMessages_EditTime: String { return self._s[1669]! } - public var Conversation_ClearSelfHistory: String { return self._s[1670]! } - public var Checkout_NewCard_PostcodePlaceholder: String { return self._s[1671]! } - public var PasscodeSettings_DoNotMatch: String { return self._s[1672]! } - public var Stickers_SuggestNone: String { return self._s[1673]! } - public var ChatSettings_Cache: String { return self._s[1674]! } - public var Settings_SaveIncomingPhotos: String { return self._s[1675]! } - public var Media_ShareThisPhoto: String { return self._s[1676]! } - public var Chat_SlowmodeTooltipPending: String { return self._s[1677]! } - public var InfoPlist_NSContactsUsageDescription: String { return self._s[1678]! } - public var Conversation_ContextMenuCopyLink: String { return self._s[1679]! } - public var PrivacyPolicy_AgeVerificationTitle: String { return self._s[1680]! } - public var SettingsSearch_Synonyms_Stickers_Masks: String { return self._s[1681]! } - public var TwoStepAuth_SetupPasswordEnterPasswordNew: String { return self._s[1682]! } + public var Notification_Exceptions_RemoveFromExceptions: String { return self._s[1742]! } + public var ScheduledMessages_EditTime: String { return self._s[1743]! } + public var Conversation_ClearSelfHistory: String { return self._s[1744]! } + public var Checkout_NewCard_PostcodePlaceholder: String { return self._s[1745]! } + public var PasscodeSettings_DoNotMatch: String { return self._s[1746]! } + public var Stickers_SuggestNone: String { return self._s[1747]! } + public var ChatSettings_Cache: String { return self._s[1748]! } + public var Settings_SaveIncomingPhotos: String { return self._s[1749]! } + public var Media_ShareThisPhoto: String { return self._s[1750]! } + public var Chat_SlowmodeTooltipPending: String { return self._s[1751]! } + public var InfoPlist_NSContactsUsageDescription: String { return self._s[1752]! } + public var Conversation_ContextMenuCopyLink: String { return self._s[1753]! } + public var PrivacyPolicy_AgeVerificationTitle: String { return self._s[1754]! } + public var SettingsSearch_Synonyms_Stickers_Masks: String { return self._s[1755]! } + public var TwoStepAuth_SetupPasswordEnterPasswordNew: String { return self._s[1756]! } + public var Appearance_ThemePreview_Chat_6_Text: String { return self._s[1757]! } public func Wallet_SecureStorageReset_BiometryText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1683]!, self._r[1683]!, [_0]) + return formatWithArgumentRanges(self._s[1758]!, self._r[1758]!, [_0]) } - public var Permissions_CellularDataTitle_v0: String { return self._s[1684]! } - public var WallpaperSearch_ColorWhite: String { return self._s[1686]! } - public var Channel_AdminLog_DefaultRestrictionsUpdated: String { return self._s[1687]! } - public var Conversation_ErrorInaccessibleMessage: String { return self._s[1688]! } - public var Map_OpenIn: String { return self._s[1689]! } + public var Permissions_CellularDataTitle_v0: String { return self._s[1759]! } + public var WallpaperSearch_ColorWhite: String { return self._s[1761]! } + public var Channel_AdminLog_DefaultRestrictionsUpdated: String { return self._s[1762]! } + public var Conversation_ErrorInaccessibleMessage: String { return self._s[1763]! } + public var Map_OpenIn: String { return self._s[1764]! } + public var PeerInfo_ButtonCall: String { return self._s[1765]! } public func PUSH_PHONE_CALL_MISSED(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1692]!, self._r[1692]!, [_1]) + return formatWithArgumentRanges(self._s[1768]!, self._r[1768]!, [_1]) } public func ChannelInfo_AddParticipantConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1693]!, self._r[1693]!, [_0]) + return formatWithArgumentRanges(self._s[1769]!, self._r[1769]!, [_0]) } - public var GroupInfo_Permissions_SlowmodeHeader: String { return self._s[1694]! } - public var MessagePoll_LabelClosed: String { return self._s[1695]! } - public var GroupPermission_PermissionGloballyDisabled: String { return self._s[1697]! } - public var Wallet_Send_SendAnyway: String { return self._s[1698]! } - public var Passport_Identity_MiddleNamePlaceholder: String { return self._s[1699]! } - public var UserInfo_FirstNamePlaceholder: String { return self._s[1700]! } - public var PrivacyLastSeenSettings_WhoCanSeeMyTimestamp: String { return self._s[1701]! } - public var Login_SelectCountry_Title: String { return self._s[1702]! } - public var Channel_EditAdmin_PermissionBanUsers: String { return self._s[1703]! } + public var GroupInfo_Permissions_SlowmodeHeader: String { return self._s[1770]! } + public var MessagePoll_LabelClosed: String { return self._s[1771]! } + public var GroupPermission_PermissionGloballyDisabled: String { return self._s[1773]! } + public var Wallet_Send_SendAnyway: String { return self._s[1774]! } + public var Passport_Identity_MiddleNamePlaceholder: String { return self._s[1775]! } + public var UserInfo_FirstNamePlaceholder: String { return self._s[1776]! } + public var PrivacyLastSeenSettings_WhoCanSeeMyTimestamp: String { return self._s[1777]! } + public var Map_SetThisPlace: String { return self._s[1778]! } + public var Login_SelectCountry_Title: String { return self._s[1779]! } + public var Channel_EditAdmin_PermissionBanUsers: String { return self._s[1780]! } public func Conversation_OpenBotLinkLogin(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1704]!, self._r[1704]!, [_1, _2]) + return formatWithArgumentRanges(self._s[1781]!, self._r[1781]!, [_1, _2]) } - public var Channel_AdminLog_ChangeInfo: String { return self._s[1705]! } - public var Watch_Suggestion_BRB: String { return self._s[1706]! } - public var Passport_Identity_EditIdentityCard: String { return self._s[1707]! } - public var Contacts_PermissionsTitle: String { return self._s[1708]! } - public var Conversation_RestrictedInline: String { return self._s[1709]! } - public var StickerPack_ViewPack: String { return self._s[1711]! } - public var Wallet_UnknownError: String { return self._s[1712]! } + public var Channel_AdminLog_ChangeInfo: String { return self._s[1782]! } + public var Watch_Suggestion_BRB: String { return self._s[1783]! } + public var Passport_Identity_EditIdentityCard: String { return self._s[1784]! } + public var Contacts_PermissionsTitle: String { return self._s[1785]! } + public var Conversation_RestrictedInline: String { return self._s[1786]! } + public var Appearance_RemoveThemeColor: String { return self._s[1788]! } + public var StickerPack_ViewPack: String { return self._s[1789]! } + public var Wallet_UnknownError: String { return self._s[1790]! } public func Update_AppVersion(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1713]!, self._r[1713]!, [_0]) + return formatWithArgumentRanges(self._s[1791]!, self._r[1791]!, [_0]) } - public var Compose_NewChannel: String { return self._s[1715]! } - public var ChatSettings_AutoDownloadSettings_TypePhoto: String { return self._s[1718]! } - public var Conversation_ReportSpamGroupConfirmation: String { return self._s[1720]! } - public var Channel_Info_Stickers: String { return self._s[1721]! } - public var AutoNightTheme_PreferredTheme: String { return self._s[1722]! } - public var PrivacyPolicy_AgeVerificationAgree: String { return self._s[1723]! } - public var Passport_DeletePersonalDetails: String { return self._s[1724]! } - public var LogoutOptions_AddAccountTitle: String { return self._s[1725]! } - public var Channel_DiscussionGroupInfo: String { return self._s[1726]! } - public var Group_EditAdmin_RankOwnerPlaceholder: String { return self._s[1727]! } - public var Conversation_SearchNoResults: String { return self._s[1729]! } - public var Wallet_Configuration_ApplyErrorTextURLInvalid: String { return self._s[1730]! } - public var MessagePoll_LabelAnonymous: String { return self._s[1731]! } - public var Channel_Members_AddAdminErrorNotAMember: String { return self._s[1732]! } - public var Login_Code: String { return self._s[1733]! } - public var EditTheme_Create_BottomInfo: String { return self._s[1734]! } - public var Watch_Suggestion_WhatsUp: String { return self._s[1735]! } - public var Weekday_ShortThursday: String { return self._s[1736]! } - public var Resolve_ErrorNotFound: String { return self._s[1738]! } - public var LastSeen_Offline: String { return self._s[1739]! } - public var PeopleNearby_NoMembers: String { return self._s[1740]! } - public var GroupPermission_AddMembersNotAvailable: String { return self._s[1741]! } - public var Privacy_Calls_AlwaysAllow_Title: String { return self._s[1742]! } - public var GroupInfo_Title: String { return self._s[1744]! } - public var NotificationsSound_Note: String { return self._s[1745]! } - public var Conversation_EditingMessagePanelTitle: String { return self._s[1746]! } - public var Watch_Message_Poll: String { return self._s[1747]! } - public var Privacy_Calls: String { return self._s[1748]! } + public var Compose_NewChannel: String { return self._s[1793]! } + public var ChatSettings_AutoDownloadSettings_TypePhoto: String { return self._s[1796]! } + public var MessagePoll_LabelQuiz: String { return self._s[1798]! } + public var Conversation_ReportSpamGroupConfirmation: String { return self._s[1799]! } + public var Channel_Info_Stickers: String { return self._s[1800]! } + public var AutoNightTheme_PreferredTheme: String { return self._s[1801]! } + public var PrivacyPolicy_AgeVerificationAgree: String { return self._s[1802]! } + public var Passport_DeletePersonalDetails: String { return self._s[1803]! } + public var LogoutOptions_AddAccountTitle: String { return self._s[1804]! } + public var Channel_DiscussionGroupInfo: String { return self._s[1805]! } + public var Group_EditAdmin_RankOwnerPlaceholder: String { return self._s[1806]! } + public var Conversation_SearchNoResults: String { return self._s[1809]! } + public var Wallet_Configuration_ApplyErrorTextURLInvalid: String { return self._s[1810]! } + public var MessagePoll_LabelAnonymous: String { return self._s[1811]! } + public var Channel_Members_AddAdminErrorNotAMember: String { return self._s[1812]! } + public var Login_Code: String { return self._s[1813]! } + public var EditTheme_Create_BottomInfo: String { return self._s[1814]! } + public var Watch_Suggestion_WhatsUp: String { return self._s[1815]! } + public var Weekday_ShortThursday: String { return self._s[1816]! } + public var Resolve_ErrorNotFound: String { return self._s[1818]! } + public var LastSeen_Offline: String { return self._s[1819]! } + public var PeopleNearby_NoMembers: String { return self._s[1820]! } + public var GroupPermission_AddMembersNotAvailable: String { return self._s[1821]! } + public var Privacy_Calls_AlwaysAllow_Title: String { return self._s[1822]! } + public var GroupInfo_Title: String { return self._s[1824]! } + public var NotificationsSound_Note: String { return self._s[1825]! } + public var Conversation_EditingMessagePanelTitle: String { return self._s[1826]! } + public var Watch_Message_Poll: String { return self._s[1827]! } + public var Privacy_Calls: String { return self._s[1828]! } public func Channel_AdminLog_MessageRankUsername(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1749]!, self._r[1749]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[1829]!, self._r[1829]!, [_1, _2, _3]) } - public var Month_ShortAugust: String { return self._s[1750]! } - public var TwoStepAuth_SetPasswordHelp: String { return self._s[1751]! } - public var Notifications_Reset: String { return self._s[1752]! } - public var Conversation_Pin: String { return self._s[1753]! } - public var Passport_Language_lv: String { return self._s[1754]! } - public var Permissions_PeopleNearbyAllowInSettings_v0: String { return self._s[1755]! } - public var BlockedUsers_Info: String { return self._s[1756]! } - public var SettingsSearch_Synonyms_Data_AutoplayVideos: String { return self._s[1758]! } - public var Watch_Conversation_Unblock: String { return self._s[1760]! } + public var Month_ShortAugust: String { return self._s[1830]! } + public var TwoStepAuth_SetPasswordHelp: String { return self._s[1831]! } + public var Notifications_Reset: String { return self._s[1832]! } + public var Conversation_Pin: String { return self._s[1833]! } + public var Passport_Language_lv: String { return self._s[1834]! } + public var Permissions_PeopleNearbyAllowInSettings_v0: String { return self._s[1835]! } + public var BlockedUsers_Info: String { return self._s[1836]! } + public var SettingsSearch_Synonyms_Data_AutoplayVideos: String { return self._s[1838]! } + public var Watch_Conversation_Unblock: String { return self._s[1840]! } public func Time_MonthOfYear_m9(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1761]!, self._r[1761]!, [_0]) + return formatWithArgumentRanges(self._s[1841]!, self._r[1841]!, [_0]) } - public var CloudStorage_Title: String { return self._s[1762]! } - public var GroupInfo_DeleteAndExitConfirmation: String { return self._s[1763]! } + public var CloudStorage_Title: String { return self._s[1842]! } + public var GroupInfo_DeleteAndExitConfirmation: String { return self._s[1843]! } public func NetworkUsageSettings_WifiUsageSince(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1764]!, self._r[1764]!, [_0]) + return formatWithArgumentRanges(self._s[1844]!, self._r[1844]!, [_0]) } - public var Channel_AdminLogFilter_AdminsTitle: String { return self._s[1765]! } - public var Watch_Suggestion_OnMyWay: String { return self._s[1766]! } - public var TwoStepAuth_RecoveryEmailTitle: String { return self._s[1767]! } - public var Passport_Address_EditBankStatement: String { return self._s[1768]! } + public var Channel_AdminLogFilter_AdminsTitle: String { return self._s[1845]! } + public var Watch_Suggestion_OnMyWay: String { return self._s[1846]! } + public var TwoStepAuth_RecoveryEmailTitle: String { return self._s[1847]! } + public var Passport_Address_EditBankStatement: String { return self._s[1848]! } public func Channel_AdminLog_MessageChangedUnlinkedGroup(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1769]!, self._r[1769]!, [_1, _2]) - } - public var ChatSettings_DownloadInBackgroundInfo: String { return self._s[1770]! } - public var ShareMenu_Comment: String { return self._s[1771]! } - public var Permissions_ContactsTitle_v0: String { return self._s[1772]! } - public var Notifications_PermissionsTitle: String { return self._s[1773]! } - public var GroupPermission_NoSendLinks: String { return self._s[1774]! } - public var Privacy_Forwards_NeverAllow_Title: String { return self._s[1775]! } - public var Wallet_SecureStorageChanged_ImportWallet: String { return self._s[1776]! } - public var Settings_Support: String { return self._s[1777]! } - public var Notifications_ChannelNotificationsSound: String { return self._s[1778]! } - public var SettingsSearch_Synonyms_Data_AutoDownloadReset: String { return self._s[1779]! } - public var Privacy_Forwards_Preview: String { return self._s[1780]! } - public var GroupPermission_ApplyAlertAction: String { return self._s[1781]! } - public var Watch_Stickers_StickerPacks: String { return self._s[1782]! } - public var Common_Select: String { return self._s[1784]! } - public var CheckoutInfo_ErrorEmailInvalid: String { return self._s[1785]! } - public var WallpaperSearch_ColorGray: String { return self._s[1788]! } - public var TwoFactorSetup_Password_PlaceholderPassword: String { return self._s[1789]! } - public var TwoFactorSetup_Hint_SkipAction: String { return self._s[1790]! } - public var ChatAdmins_AllMembersAreAdminsOffHelp: String { return self._s[1791]! } - public var PasscodeSettings_AutoLock_IfAwayFor_5hours: String { return self._s[1792]! } - public var Appearance_PreviewReplyAuthor: String { return self._s[1793]! } - public var TwoStepAuth_RecoveryTitle: String { return self._s[1794]! } - public var Widget_AuthRequired: String { return self._s[1795]! } - public var Camera_FlashOn: String { return self._s[1796]! } - public var Conversation_ContextMenuLookUp: String { return self._s[1797]! } - public var Channel_Stickers_NotFoundHelp: String { return self._s[1798]! } - public var Watch_Suggestion_OK: String { return self._s[1799]! } - public func Username_LinkHint(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1801]!, self._r[1801]!, [_0]) - } - public func Notification_PinnedLiveLocationMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1803]!, self._r[1803]!, [_0]) - } - public var TextFormat_Strikethrough: String { return self._s[1804]! } - public var DialogList_AdLabel: String { return self._s[1805]! } - public var WatchRemote_NotificationText: String { return self._s[1806]! } - public var SettingsSearch_Synonyms_Notifications_MessageNotificationsAlert: String { return self._s[1807]! } - public var Conversation_ReportSpam: String { return self._s[1808]! } - public var SettingsSearch_Synonyms_Privacy_Data_TopPeers: String { return self._s[1809]! } - public var Settings_LogoutConfirmationTitle: String { return self._s[1811]! } - public var PhoneLabel_Title: String { return self._s[1812]! } - public var Passport_Address_EditRentalAgreement: String { return self._s[1813]! } - public var Settings_ChangePhoneNumber: String { return self._s[1814]! } - public var Notifications_ExceptionsTitle: String { return self._s[1815]! } - public var Notifications_AlertTones: String { return self._s[1816]! } - public var Call_ReportIncludeLogDescription: String { return self._s[1817]! } - public var SettingsSearch_Synonyms_Notifications_ResetAllNotifications: String { return self._s[1818]! } - public var AutoDownloadSettings_PrivateChats: String { return self._s[1819]! } - public var VoiceOver_Chat_Photo: String { return self._s[1821]! } - public var TwoStepAuth_AddHintTitle: String { return self._s[1822]! } - public var ReportPeer_ReasonOther: String { return self._s[1823]! } - public var ChatList_Context_JoinChannel: String { return self._s[1824]! } - public var KeyCommand_ScrollDown: String { return self._s[1826]! } - public var Conversation_ScheduleMessage_Title: String { return self._s[1827]! } - public func Login_BannedPhoneSubject(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1828]!, self._r[1828]!, [_0]) - } - public var NetworkUsageSettings_MediaVideoDataSection: String { return self._s[1829]! } - public var ChannelInfo_DeleteGroupConfirmation: String { return self._s[1830]! } - public var AuthSessions_LogOut: String { return self._s[1831]! } - public var Passport_Identity_TypeInternalPassport: String { return self._s[1832]! } - public var ChatSettings_AutoDownloadVoiceMessages: String { return self._s[1833]! } - public var Passport_Phone_Title: String { return self._s[1834]! } - public var ContactList_Context_StartSecretChat: String { return self._s[1835]! } - public var Settings_PhoneNumber: String { return self._s[1836]! } - public func Conversation_ScheduleMessage_SendToday(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1837]!, self._r[1837]!, [_0]) - } - public var NotificationsSound_Alert: String { return self._s[1838]! } - public var Wallet_SecureStorageChanged_CreateWallet: String { return self._s[1839]! } - public var WebSearch_SearchNoResults: String { return self._s[1840]! } - public var Privacy_ProfilePhoto_AlwaysShareWith_Title: String { return self._s[1842]! } - public var Wallet_Configuration_SourceInfo: String { return self._s[1843]! } - public var LogoutOptions_AlternativeOptionsSection: String { return self._s[1844]! } - public var SettingsSearch_Synonyms_Passport: String { return self._s[1845]! } - public var PhotoEditor_CurvesTool: String { return self._s[1846]! } - public var Checkout_PaymentMethod: String { return self._s[1848]! } - public func PUSH_CHAT_ADD_YOU(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[1849]!, self._r[1849]!, [_1, _2]) } - public var Contacts_AccessDeniedError: String { return self._s[1850]! } - public var Camera_PhotoMode: String { return self._s[1853]! } - public var EditTheme_Expand_Preview_IncomingText: String { return self._s[1854]! } - public var Passport_Address_AddUtilityBill: String { return self._s[1856]! } - public var CallSettings_OnMobile: String { return self._s[1857]! } - public var Tour_Text2: String { return self._s[1858]! } + public var ChatSettings_DownloadInBackgroundInfo: String { return self._s[1850]! } + public var ShareMenu_Comment: String { return self._s[1851]! } + public var Permissions_ContactsTitle_v0: String { return self._s[1852]! } + public var Notifications_PermissionsTitle: String { return self._s[1853]! } + public var GroupPermission_NoSendLinks: String { return self._s[1854]! } + public var Privacy_Forwards_NeverAllow_Title: String { return self._s[1855]! } + public var Wallet_SecureStorageChanged_ImportWallet: String { return self._s[1856]! } + public var PeerInfo_PaneLinks: String { return self._s[1857]! } + public var Settings_Support: String { return self._s[1858]! } + public var Notifications_ChannelNotificationsSound: String { return self._s[1859]! } + public var SettingsSearch_Synonyms_Data_AutoDownloadReset: String { return self._s[1860]! } + public var Privacy_Forwards_Preview: String { return self._s[1861]! } + public var GroupPermission_ApplyAlertAction: String { return self._s[1862]! } + public var Watch_Stickers_StickerPacks: String { return self._s[1863]! } + public var Common_Select: String { return self._s[1865]! } + public var CheckoutInfo_ErrorEmailInvalid: String { return self._s[1866]! } + public var WallpaperSearch_ColorGray: String { return self._s[1869]! } + public var TwoFactorSetup_Password_PlaceholderPassword: String { return self._s[1870]! } + public var TwoFactorSetup_Hint_SkipAction: String { return self._s[1871]! } + public var ChatAdmins_AllMembersAreAdminsOffHelp: String { return self._s[1872]! } + public var PollResults_Title: String { return self._s[1873]! } + public var PasscodeSettings_AutoLock_IfAwayFor_5hours: String { return self._s[1874]! } + public var Appearance_PreviewReplyAuthor: String { return self._s[1875]! } + public var TwoStepAuth_RecoveryTitle: String { return self._s[1876]! } + public var Widget_AuthRequired: String { return self._s[1877]! } + public var Camera_FlashOn: String { return self._s[1878]! } + public var Conversation_ContextMenuLookUp: String { return self._s[1879]! } + public var Channel_Stickers_NotFoundHelp: String { return self._s[1880]! } + public var Watch_Suggestion_OK: String { return self._s[1881]! } + public func Username_LinkHint(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1883]!, self._r[1883]!, [_0]) + } + public func Notification_PinnedLiveLocationMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1885]!, self._r[1885]!, [_0]) + } + public var TextFormat_Strikethrough: String { return self._s[1886]! } + public var DialogList_AdLabel: String { return self._s[1887]! } + public var WatchRemote_NotificationText: String { return self._s[1888]! } + public var IntentsSettings_SuggestedChatsSavedMessages: String { return self._s[1889]! } + public var SettingsSearch_Synonyms_Notifications_MessageNotificationsAlert: String { return self._s[1890]! } + public var Conversation_ReportSpam: String { return self._s[1891]! } + public var SettingsSearch_Synonyms_Privacy_Data_TopPeers: String { return self._s[1892]! } + public var Settings_LogoutConfirmationTitle: String { return self._s[1894]! } + public var PhoneLabel_Title: String { return self._s[1895]! } + public var Passport_Address_EditRentalAgreement: String { return self._s[1896]! } + public var Settings_ChangePhoneNumber: String { return self._s[1897]! } + public var Notifications_ExceptionsTitle: String { return self._s[1898]! } + public var Notifications_AlertTones: String { return self._s[1899]! } + public var Call_ReportIncludeLogDescription: String { return self._s[1900]! } + public var SettingsSearch_Synonyms_Notifications_ResetAllNotifications: String { return self._s[1901]! } + public var AutoDownloadSettings_PrivateChats: String { return self._s[1902]! } + public var VoiceOver_Chat_Photo: String { return self._s[1904]! } + public var TwoStepAuth_AddHintTitle: String { return self._s[1905]! } + public var ReportPeer_ReasonOther: String { return self._s[1906]! } + public var ChatList_Context_JoinChannel: String { return self._s[1907]! } + public var KeyCommand_ScrollDown: String { return self._s[1909]! } + public var Conversation_ScheduleMessage_Title: String { return self._s[1910]! } + public func Login_BannedPhoneSubject(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1911]!, self._r[1911]!, [_0]) + } + public var NetworkUsageSettings_MediaVideoDataSection: String { return self._s[1912]! } + public var ChannelInfo_DeleteGroupConfirmation: String { return self._s[1913]! } + public var AuthSessions_LogOut: String { return self._s[1914]! } + public var Passport_Identity_TypeInternalPassport: String { return self._s[1915]! } + public var ChatSettings_AutoDownloadVoiceMessages: String { return self._s[1916]! } + public var Passport_Phone_Title: String { return self._s[1917]! } + public var ContactList_Context_StartSecretChat: String { return self._s[1918]! } + public var Settings_PhoneNumber: String { return self._s[1919]! } + public func Conversation_ScheduleMessage_SendToday(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1920]!, self._r[1920]!, [_0]) + } + public var NotificationsSound_Alert: String { return self._s[1922]! } + public var Wallet_SecureStorageChanged_CreateWallet: String { return self._s[1923]! } + public var WebSearch_SearchNoResults: String { return self._s[1924]! } + public var Privacy_ProfilePhoto_AlwaysShareWith_Title: String { return self._s[1926]! } + public var Wallet_Configuration_SourceInfo: String { return self._s[1927]! } + public var LogoutOptions_AlternativeOptionsSection: String { return self._s[1928]! } + public var SettingsSearch_Synonyms_Passport: String { return self._s[1929]! } + public var PhotoEditor_CurvesTool: String { return self._s[1930]! } + public var Checkout_PaymentMethod: String { return self._s[1932]! } + public func PUSH_CHAT_ADD_YOU(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1933]!, self._r[1933]!, [_1, _2]) + } + public var Contacts_AccessDeniedError: String { return self._s[1934]! } + public var Camera_PhotoMode: String { return self._s[1937]! } + public var EditTheme_Expand_Preview_IncomingText: String { return self._s[1938]! } + public var Appearance_TextSize_Apply: String { return self._s[1939]! } + public var Passport_Address_AddUtilityBill: String { return self._s[1941]! } + public var CallSettings_OnMobile: String { return self._s[1942]! } + public var Tour_Text2: String { return self._s[1943]! } public func PUSH_CHAT_MESSAGE_ROUND(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1859]!, self._r[1859]!, [_1, _2]) - } - public var DialogList_EncryptionProcessing: String { return self._s[1861]! } - public var Permissions_Skip: String { return self._s[1862]! } - public var Wallet_Words_NotDoneOk: String { return self._s[1863]! } - public var SecretImage_Title: String { return self._s[1864]! } - public var Watch_MessageView_Title: String { return self._s[1865]! } - public var Channel_DiscussionGroupAdd: String { return self._s[1866]! } - public var AttachmentMenu_Poll: String { return self._s[1867]! } - public func Notification_GroupInviter(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1868]!, self._r[1868]!, [_0]) - } - public func Channel_DiscussionGroup_PrivateChannelLink(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1869]!, self._r[1869]!, [_1, _2]) - } - public var Notification_CallCanceled: String { return self._s[1870]! } - public var WallpaperPreview_Title: String { return self._s[1871]! } - public var Privacy_PaymentsClear_PaymentInfo: String { return self._s[1872]! } - public var Settings_ProxyConnecting: String { return self._s[1873]! } - public var Settings_CheckPhoneNumberText: String { return self._s[1875]! } - public var VoiceOver_Chat_YourVideo: String { return self._s[1876]! } - public var Wallet_Intro_Title: String { return self._s[1877]! } - public var TwoFactorSetup_Password_Action: String { return self._s[1878]! } - public var Profile_MessageLifetime5s: String { return self._s[1879]! } - public var Username_InvalidCharacters: String { return self._s[1880]! } - public var VoiceOver_Media_PlaybackRateFast: String { return self._s[1881]! } - public var ScheduledMessages_ClearAll: String { return self._s[1882]! } - public var WallpaperPreview_CropBottomText: String { return self._s[1883]! } - public var AutoDownloadSettings_LimitBySize: String { return self._s[1884]! } - public var Settings_AddAccount: String { return self._s[1885]! } - public var Notification_CreatedChannel: String { return self._s[1888]! } - public func PUSH_CHAT_DELETE_MEMBER(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1889]!, self._r[1889]!, [_1, _2, _3]) - } - public var Passcode_AppLockedAlert: String { return self._s[1891]! } - public var StickerPacksSettings_AnimatedStickersInfo: String { return self._s[1892]! } - public var VoiceOver_Media_PlaybackStop: String { return self._s[1893]! } - public var Contacts_TopSection: String { return self._s[1894]! } - public var ChatList_DeleteForEveryoneConfirmationAction: String { return self._s[1895]! } - public func Conversation_SetReminder_RemindOn(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1896]!, self._r[1896]!, [_0, _1]) - } - public var Wallet_Info_Receive: String { return self._s[1897]! } - public var Wallet_Completed_ViewWallet: String { return self._s[1898]! } - public func Time_MonthOfYear_m6(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1899]!, self._r[1899]!, [_0]) - } - public var ReportPeer_ReasonSpam: String { return self._s[1900]! } - public var UserInfo_TapToCall: String { return self._s[1901]! } - public var Conversation_ForwardAuthorHiddenTooltip: String { return self._s[1903]! } - public var AutoDownloadSettings_DataUsageCustom: String { return self._s[1904]! } - public var Common_Search: String { return self._s[1905]! } - public var ScheduledMessages_EmptyPlaceholder: String { return self._s[1906]! } - public func Channel_AdminLog_MessageChangedGroupGeoLocation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1907]!, self._r[1907]!, [_0]) - } - public var Wallet_Month_ShortJuly: String { return self._s[1908]! } - public var AuthSessions_IncompleteAttemptsInfo: String { return self._s[1910]! } - public var Message_InvoiceLabel: String { return self._s[1911]! } - public var Conversation_InputTextPlaceholder: String { return self._s[1912]! } - public var NetworkUsageSettings_MediaImageDataSection: String { return self._s[1913]! } - public func Passport_Address_UploadOneOfScan(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1914]!, self._r[1914]!, [_0]) - } - public var Conversation_Info: String { return self._s[1915]! } - public var Login_InfoDeletePhoto: String { return self._s[1916]! } - public var Passport_Language_vi: String { return self._s[1918]! } - public var UserInfo_ScamUserWarning: String { return self._s[1919]! } - public var Conversation_Search: String { return self._s[1920]! } - public var DialogList_DeleteBotConversationConfirmation: String { return self._s[1922]! } - public var ReportPeer_ReasonPornography: String { return self._s[1923]! } - public var AutoDownloadSettings_PhotosTitle: String { return self._s[1924]! } - public var Conversation_SendMessageErrorGroupRestricted: String { return self._s[1925]! } - public var Map_LiveLocationGroupDescription: String { return self._s[1926]! } - public var Channel_Setup_TypeHeader: String { return self._s[1927]! } - public var AuthSessions_LoggedIn: String { return self._s[1928]! } - public var Privacy_Forwards_AlwaysAllow_Title: String { return self._s[1929]! } - public var Login_SmsRequestState3: String { return self._s[1930]! } - public var Passport_Address_EditUtilityBill: String { return self._s[1931]! } - public var Appearance_ReduceMotionInfo: String { return self._s[1932]! } - public var Join_ChannelsTooMuch: String { return self._s[1933]! } - public var Channel_Edit_LinkItem: String { return self._s[1934]! } - public var Privacy_Calls_P2PNever: String { return self._s[1935]! } - public var Conversation_AddToReadingList: String { return self._s[1937]! } - public var Share_MultipleMessagesDisabled: String { return self._s[1938]! } - public var Message_Animation: String { return self._s[1939]! } - public var Conversation_DefaultRestrictedMedia: String { return self._s[1940]! } - public var Map_Unknown: String { return self._s[1941]! } - public var AutoDownloadSettings_LastDelimeter: String { return self._s[1942]! } - public func PUSH_PINNED_TEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1943]!, self._r[1943]!, [_1, _2]) - } - public func Passport_FieldOneOf_Or(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[1944]!, self._r[1944]!, [_1, _2]) } - public var Call_StatusRequesting: String { return self._s[1945]! } - public var Conversation_SecretChatContextBotAlert: String { return self._s[1946]! } - public var SocksProxySetup_ProxyStatusChecking: String { return self._s[1947]! } + public var DialogList_EncryptionProcessing: String { return self._s[1946]! } + public var Permissions_Skip: String { return self._s[1947]! } + public var Wallet_Words_NotDoneOk: String { return self._s[1948]! } + public var SecretImage_Title: String { return self._s[1949]! } + public var Watch_MessageView_Title: String { return self._s[1950]! } + public var Channel_DiscussionGroupAdd: String { return self._s[1951]! } + public var AttachmentMenu_Poll: String { return self._s[1952]! } + public func Notification_GroupInviter(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1953]!, self._r[1953]!, [_0]) + } + public func Channel_DiscussionGroup_PrivateChannelLink(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1954]!, self._r[1954]!, [_1, _2]) + } + public var Notification_CallCanceled: String { return self._s[1955]! } + public var WallpaperPreview_Title: String { return self._s[1956]! } + public var Privacy_PaymentsClear_PaymentInfo: String { return self._s[1957]! } + public var Settings_ProxyConnecting: String { return self._s[1958]! } + public var Settings_CheckPhoneNumberText: String { return self._s[1960]! } + public var VoiceOver_Chat_YourVideo: String { return self._s[1961]! } + public var Wallet_Intro_Title: String { return self._s[1962]! } + public var TwoFactorSetup_Password_Action: String { return self._s[1963]! } + public var Profile_MessageLifetime5s: String { return self._s[1964]! } + public var Username_InvalidCharacters: String { return self._s[1965]! } + public var VoiceOver_Media_PlaybackRateFast: String { return self._s[1966]! } + public var ScheduledMessages_ClearAll: String { return self._s[1967]! } + public var WallpaperPreview_CropBottomText: String { return self._s[1968]! } + public var AutoDownloadSettings_LimitBySize: String { return self._s[1969]! } + public var Settings_AddAccount: String { return self._s[1970]! } + public var Notification_CreatedChannel: String { return self._s[1973]! } + public func PUSH_CHAT_DELETE_MEMBER(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1974]!, self._r[1974]!, [_1, _2, _3]) + } + public var Passcode_AppLockedAlert: String { return self._s[1976]! } + public var StickerPacksSettings_AnimatedStickersInfo: String { return self._s[1977]! } + public var VoiceOver_Media_PlaybackStop: String { return self._s[1978]! } + public var Contacts_TopSection: String { return self._s[1979]! } + public var ChatList_DeleteForEveryoneConfirmationAction: String { return self._s[1980]! } + public func Conversation_SetReminder_RemindOn(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1981]!, self._r[1981]!, [_0, _1]) + } + public var Wallet_Info_Receive: String { return self._s[1982]! } + public var Wallet_Completed_ViewWallet: String { return self._s[1983]! } + public func Time_MonthOfYear_m6(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1984]!, self._r[1984]!, [_0]) + } + public var ReportPeer_ReasonSpam: String { return self._s[1985]! } + public var UserInfo_TapToCall: String { return self._s[1986]! } + public var Conversation_ForwardAuthorHiddenTooltip: String { return self._s[1988]! } + public var AutoDownloadSettings_DataUsageCustom: String { return self._s[1989]! } + public var Common_Search: String { return self._s[1990]! } + public var ScheduledMessages_EmptyPlaceholder: String { return self._s[1991]! } + public func Channel_AdminLog_MessageChangedGroupGeoLocation(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1992]!, self._r[1992]!, [_0]) + } + public var Wallet_Month_ShortJuly: String { return self._s[1993]! } + public var AuthSessions_IncompleteAttemptsInfo: String { return self._s[1995]! } + public var Message_InvoiceLabel: String { return self._s[1996]! } + public var Conversation_InputTextPlaceholder: String { return self._s[1997]! } + public var NetworkUsageSettings_MediaImageDataSection: String { return self._s[1998]! } + public func Passport_Address_UploadOneOfScan(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1999]!, self._r[1999]!, [_0]) + } + public var IntentsSettings_Reset: String { return self._s[2000]! } + public var Conversation_Info: String { return self._s[2001]! } + public var Login_InfoDeletePhoto: String { return self._s[2002]! } + public var Passport_Language_vi: String { return self._s[2004]! } + public var UserInfo_ScamUserWarning: String { return self._s[2005]! } + public var Conversation_Search: String { return self._s[2006]! } + public var DialogList_DeleteBotConversationConfirmation: String { return self._s[2008]! } + public var ReportPeer_ReasonPornography: String { return self._s[2009]! } + public var AutoDownloadSettings_PhotosTitle: String { return self._s[2010]! } + public var Conversation_SendMessageErrorGroupRestricted: String { return self._s[2011]! } + public var Map_LiveLocationGroupDescription: String { return self._s[2012]! } + public var Channel_Setup_TypeHeader: String { return self._s[2013]! } + public var AuthSessions_LoggedIn: String { return self._s[2014]! } + public var Privacy_Forwards_AlwaysAllow_Title: String { return self._s[2015]! } + public var Login_SmsRequestState3: String { return self._s[2016]! } + public var Passport_Address_EditUtilityBill: String { return self._s[2017]! } + public var Appearance_ReduceMotionInfo: String { return self._s[2018]! } + public var Join_ChannelsTooMuch: String { return self._s[2019]! } + public var Channel_Edit_LinkItem: String { return self._s[2020]! } + public var Privacy_Calls_P2PNever: String { return self._s[2021]! } + public var Conversation_AddToReadingList: String { return self._s[2023]! } + public var Share_MultipleMessagesDisabled: String { return self._s[2024]! } + public var Message_Animation: String { return self._s[2025]! } + public var Conversation_DefaultRestrictedMedia: String { return self._s[2026]! } + public var Map_Unknown: String { return self._s[2027]! } + public var AutoDownloadSettings_LastDelimeter: String { return self._s[2028]! } + public func PUSH_PINNED_TEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2029]!, self._r[2029]!, [_1, _2]) + } + public func Passport_FieldOneOf_Or(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2030]!, self._r[2030]!, [_1, _2]) + } + public var Call_StatusRequesting: String { return self._s[2031]! } + public var Conversation_SecretChatContextBotAlert: String { return self._s[2032]! } + public var SocksProxySetup_ProxyStatusChecking: String { return self._s[2033]! } public func PUSH_CHAT_MESSAGE_DOC(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1948]!, self._r[1948]!, [_1, _2]) + return formatWithArgumentRanges(self._s[2034]!, self._r[2034]!, [_1, _2]) } public func Notification_PinnedLocationMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1949]!, self._r[1949]!, [_0]) + return formatWithArgumentRanges(self._s[2035]!, self._r[2035]!, [_0]) } - public var Update_Skip: String { return self._s[1950]! } - public var Group_Username_RemoveExistingUsernamesInfo: String { return self._s[1951]! } - public var Message_PinnedPollMessage: String { return self._s[1952]! } - public var BlockedUsers_Title: String { return self._s[1953]! } + public var Update_Skip: String { return self._s[2036]! } + public var Group_Username_RemoveExistingUsernamesInfo: String { return self._s[2037]! } + public var BlockedUsers_Title: String { return self._s[2038]! } + public var Weekday_Monday: String { return self._s[2039]! } public func PUSH_CHANNEL_MESSAGE_AUDIO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1954]!, self._r[1954]!, [_1]) - } - public var Username_CheckingUsername: String { return self._s[1955]! } - public var NotificationsSound_Bell: String { return self._s[1956]! } - public var Conversation_SendMessageErrorFlood: String { return self._s[1957]! } - public var Weekday_Monday: String { return self._s[1958]! } - public var SettingsSearch_Synonyms_Notifications_DisplayNamesOnLockScreen: String { return self._s[1959]! } - public var ChannelMembers_ChannelAdminsTitle: String { return self._s[1960]! } - public var ChatSettings_Groups: String { return self._s[1961]! } - public func Conversation_SetReminder_RemindTomorrow(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1962]!, self._r[1962]!, [_0]) - } - public var Your_card_was_declined: String { return self._s[1963]! } - public var TwoStepAuth_EnterPasswordHelp: String { return self._s[1965]! } - public var Wallet_Month_ShortApril: String { return self._s[1966]! } - public var ChatList_Unmute: String { return self._s[1967]! } - public var PhotoEditor_CurvesAll: String { return self._s[1968]! } - public var Weekday_ShortTuesday: String { return self._s[1969]! } - public var DialogList_Read: String { return self._s[1970]! } - public var Appearance_AppIconClassic: String { return self._s[1971]! } - public var ChannelMembers_WhoCanAddMembers_AllMembers: String { return self._s[1972]! } - public var Passport_Identity_Gender: String { return self._s[1973]! } - public func Target_ShareGameConfirmationPrivate(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1974]!, self._r[1974]!, [_0]) - } - public var Target_SelectGroup: String { return self._s[1975]! } - public func DialogList_EncryptedChatStartedIncoming(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1977]!, self._r[1977]!, [_0]) - } - public var Passport_Language_en: String { return self._s[1978]! } - public var AutoDownloadSettings_AutodownloadPhotos: String { return self._s[1979]! } - public var Channel_Username_CreatePublicLinkHelp: String { return self._s[1980]! } - public var Login_CancelPhoneVerificationContinue: String { return self._s[1981]! } - public var ScheduledMessages_SendNow: String { return self._s[1982]! } - public var Checkout_NewCard_PaymentCard: String { return self._s[1984]! } - public var Login_InfoHelp: String { return self._s[1985]! } - public var Contacts_PermissionsSuppressWarningTitle: String { return self._s[1986]! } - public var SettingsSearch_Synonyms_Stickers_FeaturedPacks: String { return self._s[1987]! } - public func Channel_AdminLog_MessageChangedLinkedChannel(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1988]!, self._r[1988]!, [_1, _2]) - } - public var SocksProxySetup_AddProxy: String { return self._s[1991]! } - public var CreatePoll_Title: String { return self._s[1992]! } - public var Conversation_ViewTheme: String { return self._s[1993]! } - public var SettingsSearch_Synonyms_Privacy_Data_SecretChatLinkPreview: String { return self._s[1994]! } - public var PasscodeSettings_SimplePasscodeHelp: String { return self._s[1995]! } - public var TwoFactorSetup_Intro_Text: String { return self._s[1996]! } - public var UserInfo_GroupsInCommon: String { return self._s[1997]! } - public var TelegramWallet_Intro_TermsUrl: String { return self._s[1998]! } - public var Call_AudioRouteHide: String { return self._s[1999]! } - public func Wallet_Info_TransactionDateHeader(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2001]!, self._r[2001]!, [_1, _2]) - } - public var ContactInfo_PhoneLabelMobile: String { return self._s[2002]! } - public func ChatList_LeaveGroupConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2003]!, self._r[2003]!, [_0]) - } - public var TextFormat_Bold: String { return self._s[2004]! } - public var FastTwoStepSetup_EmailSection: String { return self._s[2005]! } - public var Notifications_Title: String { return self._s[2006]! } - public var Group_Username_InvalidTooShort: String { return self._s[2007]! } - public var Channel_ErrorAddTooMuch: String { return self._s[2008]! } - public func DialogList_MultipleTypingSuffix(_ _0: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2009]!, self._r[2009]!, ["\(_0)"]) - } - public var VoiceOver_DiscardPreparedContent: String { return self._s[2011]! } - public var Stickers_SuggestAdded: String { return self._s[2012]! } - public var Login_CountryCode: String { return self._s[2013]! } - public var ChatSettings_AutoPlayVideos: String { return self._s[2014]! } - public var Map_GetDirections: String { return self._s[2015]! } - public var Wallet_Receive_ShareInvoiceUrl: String { return self._s[2016]! } - public var Login_PhoneFloodError: String { return self._s[2017]! } - public func Time_MonthOfYear_m3(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2018]!, self._r[2018]!, [_0]) - } - public func Wallet_Time_PreciseDate_m10(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2019]!, self._r[2019]!, [_1, _2, _3]) - } - public var Settings_SetUsername: String { return self._s[2021]! } - public var Group_Location_ChangeLocation: String { return self._s[2022]! } - public var Notification_GroupInviterSelf: String { return self._s[2023]! } - public var InstantPage_TapToOpenLink: String { return self._s[2024]! } - public func Notification_ChannelInviter(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2025]!, self._r[2025]!, [_0]) - } - public var Watch_Suggestion_TalkLater: String { return self._s[2026]! } - public var SecretChat_Title: String { return self._s[2027]! } - public var Group_UpgradeNoticeText1: String { return self._s[2028]! } - public var AuthSessions_Title: String { return self._s[2029]! } - public func TextFormat_AddLinkText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2030]!, self._r[2030]!, [_0]) - } - public var PhotoEditor_CropAuto: String { return self._s[2031]! } - public var Channel_About_Title: String { return self._s[2032]! } - public var FastTwoStepSetup_EmailHelp: String { return self._s[2033]! } - public func Conversation_Bytes(_ _0: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2035]!, self._r[2035]!, ["\(_0)"]) - } - public var VoiceOver_MessageContextReport: String { return self._s[2036]! } - public var Conversation_PinMessageAlert_OnlyPin: String { return self._s[2038]! } - public var Group_Setup_HistoryVisibleHelp: String { return self._s[2039]! } - public func PUSH_MESSAGE_GIF(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[2040]!, self._r[2040]!, [_1]) } + public var Username_CheckingUsername: String { return self._s[2041]! } + public var NotificationsSound_Bell: String { return self._s[2042]! } + public var Conversation_SendMessageErrorFlood: String { return self._s[2043]! } + public var SettingsSearch_Synonyms_Notifications_DisplayNamesOnLockScreen: String { return self._s[2044]! } + public var ChannelMembers_ChannelAdminsTitle: String { return self._s[2045]! } + public var ChatSettings_Groups: String { return self._s[2046]! } + public var WallpaperPreview_PatternPaternDiscard: String { return self._s[2047]! } + public func Conversation_SetReminder_RemindTomorrow(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2048]!, self._r[2048]!, [_0]) + } + public var Your_card_was_declined: String { return self._s[2049]! } + public var TwoStepAuth_EnterPasswordHelp: String { return self._s[2051]! } + public var Wallet_Month_ShortApril: String { return self._s[2052]! } + public var ChatList_Unmute: String { return self._s[2053]! } + public var AuthSessions_AddDevice_ScanTitle: String { return self._s[2054]! } + public var PhotoEditor_CurvesAll: String { return self._s[2055]! } + public var Weekday_ShortTuesday: String { return self._s[2056]! } + public var DialogList_Read: String { return self._s[2057]! } + public var Appearance_AppIconClassic: String { return self._s[2058]! } + public var ChannelMembers_WhoCanAddMembers_AllMembers: String { return self._s[2059]! } + public var Passport_Identity_Gender: String { return self._s[2060]! } + public func Target_ShareGameConfirmationPrivate(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2061]!, self._r[2061]!, [_0]) + } + public var Target_SelectGroup: String { return self._s[2062]! } + public var Map_HomeAndWorkInfo: String { return self._s[2064]! } + public func DialogList_EncryptedChatStartedIncoming(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2065]!, self._r[2065]!, [_0]) + } + public var Passport_Language_en: String { return self._s[2066]! } + public var AutoDownloadSettings_AutodownloadPhotos: String { return self._s[2067]! } + public var Channel_Username_CreatePublicLinkHelp: String { return self._s[2068]! } + public var Login_CancelPhoneVerificationContinue: String { return self._s[2069]! } + public var ScheduledMessages_SendNow: String { return self._s[2070]! } + public var Checkout_NewCard_PaymentCard: String { return self._s[2072]! } + public var Login_InfoHelp: String { return self._s[2073]! } + public var Appearance_BubbleCorners_AdjustAdjacent: String { return self._s[2074]! } + public var Contacts_PermissionsSuppressWarningTitle: String { return self._s[2075]! } + public var SettingsSearch_Synonyms_Stickers_FeaturedPacks: String { return self._s[2076]! } + public func Channel_AdminLog_MessageChangedLinkedChannel(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2077]!, self._r[2077]!, [_1, _2]) + } + public var SocksProxySetup_AddProxy: String { return self._s[2080]! } + public var CreatePoll_Title: String { return self._s[2081]! } + public var MessagePoll_QuizNoUsers: String { return self._s[2082]! } + public var Conversation_ViewTheme: String { return self._s[2083]! } + public var SettingsSearch_Synonyms_Privacy_Data_SecretChatLinkPreview: String { return self._s[2084]! } + public var PasscodeSettings_SimplePasscodeHelp: String { return self._s[2085]! } + public var TwoFactorSetup_Intro_Text: String { return self._s[2086]! } + public var UserInfo_GroupsInCommon: String { return self._s[2087]! } + public var TelegramWallet_Intro_TermsUrl: String { return self._s[2088]! } + public var Call_AudioRouteHide: String { return self._s[2089]! } + public func Wallet_Info_TransactionDateHeader(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2091]!, self._r[2091]!, [_1, _2]) + } + public var ContactInfo_PhoneLabelMobile: String { return self._s[2092]! } + public var IntentsSettings_SuggestedChatsInfo: String { return self._s[2093]! } + public var CreatePoll_QuizOptionsHeader: String { return self._s[2094]! } + public func ChatList_LeaveGroupConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2095]!, self._r[2095]!, [_0]) + } + public var TextFormat_Bold: String { return self._s[2096]! } + public var FastTwoStepSetup_EmailSection: String { return self._s[2097]! } + public var StickerPackActionInfo_AddedTitle: String { return self._s[2098]! } + public var Notifications_Title: String { return self._s[2099]! } + public var Group_Username_InvalidTooShort: String { return self._s[2100]! } + public var Channel_ErrorAddTooMuch: String { return self._s[2101]! } + public func DialogList_MultipleTypingSuffix(_ _0: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2102]!, self._r[2102]!, ["\(_0)"]) + } + public var VoiceOver_DiscardPreparedContent: String { return self._s[2104]! } + public var Stickers_SuggestAdded: String { return self._s[2105]! } + public var Login_CountryCode: String { return self._s[2106]! } + public var ChatSettings_AutoPlayVideos: String { return self._s[2107]! } + public var Map_GetDirections: String { return self._s[2108]! } + public var Wallet_Receive_ShareInvoiceUrl: String { return self._s[2109]! } + public var Login_PhoneFloodError: String { return self._s[2110]! } + public func Time_MonthOfYear_m3(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2111]!, self._r[2111]!, [_0]) + } + public func Wallet_Time_PreciseDate_m10(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2112]!, self._r[2112]!, [_1, _2, _3]) + } + public var IntentsSettings_SuggestedChatsPrivateChats: String { return self._s[2113]! } + public var Settings_SetUsername: String { return self._s[2115]! } + public var Group_Location_ChangeLocation: String { return self._s[2116]! } + public var Notification_GroupInviterSelf: String { return self._s[2117]! } + public var InstantPage_TapToOpenLink: String { return self._s[2118]! } + public func Notification_ChannelInviter(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2119]!, self._r[2119]!, [_0]) + } + public var Watch_Suggestion_TalkLater: String { return self._s[2120]! } + public var SecretChat_Title: String { return self._s[2121]! } + public var Group_UpgradeNoticeText1: String { return self._s[2122]! } + public var AuthSessions_Title: String { return self._s[2123]! } + public func TextFormat_AddLinkText(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2124]!, self._r[2124]!, [_0]) + } + public var PhotoEditor_CropAuto: String { return self._s[2125]! } + public var Channel_About_Title: String { return self._s[2126]! } + public var Theme_ThemeChanged: String { return self._s[2127]! } + public var FastTwoStepSetup_EmailHelp: String { return self._s[2128]! } + public func Conversation_Bytes(_ _0: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2130]!, self._r[2130]!, ["\(_0)"]) + } + public var VoiceOver_MessageContextReport: String { return self._s[2131]! } + public var Conversation_PinMessageAlert_OnlyPin: String { return self._s[2133]! } + public var Group_Setup_HistoryVisibleHelp: String { return self._s[2134]! } + public func PUSH_MESSAGE_GIF(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2135]!, self._r[2135]!, [_1]) + } public func SharedMedia_SearchNoResultsDescription(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2042]!, self._r[2042]!, [_0]) - } - public func TwoStepAuth_RecoveryEmailUnavailable(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2043]!, self._r[2043]!, [_0]) - } - public var Privacy_PaymentsClearInfoHelp: String { return self._s[2044]! } - public var Presence_online: String { return self._s[2047]! } - public var PasscodeSettings_Title: String { return self._s[2048]! } - public var Passport_Identity_ExpiryDatePlaceholder: String { return self._s[2049]! } - public var Web_OpenExternal: String { return self._s[2050]! } - public var AutoDownloadSettings_AutoDownload: String { return self._s[2052]! } - public var Channel_OwnershipTransfer_EnterPasswordText: String { return self._s[2053]! } - public var LocalGroup_Title: String { return self._s[2054]! } - public func AutoNightTheme_AutomaticHelp(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2055]!, self._r[2055]!, [_0]) - } - public var FastTwoStepSetup_PasswordConfirmationPlaceholder: String { return self._s[2056]! } - public var Map_YouAreHere: String { return self._s[2057]! } - public func AuthSessions_Message(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2058]!, self._r[2058]!, [_0]) - } - public func ChatList_DeleteChatConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2059]!, self._r[2059]!, [_0]) - } - public var PrivacyLastSeenSettings_AlwaysShareWith: String { return self._s[2060]! } - public var Target_InviteToGroupErrorAlreadyInvited: String { return self._s[2061]! } - public func AuthSessions_AppUnofficial(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2062]!, self._r[2062]!, [_0]) - } - public func DialogList_LiveLocationSharingTo(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2063]!, self._r[2063]!, [_0]) - } - public var SocksProxySetup_Username: String { return self._s[2064]! } - public var Bot_Start: String { return self._s[2065]! } - public func Channel_AdminLog_EmptyFilterQueryText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2066]!, self._r[2066]!, [_0]) - } - public func Channel_AdminLog_MessagePinned(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2067]!, self._r[2067]!, [_0]) - } - public var Contacts_SortByPresence: String { return self._s[2068]! } - public var AccentColor_Title: String { return self._s[2070]! } - public var Conversation_DiscardVoiceMessageTitle: String { return self._s[2071]! } - public func PUSH_CHAT_CREATED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2072]!, self._r[2072]!, [_1, _2]) - } - public func PrivacySettings_LastSeenContactsMinus(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2073]!, self._r[2073]!, [_0]) - } - public func Channel_AdminLog_MessageChangedLinkedGroup(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2074]!, self._r[2074]!, [_1, _2]) - } - public var Passport_Email_EnterOtherEmail: String { return self._s[2075]! } - public var Login_InfoAvatarPhoto: String { return self._s[2076]! } - public var Privacy_PaymentsClear_ShippingInfo: String { return self._s[2077]! } - public var Tour_Title4: String { return self._s[2078]! } - public var Passport_Identity_Translation: String { return self._s[2079]! } - public var SettingsSearch_Synonyms_Notifications_ContactJoined: String { return self._s[2080]! } - public var Login_TermsOfServiceLabel: String { return self._s[2082]! } - public var Passport_Language_it: String { return self._s[2083]! } - public var KeyCommand_JumpToNextUnreadChat: String { return self._s[2084]! } - public var Passport_Identity_SelfieHelp: String { return self._s[2085]! } - public var Conversation_ClearAll: String { return self._s[2087]! } - public var Wallet_Send_UninitializedText: String { return self._s[2089]! } - public var Channel_OwnershipTransfer_Title: String { return self._s[2090]! } - public var TwoStepAuth_FloodError: String { return self._s[2091]! } - public func PUSH_CHANNEL_MESSAGE_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2092]!, self._r[2092]!, [_1]) - } - public var Paint_Delete: String { return self._s[2093]! } - public func Wallet_Sent_Text(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2094]!, self._r[2094]!, [_0]) - } - public var Privacy_AddNewPeer: String { return self._s[2095]! } - public func Channel_AdminLog_MessageRank(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2096]!, self._r[2096]!, [_1]) - } - public var LogoutOptions_SetPasscodeText: String { return self._s[2097]! } - public func Passport_AcceptHelp(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2098]!, self._r[2098]!, [_1, _2]) - } - public var Message_PinnedAudioMessage: String { return self._s[2099]! } - public func Watch_Time_ShortTodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2100]!, self._r[2100]!, [_0]) - } - public var Notification_Mute1hMin: String { return self._s[2101]! } - public var Notifications_GroupNotificationsSound: String { return self._s[2102]! } - public var Wallet_Month_GenNovember: String { return self._s[2103]! } - public var SocksProxySetup_ShareProxyList: String { return self._s[2104]! } - public var Conversation_MessageEditedLabel: String { return self._s[2105]! } - public func ClearCache_Success(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2106]!, self._r[2106]!, [_0, _1]) - } - public var Notification_Exceptions_AlwaysOff: String { return self._s[2107]! } - public var Notification_Exceptions_NewException_MessagePreviewHeader: String { return self._s[2108]! } - public func Channel_AdminLog_MessageAdmin(_ _0: String, _ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2109]!, self._r[2109]!, [_0, _1, _2]) - } - public var NetworkUsageSettings_ResetStats: String { return self._s[2110]! } - public func PUSH_MESSAGE_GEOLIVE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2111]!, self._r[2111]!, [_1]) - } - public var AccessDenied_LocationTracking: String { return self._s[2112]! } - public var Month_GenOctober: String { return self._s[2113]! } - public var GroupInfo_InviteLink_RevokeAlert_Revoke: String { return self._s[2114]! } - public var EnterPasscode_EnterPasscode: String { return self._s[2115]! } - public var MediaPicker_TimerTooltip: String { return self._s[2117]! } - public var SharedMedia_TitleAll: String { return self._s[2118]! } - public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsExceptions: String { return self._s[2121]! } - public var Conversation_RestrictedMedia: String { return self._s[2122]! } - public var AccessDenied_PhotosRestricted: String { return self._s[2123]! } - public var Privacy_Forwards_WhoCanForward: String { return self._s[2125]! } - public var ChangePhoneNumberCode_Called: String { return self._s[2126]! } - public func Notification_PinnedDocumentMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2127]!, self._r[2127]!, [_0]) - } - public var Conversation_SavedMessages: String { return self._s[2130]! } - public var Your_cards_expiration_month_is_invalid: String { return self._s[2132]! } - public var FastTwoStepSetup_PasswordPlaceholder: String { return self._s[2133]! } - public func Target_ShareGameConfirmationGroup(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2135]!, self._r[2135]!, [_0]) - } - public var VoiceOver_Chat_YourMessage: String { return self._s[2136]! } - public func VoiceOver_Chat_Title(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[2137]!, self._r[2137]!, [_0]) } - public var ReportPeer_AlertSuccess: String { return self._s[2138]! } - public var PhotoEditor_CropAspectRatioOriginal: String { return self._s[2139]! } - public func InstantPage_RelatedArticleAuthorAndDateTitle(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2140]!, self._r[2140]!, [_1, _2]) + public func TwoStepAuth_RecoveryEmailUnavailable(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2138]!, self._r[2138]!, [_0]) } - public var Checkout_PasswordEntry_Title: String { return self._s[2141]! } - public var PhotoEditor_FadeTool: String { return self._s[2142]! } - public var Privacy_ContactsReset: String { return self._s[2143]! } - public func Channel_AdminLog_MessageRestrictedUntil(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2145]!, self._r[2145]!, [_0]) + public var Privacy_PaymentsClearInfoHelp: String { return self._s[2139]! } + public var PeopleNearby_DiscoverDescription: String { return self._s[2141]! } + public var Presence_online: String { return self._s[2143]! } + public var PasscodeSettings_Title: String { return self._s[2144]! } + public var Passport_Identity_ExpiryDatePlaceholder: String { return self._s[2145]! } + public var Web_OpenExternal: String { return self._s[2146]! } + public var AutoDownloadSettings_AutoDownload: String { return self._s[2148]! } + public var Channel_OwnershipTransfer_EnterPasswordText: String { return self._s[2149]! } + public var LocalGroup_Title: String { return self._s[2150]! } + public func AutoNightTheme_AutomaticHelp(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2151]!, self._r[2151]!, [_0]) } - public var Message_PinnedVideoMessage: String { return self._s[2146]! } - public var ChatList_Mute: String { return self._s[2147]! } - public func Wallet_Time_PreciseDate_m5(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2148]!, self._r[2148]!, [_1, _2, _3]) + public var FastTwoStepSetup_PasswordConfirmationPlaceholder: String { return self._s[2152]! } + public var Conversation_StopQuizConfirmation: String { return self._s[2153]! } + public var Map_YouAreHere: String { return self._s[2154]! } + public func AuthSessions_Message(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2155]!, self._r[2155]!, [_0]) } - public var Permissions_CellularDataText_v0: String { return self._s[2149]! } - public var ShareMenu_SelectChats: String { return self._s[2152]! } - public var ChatList_Context_Unarchive: String { return self._s[2153]! } - public var MusicPlayer_VoiceNote: String { return self._s[2154]! } - public var Conversation_RestrictedText: String { return self._s[2155]! } - public var SettingsSearch_Synonyms_Privacy_Data_DeleteDrafts: String { return self._s[2156]! } - public var Wallet_Month_GenApril: String { return self._s[2157]! } - public var Wallet_Month_ShortMarch: String { return self._s[2158]! } - public var TwoStepAuth_DisableSuccess: String { return self._s[2159]! } - public var Cache_Videos: String { return self._s[2160]! } - public var PrivacySettings_PhoneNumber: String { return self._s[2161]! } - public var Wallet_Month_GenFebruary: String { return self._s[2162]! } - public var FeatureDisabled_Oops: String { return self._s[2164]! } - public var Passport_Address_PostcodePlaceholder: String { return self._s[2165]! } - public func AddContact_StatusSuccess(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2166]!, self._r[2166]!, [_0]) + public func ChatList_DeleteChatConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2156]!, self._r[2156]!, [_0]) } - public var Stickers_GroupStickersHelp: String { return self._s[2167]! } - public var GroupPermission_NoSendPolls: String { return self._s[2168]! } - public var Wallet_Qr_ScanCode: String { return self._s[2169]! } - public var Message_VideoExpired: String { return self._s[2171]! } - public var Notifications_Badge: String { return self._s[2172]! } - public var GroupInfo_GroupHistoryVisible: String { return self._s[2173]! } - public var Wallet_Receive_AddressCopied: String { return self._s[2174]! } - public var CreatePoll_OptionPlaceholder: String { return self._s[2175]! } - public var Username_InvalidTooShort: String { return self._s[2176]! } - public var EnterPasscode_EnterNewPasscodeChange: String { return self._s[2177]! } - public var Channel_AdminLog_PinMessages: String { return self._s[2178]! } - public var ArchivedChats_IntroTitle3: String { return self._s[2179]! } - public func Notification_MessageLifetimeRemoved(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2180]!, self._r[2180]!, [_1]) + public var Theme_Context_ChangeColors: String { return self._s[2157]! } + public var PrivacyLastSeenSettings_AlwaysShareWith: String { return self._s[2158]! } + public var Target_InviteToGroupErrorAlreadyInvited: String { return self._s[2159]! } + public func AuthSessions_AppUnofficial(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2160]!, self._r[2160]!, [_0]) } - public var Permissions_SiriAllowInSettings_v0: String { return self._s[2181]! } - public var Conversation_DefaultRestrictedText: String { return self._s[2182]! } - public var SharedMedia_CategoryDocs: String { return self._s[2185]! } - public func PUSH_MESSAGE_CONTACT(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2186]!, self._r[2186]!, [_1]) + public func DialogList_LiveLocationSharingTo(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2161]!, self._r[2161]!, [_0]) } - public var Wallet_Send_UninitializedTitle: String { return self._s[2187]! } - public var Privacy_Forwards_NeverLink: String { return self._s[2189]! } - public func Notification_MessageLifetimeChangedOutgoing(_ _1: String) -> (String, [(Int, NSRange)]) { + public var SocksProxySetup_Username: String { return self._s[2162]! } + public var Bot_Start: String { return self._s[2163]! } + public func Channel_AdminLog_EmptyFilterQueryText(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2164]!, self._r[2164]!, [_0]) + } + public func Channel_AdminLog_MessagePinned(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2165]!, self._r[2165]!, [_0]) + } + public var Contacts_SortByPresence: String { return self._s[2166]! } + public var AccentColor_Title: String { return self._s[2168]! } + public var Conversation_DiscardVoiceMessageTitle: String { return self._s[2169]! } + public func PUSH_CHAT_CREATED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2170]!, self._r[2170]!, [_1, _2]) + } + public func PrivacySettings_LastSeenContactsMinus(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2171]!, self._r[2171]!, [_0]) + } + public func Channel_AdminLog_MessageChangedLinkedGroup(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2172]!, self._r[2172]!, [_1, _2]) + } + public var Passport_Email_EnterOtherEmail: String { return self._s[2173]! } + public var Login_InfoAvatarPhoto: String { return self._s[2174]! } + public var Privacy_PaymentsClear_ShippingInfo: String { return self._s[2175]! } + public var Tour_Title4: String { return self._s[2176]! } + public var Passport_Identity_Translation: String { return self._s[2177]! } + public var SettingsSearch_Synonyms_Notifications_ContactJoined: String { return self._s[2178]! } + public var Login_TermsOfServiceLabel: String { return self._s[2180]! } + public var Passport_Language_it: String { return self._s[2181]! } + public var KeyCommand_JumpToNextUnreadChat: String { return self._s[2182]! } + public var Passport_Identity_SelfieHelp: String { return self._s[2183]! } + public var Conversation_ClearAll: String { return self._s[2185]! } + public var Wallet_Send_UninitializedText: String { return self._s[2187]! } + public var Channel_OwnershipTransfer_Title: String { return self._s[2188]! } + public var TwoStepAuth_FloodError: String { return self._s[2189]! } + public func PUSH_CHANNEL_MESSAGE_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[2190]!, self._r[2190]!, [_1]) } - public var CheckoutInfo_ErrorShippingNotAvailable: String { return self._s[2191]! } - public func Time_MonthOfYear_m12(_ _0: String) -> (String, [(Int, NSRange)]) { + public var Paint_Delete: String { return self._s[2191]! } + public func Wallet_Sent_Text(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[2192]!, self._r[2192]!, [_0]) } - public var ChatSettings_PrivateChats: String { return self._s[2193]! } - public var SettingsSearch_Synonyms_EditProfile_Logout: String { return self._s[2194]! } - public var Conversation_PrivateMessageLinkCopied: String { return self._s[2195]! } - public var Channel_UpdatePhotoItem: String { return self._s[2196]! } - public var GroupInfo_LeftStatus: String { return self._s[2197]! } - public var Watch_MessageView_Forward: String { return self._s[2199]! } - public var ReportPeer_ReasonChildAbuse: String { return self._s[2200]! } - public var Cache_ClearEmpty: String { return self._s[2202]! } - public var Localization_LanguageName: String { return self._s[2203]! } - public var Wallet_AccessDenied_Title: String { return self._s[2204]! } - public var WebSearch_GIFs: String { return self._s[2205]! } - public var Notifications_DisplayNamesOnLockScreenInfoWithLink: String { return self._s[2206]! } - public var Wallet_AccessDenied_Settings: String { return self._s[2207]! } - public var Username_InvalidStartsWithNumber: String { return self._s[2208]! } - public var Common_Back: String { return self._s[2209]! } - public var GroupInfo_Permissions_EditingDisabled: String { return self._s[2210]! } - public var Passport_Identity_DateOfBirthPlaceholder: String { return self._s[2211]! } - public var Wallet_Send_Send: String { return self._s[2212]! } + public var Privacy_AddNewPeer: String { return self._s[2193]! } + public func Channel_AdminLog_MessageRank(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2194]!, self._r[2194]!, [_1]) + } + public var LogoutOptions_SetPasscodeText: String { return self._s[2195]! } + public func Passport_AcceptHelp(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2196]!, self._r[2196]!, [_1, _2]) + } + public var Message_PinnedAudioMessage: String { return self._s[2197]! } + public func Watch_Time_ShortTodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2198]!, self._r[2198]!, [_0]) + } + public var Notification_Mute1hMin: String { return self._s[2199]! } + public var Notifications_GroupNotificationsSound: String { return self._s[2200]! } + public var Wallet_Month_GenNovember: String { return self._s[2201]! } + public var SocksProxySetup_ShareProxyList: String { return self._s[2202]! } + public var Conversation_MessageEditedLabel: String { return self._s[2203]! } + public func ClearCache_Success(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2204]!, self._r[2204]!, [_0, _1]) + } + public var Notification_Exceptions_AlwaysOff: String { return self._s[2205]! } + public var Notification_Exceptions_NewException_MessagePreviewHeader: String { return self._s[2206]! } + public func Channel_AdminLog_MessageAdmin(_ _0: String, _ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2207]!, self._r[2207]!, [_0, _1, _2]) + } + public var NetworkUsageSettings_ResetStats: String { return self._s[2208]! } + public func PUSH_MESSAGE_GEOLIVE(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2209]!, self._r[2209]!, [_1]) + } + public var AccessDenied_LocationTracking: String { return self._s[2210]! } + public var Month_GenOctober: String { return self._s[2211]! } + public var GroupInfo_InviteLink_RevokeAlert_Revoke: String { return self._s[2212]! } + public var EnterPasscode_EnterPasscode: String { return self._s[2213]! } + public var MediaPicker_TimerTooltip: String { return self._s[2215]! } + public var SharedMedia_TitleAll: String { return self._s[2216]! } + public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsExceptions: String { return self._s[2219]! } + public var Conversation_RestrictedMedia: String { return self._s[2220]! } + public var AccessDenied_PhotosRestricted: String { return self._s[2221]! } + public var Privacy_Forwards_WhoCanForward: String { return self._s[2223]! } + public var ChangePhoneNumberCode_Called: String { return self._s[2224]! } + public func Notification_PinnedDocumentMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2225]!, self._r[2225]!, [_0]) + } + public var Conversation_SavedMessages: String { return self._s[2228]! } + public var Your_cards_expiration_month_is_invalid: String { return self._s[2230]! } + public var FastTwoStepSetup_PasswordPlaceholder: String { return self._s[2231]! } + public func Target_ShareGameConfirmationGroup(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2233]!, self._r[2233]!, [_0]) + } + public var VoiceOver_Chat_YourMessage: String { return self._s[2234]! } + public func VoiceOver_Chat_Title(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2235]!, self._r[2235]!, [_0]) + } + public var ReportPeer_AlertSuccess: String { return self._s[2236]! } + public var PhotoEditor_CropAspectRatioOriginal: String { return self._s[2237]! } + public func InstantPage_RelatedArticleAuthorAndDateTitle(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2238]!, self._r[2238]!, [_1, _2]) + } + public var Checkout_PasswordEntry_Title: String { return self._s[2239]! } + public var PhotoEditor_FadeTool: String { return self._s[2240]! } + public var Privacy_ContactsReset: String { return self._s[2241]! } + public func Channel_AdminLog_MessageRestrictedUntil(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2243]!, self._r[2243]!, [_0]) + } + public var Message_PinnedVideoMessage: String { return self._s[2244]! } + public var ChatList_Mute: String { return self._s[2245]! } + public func Wallet_Time_PreciseDate_m5(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2246]!, self._r[2246]!, [_1, _2, _3]) + } + public var Permissions_CellularDataText_v0: String { return self._s[2247]! } + public var Conversation_PinnedQuiz: String { return self._s[2249]! } + public var ShareMenu_SelectChats: String { return self._s[2251]! } + public var ChatList_Context_Unarchive: String { return self._s[2252]! } + public var MusicPlayer_VoiceNote: String { return self._s[2253]! } + public var Conversation_RestrictedText: String { return self._s[2254]! } + public var SettingsSearch_Synonyms_Privacy_Data_DeleteDrafts: String { return self._s[2255]! } + public var Wallet_Month_GenApril: String { return self._s[2256]! } + public var Wallet_Month_ShortMarch: String { return self._s[2257]! } + public var TwoStepAuth_DisableSuccess: String { return self._s[2258]! } + public var Cache_Videos: String { return self._s[2259]! } + public var PrivacySettings_PhoneNumber: String { return self._s[2260]! } + public var Wallet_Month_GenFebruary: String { return self._s[2261]! } + public var FeatureDisabled_Oops: String { return self._s[2263]! } + public var Passport_Address_PostcodePlaceholder: String { return self._s[2264]! } + public func AddContact_StatusSuccess(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2265]!, self._r[2265]!, [_0]) + } + public var Stickers_GroupStickersHelp: String { return self._s[2267]! } + public var GroupPermission_NoSendPolls: String { return self._s[2268]! } + public var Wallet_Qr_ScanCode: String { return self._s[2269]! } + public var Message_VideoExpired: String { return self._s[2271]! } + public var GroupInfo_GroupHistoryVisible: String { return self._s[2272]! } + public var Notifications_Badge: String { return self._s[2273]! } + public var Wallet_Receive_AddressCopied: String { return self._s[2274]! } + public var CreatePoll_OptionPlaceholder: String { return self._s[2275]! } + public var Username_InvalidTooShort: String { return self._s[2276]! } + public var EnterPasscode_EnterNewPasscodeChange: String { return self._s[2277]! } + public var Channel_AdminLog_PinMessages: String { return self._s[2278]! } + public var ArchivedChats_IntroTitle3: String { return self._s[2279]! } + public func Notification_MessageLifetimeRemoved(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2280]!, self._r[2280]!, [_1]) + } + public var Permissions_SiriAllowInSettings_v0: String { return self._s[2281]! } + public var Conversation_DefaultRestrictedText: String { return self._s[2282]! } + public var SharedMedia_CategoryDocs: String { return self._s[2285]! } + public func PUSH_MESSAGE_CONTACT(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2286]!, self._r[2286]!, [_1]) + } + public var Wallet_Send_UninitializedTitle: String { return self._s[2287]! } + public var StickerPackActionInfo_ArchivedTitle: String { return self._s[2288]! } + public var Privacy_Forwards_NeverLink: String { return self._s[2290]! } + public func Notification_MessageLifetimeChangedOutgoing(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2291]!, self._r[2291]!, [_1]) + } + public var CheckoutInfo_ErrorShippingNotAvailable: String { return self._s[2292]! } + public func Time_MonthOfYear_m12(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2293]!, self._r[2293]!, [_0]) + } + public var ChatSettings_PrivateChats: String { return self._s[2294]! } + public var SettingsSearch_Synonyms_EditProfile_Logout: String { return self._s[2295]! } + public var Conversation_PrivateMessageLinkCopied: String { return self._s[2296]! } + public var Channel_UpdatePhotoItem: String { return self._s[2297]! } + public var GroupInfo_LeftStatus: String { return self._s[2298]! } + public var Watch_MessageView_Forward: String { return self._s[2300]! } + public var ReportPeer_ReasonChildAbuse: String { return self._s[2301]! } + public var Cache_ClearEmpty: String { return self._s[2303]! } + public var Localization_LanguageName: String { return self._s[2304]! } + public var Wallet_AccessDenied_Title: String { return self._s[2305]! } + public var WebSearch_GIFs: String { return self._s[2306]! } + public var Notifications_DisplayNamesOnLockScreenInfoWithLink: String { return self._s[2307]! } + public var Wallet_AccessDenied_Settings: String { return self._s[2308]! } + public var Username_InvalidStartsWithNumber: String { return self._s[2309]! } + public var Common_Back: String { return self._s[2310]! } + public var GroupInfo_Permissions_EditingDisabled: String { return self._s[2311]! } + public var Passport_Identity_DateOfBirthPlaceholder: String { return self._s[2312]! } + public var Wallet_Send_Send: String { return self._s[2313]! } public func PUSH_CHANNEL_MESSAGE_STICKER(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2213]!, self._r[2213]!, [_1, _2]) + return formatWithArgumentRanges(self._s[2315]!, self._r[2315]!, [_1, _2]) } - public var Wallet_Info_RefreshErrorTitle: String { return self._s[2214]! } - public var Wallet_Month_GenJune: String { return self._s[2215]! } - public var Passport_Email_Help: String { return self._s[2216]! } - public var Watch_Conversation_Reply: String { return self._s[2218]! } - public var Conversation_EditingMessageMediaChange: String { return self._s[2221]! } - public var Passport_Identity_IssueDatePlaceholder: String { return self._s[2222]! } - public var Channel_BanUser_Unban: String { return self._s[2224]! } - public var Channel_EditAdmin_PermissionPostMessages: String { return self._s[2225]! } - public var Group_Username_CreatePublicLinkHelp: String { return self._s[2226]! } - public var TwoStepAuth_ConfirmEmailCodePlaceholder: String { return self._s[2228]! } - public var Wallet_Send_AddressHeader: String { return self._s[2229]! } - public var Passport_Identity_Name: String { return self._s[2230]! } + public var Wallet_Info_RefreshErrorTitle: String { return self._s[2316]! } + public var Wallet_Month_GenJune: String { return self._s[2317]! } + public var Passport_Email_Help: String { return self._s[2318]! } + public var Watch_Conversation_Reply: String { return self._s[2320]! } + public var Conversation_EditingMessageMediaChange: String { return self._s[2323]! } + public var Passport_Identity_IssueDatePlaceholder: String { return self._s[2324]! } + public var Channel_BanUser_Unban: String { return self._s[2326]! } + public var Channel_EditAdmin_PermissionPostMessages: String { return self._s[2327]! } + public var Group_Username_CreatePublicLinkHelp: String { return self._s[2328]! } + public var TwoStepAuth_ConfirmEmailCodePlaceholder: String { return self._s[2330]! } + public var Wallet_Send_AddressHeader: String { return self._s[2331]! } + public var Passport_Identity_Name: String { return self._s[2332]! } public func Channel_DiscussionGroup_HeaderGroupSet(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2231]!, self._r[2231]!, [_0]) - } - public var GroupRemoved_ViewUserInfo: String { return self._s[2232]! } - public var Conversation_BlockUser: String { return self._s[2233]! } - public var Month_GenJanuary: String { return self._s[2234]! } - public var ChatSettings_TextSize: String { return self._s[2235]! } - public var Notification_PassportValuePhone: String { return self._s[2236]! } - public var Passport_Language_ne: String { return self._s[2237]! } - public var Notification_CallBack: String { return self._s[2238]! } - public var Wallet_SecureStorageReset_BiometryTouchId: String { return self._s[2239]! } - public var TwoStepAuth_EmailHelp: String { return self._s[2240]! } - public func Time_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2241]!, self._r[2241]!, [_0]) - } - public var Channel_Info_Management: String { return self._s[2242]! } - public var Passport_FieldIdentityUploadHelp: String { return self._s[2243]! } - public var Stickers_FrequentlyUsed: String { return self._s[2244]! } - public var Channel_BanUser_PermissionSendMessages: String { return self._s[2245]! } - public var Passport_Address_OneOfTypeUtilityBill: String { return self._s[2247]! } - public func LOCAL_CHANNEL_MESSAGE_FWDS(_ _1: String, _ _2: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2248]!, self._r[2248]!, [_1, "\(_2)"]) - } - public var TwoFactorSetup_Password_Title: String { return self._s[2249]! } - public var Passport_Address_EditResidentialAddress: String { return self._s[2250]! } - public var PrivacyPolicy_DeclineTitle: String { return self._s[2251]! } - public var CreatePoll_TextHeader: String { return self._s[2252]! } - public func Checkout_SavePasswordTimeoutAndTouchId(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2253]!, self._r[2253]!, [_0]) - } - public var PhotoEditor_QualityMedium: String { return self._s[2254]! } - public var InfoPlist_NSMicrophoneUsageDescription: String { return self._s[2255]! } - public var Conversation_StatusKickedFromChannel: String { return self._s[2257]! } - public var CheckoutInfo_ReceiverInfoName: String { return self._s[2258]! } - public var Group_ErrorSendRestrictedStickers: String { return self._s[2259]! } - public func Conversation_RestrictedInlineTimed(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2260]!, self._r[2260]!, [_0]) - } - public func Channel_AdminLog_MessageTransferedName(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2261]!, self._r[2261]!, [_1]) - } - public var LogoutOptions_LogOutWalletInfo: String { return self._s[2262]! } - public var TwoFactorSetup_Email_SkipConfirmationTitle: String { return self._s[2263]! } - public var Conversation_LinkDialogOpen: String { return self._s[2265]! } - public var TwoFactorSetup_Hint_Title: String { return self._s[2266]! } - public var VoiceOver_Chat_PollNoVotes: String { return self._s[2267]! } - public var Settings_Username: String { return self._s[2269]! } - public var Conversation_Block: String { return self._s[2271]! } - public var Wallpaper_Wallpaper: String { return self._s[2272]! } - public var SocksProxySetup_UseProxy: String { return self._s[2274]! } - public var Wallet_Send_Confirmation: String { return self._s[2275]! } - public var EditTheme_UploadEditedTheme: String { return self._s[2276]! } - public var UserInfo_ShareMyContactInfo: String { return self._s[2277]! } - public var MessageTimer_Forever: String { return self._s[2278]! } - public var Privacy_Calls_WhoCanCallMe: String { return self._s[2279]! } - public var PhotoEditor_DiscardChanges: String { return self._s[2280]! } - public var AuthSessions_TerminateOtherSessionsHelp: String { return self._s[2281]! } - public var Passport_Language_da: String { return self._s[2282]! } - public var SocksProxySetup_PortPlaceholder: String { return self._s[2283]! } - public func SecretGIF_NotViewedYet(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2284]!, self._r[2284]!, [_0]) - } - public var Passport_Address_EditPassportRegistration: String { return self._s[2285]! } - public func Channel_AdminLog_MessageChangedGroupAbout(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2287]!, self._r[2287]!, [_0]) - } - public var Passport_Identity_ResidenceCountryPlaceholder: String { return self._s[2289]! } - public var Conversation_SearchByName_Prefix: String { return self._s[2290]! } - public var Conversation_PinnedPoll: String { return self._s[2291]! } - public var Conversation_EmptyGifPanelPlaceholder: String { return self._s[2292]! } - public func PUSH_ENCRYPTION_ACCEPT(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2293]!, self._r[2293]!, [_1]) - } - public var WallpaperSearch_ColorPurple: String { return self._s[2294]! } - public var Cache_ByPeerHeader: String { return self._s[2295]! } - public func Conversation_EncryptedPlaceholderTitleIncoming(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2296]!, self._r[2296]!, [_0]) - } - public var ChatSettings_AutoDownloadDocuments: String { return self._s[2297]! } - public var Appearance_ThemePreview_Chat_3_Text: String { return self._s[2300]! } - public var Wallet_Completed_Title: String { return self._s[2301]! } - public var Notification_PinnedMessage: String { return self._s[2302]! } - public var TwoFactorSetup_EmailVerification_Placeholder: String { return self._s[2303]! } - public var VoiceOver_Chat_RecordModeVideoMessage: String { return self._s[2305]! } - public var Contacts_SortBy: String { return self._s[2306]! } - public func PUSH_CHANNEL_MESSAGE_NOTEXT(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2307]!, self._r[2307]!, [_1]) - } - public var Appearance_ColorThemeNight: String { return self._s[2309]! } - public func PUSH_MESSAGE_GAME(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2310]!, self._r[2310]!, [_1, _2]) - } - public var Call_EncryptionKey_Title: String { return self._s[2311]! } - public var Watch_UserInfo_Service: String { return self._s[2312]! } - public var SettingsSearch_Synonyms_Data_SaveEditedPhotos: String { return self._s[2314]! } - public var Conversation_Unpin: String { return self._s[2316]! } - public var CancelResetAccount_Title: String { return self._s[2317]! } - public var Map_LiveLocationFor15Minutes: String { return self._s[2318]! } - public func Time_PreciseDate_m8(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2320]!, self._r[2320]!, [_1, _2, _3]) - } - public var Group_Members_AddMemberBotErrorNotAllowed: String { return self._s[2321]! } - public var CallSettings_Title: String { return self._s[2322]! } - public var SettingsSearch_Synonyms_Appearance_ChatBackground: String { return self._s[2323]! } - public var PasscodeSettings_EncryptDataHelp: String { return self._s[2325]! } - public var AutoDownloadSettings_Contacts: String { return self._s[2326]! } - public func Channel_AdminLog_MessageRankName(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2327]!, self._r[2327]!, [_1, _2]) - } - public var Passport_Identity_DocumentDetails: String { return self._s[2328]! } - public var LoginPassword_PasswordHelp: String { return self._s[2329]! } - public var SettingsSearch_Synonyms_Data_AutoDownloadUsingWifi: String { return self._s[2330]! } - public var PrivacyLastSeenSettings_CustomShareSettings_Delete: String { return self._s[2331]! } - public var Checkout_TotalPaidAmount: String { return self._s[2332]! } - public func FileSize_KB(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[2333]!, self._r[2333]!, [_0]) } - public var PasscodeSettings_ChangePasscode: String { return self._s[2334]! } - public var Conversation_SecretLinkPreviewAlert: String { return self._s[2336]! } - public var Privacy_SecretChatsLinkPreviews: String { return self._s[2337]! } - public func PUSH_CHANNEL_MESSAGE_DOC(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2338]!, self._r[2338]!, [_1]) + public var GroupRemoved_ViewUserInfo: String { return self._s[2334]! } + public var Conversation_BlockUser: String { return self._s[2335]! } + public var Month_GenJanuary: String { return self._s[2336]! } + public var ChatSettings_TextSize: String { return self._s[2337]! } + public var Notification_PassportValuePhone: String { return self._s[2338]! } + public var MediaPlayer_UnknownArtist: String { return self._s[2339]! } + public var Passport_Language_ne: String { return self._s[2340]! } + public var Notification_CallBack: String { return self._s[2341]! } + public var Wallet_SecureStorageReset_BiometryTouchId: String { return self._s[2342]! } + public var TwoStepAuth_EmailHelp: String { return self._s[2343]! } + public func Time_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2344]!, self._r[2344]!, [_0]) } - public var VoiceOver_Chat_ReplyToYourMessage: String { return self._s[2339]! } - public var Contacts_InviteFriends: String { return self._s[2341]! } - public var Map_ChooseLocationTitle: String { return self._s[2342]! } - public var Conversation_StopPoll: String { return self._s[2344]! } - public func WebSearch_SearchNoResultsDescription(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2345]!, self._r[2345]!, [_0]) + public var Channel_Info_Management: String { return self._s[2345]! } + public var Passport_FieldIdentityUploadHelp: String { return self._s[2346]! } + public var Stickers_FrequentlyUsed: String { return self._s[2347]! } + public var Channel_BanUser_PermissionSendMessages: String { return self._s[2348]! } + public var Passport_Address_OneOfTypeUtilityBill: String { return self._s[2350]! } + public func LOCAL_CHANNEL_MESSAGE_FWDS(_ _1: String, _ _2: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2351]!, self._r[2351]!, [_1, "\(_2)"]) } - public var Call_Camera: String { return self._s[2346]! } - public var LogoutOptions_ChangePhoneNumberTitle: String { return self._s[2347]! } - public var AppWallet_Intro_Text: String { return self._s[2348]! } - public var Calls_RatingFeedback: String { return self._s[2349]! } - public var GroupInfo_BroadcastListNamePlaceholder: String { return self._s[2350]! } - public var Wallet_Alert_OK: String { return self._s[2351]! } - public var NotificationsSound_Pulse: String { return self._s[2352]! } - public var Watch_LastSeen_Lately: String { return self._s[2353]! } - public var ReportGroupLocation_Report: String { return self._s[2356]! } - public var Widget_NoUsers: String { return self._s[2357]! } - public var Conversation_UnvotePoll: String { return self._s[2358]! } - public var SettingsSearch_Synonyms_Privacy_ProfilePhoto: String { return self._s[2360]! } - public var Privacy_ProfilePhoto_WhoCanSeeMyPhoto: String { return self._s[2361]! } - public var NotificationsSound_Circles: String { return self._s[2362]! } - public var PrivacyLastSeenSettings_AlwaysShareWith_Title: String { return self._s[2365]! } - public var Wallet_Settings_DeleteWallet: String { return self._s[2366]! } - public var TwoStepAuth_RecoveryCodeExpired: String { return self._s[2367]! } - public var Proxy_TooltipUnavailable: String { return self._s[2368]! } - public var Passport_Identity_CountryPlaceholder: String { return self._s[2370]! } - public var GroupInfo_Permissions_SlowmodeInfo: String { return self._s[2372]! } - public var Conversation_FileDropbox: String { return self._s[2373]! } - public var Notifications_ExceptionsUnmuted: String { return self._s[2374]! } - public var Tour_Text3: String { return self._s[2376]! } - public var Login_ResetAccountProtected_Title: String { return self._s[2378]! } - public var GroupPermission_NoSendMessages: String { return self._s[2379]! } - public var WallpaperSearch_ColorTitle: String { return self._s[2380]! } - public var ChatAdmins_AllMembersAreAdminsOnHelp: String { return self._s[2381]! } - public func Conversation_LiveLocationYouAnd(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2383]!, self._r[2383]!, [_0]) + public var TwoFactorSetup_Password_Title: String { return self._s[2352]! } + public var Passport_Address_EditResidentialAddress: String { return self._s[2353]! } + public var PrivacyPolicy_DeclineTitle: String { return self._s[2354]! } + public var CreatePoll_TextHeader: String { return self._s[2355]! } + public func Checkout_SavePasswordTimeoutAndTouchId(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2356]!, self._r[2356]!, [_0]) } - public var GroupInfo_AddParticipantTitle: String { return self._s[2384]! } - public var Checkout_ShippingOption_Title: String { return self._s[2385]! } - public var ChatSettings_AutoDownloadTitle: String { return self._s[2386]! } - public func DialogList_SingleTypingSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { + public var PhotoEditor_QualityMedium: String { return self._s[2357]! } + public var InfoPlist_NSMicrophoneUsageDescription: String { return self._s[2358]! } + public var Conversation_StatusKickedFromChannel: String { return self._s[2360]! } + public var CheckoutInfo_ReceiverInfoName: String { return self._s[2361]! } + public var Group_ErrorSendRestrictedStickers: String { return self._s[2362]! } + public func Conversation_RestrictedInlineTimed(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2363]!, self._r[2363]!, [_0]) + } + public func Channel_AdminLog_MessageTransferedName(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2364]!, self._r[2364]!, [_1]) + } + public var LogoutOptions_LogOutWalletInfo: String { return self._s[2365]! } + public var TwoFactorSetup_Email_SkipConfirmationTitle: String { return self._s[2366]! } + public var Conversation_LinkDialogOpen: String { return self._s[2368]! } + public var TwoFactorSetup_Hint_Title: String { return self._s[2369]! } + public var VoiceOver_Chat_PollNoVotes: String { return self._s[2370]! } + public var Settings_Username: String { return self._s[2372]! } + public var Conversation_Block: String { return self._s[2374]! } + public var Wallpaper_Wallpaper: String { return self._s[2375]! } + public var SocksProxySetup_UseProxy: String { return self._s[2377]! } + public var Wallet_Send_Confirmation: String { return self._s[2378]! } + public var EditTheme_UploadEditedTheme: String { return self._s[2379]! } + public var UserInfo_ShareMyContactInfo: String { return self._s[2380]! } + public var MessageTimer_Forever: String { return self._s[2381]! } + public var Privacy_Calls_WhoCanCallMe: String { return self._s[2382]! } + public var PhotoEditor_DiscardChanges: String { return self._s[2383]! } + public var AuthSessions_TerminateOtherSessionsHelp: String { return self._s[2384]! } + public var Passport_Language_da: String { return self._s[2385]! } + public var SocksProxySetup_PortPlaceholder: String { return self._s[2386]! } + public func SecretGIF_NotViewedYet(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[2387]!, self._r[2387]!, [_0]) } + public var Passport_Address_EditPassportRegistration: String { return self._s[2388]! } + public func Channel_AdminLog_MessageChangedGroupAbout(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2390]!, self._r[2390]!, [_0]) + } + public var Settings_AddDevice: String { return self._s[2391]! } + public var Passport_Identity_ResidenceCountryPlaceholder: String { return self._s[2393]! } + public var AuthSessions_AddDeviceIntro_Text1: String { return self._s[2394]! } + public var Conversation_SearchByName_Prefix: String { return self._s[2395]! } + public var Conversation_PinnedPoll: String { return self._s[2396]! } + public var AuthSessions_AddDeviceIntro_Text2: String { return self._s[2397]! } + public var Conversation_EmptyGifPanelPlaceholder: String { return self._s[2398]! } + public var AuthSessions_AddDeviceIntro_Text3: String { return self._s[2399]! } + public func PUSH_ENCRYPTION_ACCEPT(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2400]!, self._r[2400]!, [_1]) + } + public var WallpaperSearch_ColorPurple: String { return self._s[2401]! } + public var Cache_ByPeerHeader: String { return self._s[2402]! } + public func Conversation_EncryptedPlaceholderTitleIncoming(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2403]!, self._r[2403]!, [_0]) + } + public var ChatSettings_AutoDownloadDocuments: String { return self._s[2404]! } + public var Appearance_ThemePreview_Chat_3_Text: String { return self._s[2407]! } + public var Wallet_Completed_Title: String { return self._s[2408]! } + public var Notification_PinnedMessage: String { return self._s[2409]! } + public var TwoFactorSetup_EmailVerification_Placeholder: String { return self._s[2410]! } + public var VoiceOver_Chat_RecordModeVideoMessage: String { return self._s[2412]! } + public var Contacts_SortBy: String { return self._s[2413]! } + public func PUSH_CHANNEL_MESSAGE_NOTEXT(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2414]!, self._r[2414]!, [_1]) + } + public var Appearance_ColorThemeNight: String { return self._s[2416]! } + public func PUSH_MESSAGE_GAME(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2417]!, self._r[2417]!, [_1, _2]) + } + public var Call_EncryptionKey_Title: String { return self._s[2418]! } + public var Watch_UserInfo_Service: String { return self._s[2419]! } + public var SettingsSearch_Synonyms_Data_SaveEditedPhotos: String { return self._s[2421]! } + public var Conversation_Unpin: String { return self._s[2423]! } + public var CancelResetAccount_Title: String { return self._s[2424]! } + public var Map_LiveLocationFor15Minutes: String { return self._s[2425]! } + public func Time_PreciseDate_m8(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2427]!, self._r[2427]!, [_1, _2, _3]) + } + public var Group_Members_AddMemberBotErrorNotAllowed: String { return self._s[2428]! } + public var Appearance_BubbleCorners_Title: String { return self._s[2429]! } + public var CallSettings_Title: String { return self._s[2430]! } + public var SettingsSearch_Synonyms_Appearance_ChatBackground: String { return self._s[2431]! } + public var PasscodeSettings_EncryptDataHelp: String { return self._s[2433]! } + public var AutoDownloadSettings_Contacts: String { return self._s[2434]! } + public func Channel_AdminLog_MessageRankName(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2435]!, self._r[2435]!, [_1, _2]) + } + public var Passport_Identity_DocumentDetails: String { return self._s[2436]! } + public var LoginPassword_PasswordHelp: String { return self._s[2437]! } + public var SettingsSearch_Synonyms_Data_AutoDownloadUsingWifi: String { return self._s[2438]! } + public var PrivacyLastSeenSettings_CustomShareSettings_Delete: String { return self._s[2439]! } + public var ChatContextMenu_TextSelectionTip: String { return self._s[2440]! } + public var Checkout_TotalPaidAmount: String { return self._s[2441]! } + public func FileSize_KB(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2442]!, self._r[2442]!, [_0]) + } + public var ChatState_Updating: String { return self._s[2443]! } + public var PasscodeSettings_ChangePasscode: String { return self._s[2444]! } + public var Conversation_SecretLinkPreviewAlert: String { return self._s[2446]! } + public var Privacy_SecretChatsLinkPreviews: String { return self._s[2447]! } + public func PUSH_CHANNEL_MESSAGE_DOC(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2448]!, self._r[2448]!, [_1]) + } + public var VoiceOver_Chat_ReplyToYourMessage: String { return self._s[2449]! } + public var Contacts_InviteFriends: String { return self._s[2451]! } + public var Map_ChooseLocationTitle: String { return self._s[2452]! } + public var Conversation_StopPoll: String { return self._s[2454]! } + public func WebSearch_SearchNoResultsDescription(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2455]!, self._r[2455]!, [_0]) + } + public var Call_Camera: String { return self._s[2456]! } + public var LogoutOptions_ChangePhoneNumberTitle: String { return self._s[2457]! } + public var AppWallet_Intro_Text: String { return self._s[2458]! } + public var Appearance_BubbleCornersSetting: String { return self._s[2459]! } + public var Calls_RatingFeedback: String { return self._s[2460]! } + public var GroupInfo_BroadcastListNamePlaceholder: String { return self._s[2462]! } + public var Wallet_Alert_OK: String { return self._s[2463]! } + public var NotificationsSound_Pulse: String { return self._s[2464]! } + public var Watch_LastSeen_Lately: String { return self._s[2465]! } + public var ReportGroupLocation_Report: String { return self._s[2468]! } + public var Widget_NoUsers: String { return self._s[2469]! } + public var Conversation_UnvotePoll: String { return self._s[2470]! } + public var SettingsSearch_Synonyms_Privacy_ProfilePhoto: String { return self._s[2472]! } + public var Privacy_ProfilePhoto_WhoCanSeeMyPhoto: String { return self._s[2473]! } + public var NotificationsSound_Circles: String { return self._s[2474]! } + public var PrivacyLastSeenSettings_AlwaysShareWith_Title: String { return self._s[2477]! } + public var Wallet_Settings_DeleteWallet: String { return self._s[2478]! } + public var TwoStepAuth_RecoveryCodeExpired: String { return self._s[2479]! } + public var Proxy_TooltipUnavailable: String { return self._s[2480]! } + public var Passport_Identity_CountryPlaceholder: String { return self._s[2482]! } + public var GroupInfo_Permissions_SlowmodeInfo: String { return self._s[2484]! } + public var Conversation_FileDropbox: String { return self._s[2485]! } + public var Notifications_ExceptionsUnmuted: String { return self._s[2486]! } + public var Tour_Text3: String { return self._s[2488]! } + public var Login_ResetAccountProtected_Title: String { return self._s[2490]! } + public var GroupPermission_NoSendMessages: String { return self._s[2491]! } + public var WallpaperSearch_ColorTitle: String { return self._s[2492]! } + public var ChatAdmins_AllMembersAreAdminsOnHelp: String { return self._s[2493]! } + public func Conversation_LiveLocationYouAnd(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2495]!, self._r[2495]!, [_0]) + } + public var GroupInfo_AddParticipantTitle: String { return self._s[2496]! } + public var Checkout_ShippingOption_Title: String { return self._s[2497]! } + public var ChatSettings_AutoDownloadTitle: String { return self._s[2498]! } + public func DialogList_SingleTypingSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2499]!, self._r[2499]!, [_0]) + } public func ChatSettings_AutoDownloadSettings_TypeVideo(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2388]!, self._r[2388]!, [_0]) + return formatWithArgumentRanges(self._s[2500]!, self._r[2500]!, [_0]) } - public var Channel_Management_LabelAdministrator: String { return self._s[2389]! } - public var EditTheme_FileReadError: String { return self._s[2390]! } - public var OwnershipTransfer_ComeBackLater: String { return self._s[2391]! } - public var PrivacyLastSeenSettings_NeverShareWith_Placeholder: String { return self._s[2392]! } - public var AutoDownloadSettings_Photos: String { return self._s[2394]! } - public var Appearance_PreviewIncomingText: String { return self._s[2395]! } - public var ChatList_Context_MarkAllAsRead: String { return self._s[2396]! } - public var ChannelInfo_ConfirmLeave: String { return self._s[2397]! } - public var MediaPicker_MomentsDateRangeSameMonthYearFormat: String { return self._s[2398]! } - public var Passport_Identity_DocumentNumberPlaceholder: String { return self._s[2399]! } - public var Channel_AdminLogFilter_EventsNewMembers: String { return self._s[2400]! } - public var PasscodeSettings_AutoLock_IfAwayFor_5minutes: String { return self._s[2401]! } - public var GroupInfo_SetGroupPhotoStop: String { return self._s[2402]! } - public var Notification_SecretChatScreenshot: String { return self._s[2403]! } - public var AccessDenied_Wallpapers: String { return self._s[2404]! } - public var ChatList_Context_Mute: String { return self._s[2406]! } - public var Passport_Address_City: String { return self._s[2407]! } - public var InfoPlist_NSPhotoLibraryAddUsageDescription: String { return self._s[2408]! } - public var Appearance_ThemeCarouselClassic: String { return self._s[2409]! } - public var SocksProxySetup_SecretPlaceholder: String { return self._s[2410]! } - public var AccessDenied_LocationDisabled: String { return self._s[2411]! } - public var Group_Location_Title: String { return self._s[2412]! } - public var SocksProxySetup_HostnamePlaceholder: String { return self._s[2414]! } - public var GroupInfo_Sound: String { return self._s[2415]! } - public var SettingsSearch_Synonyms_ChatSettings_OpenLinksIn: String { return self._s[2416]! } - public var ChannelInfo_ScamChannelWarning: String { return self._s[2417]! } - public var Stickers_RemoveFromFavorites: String { return self._s[2418]! } - public var Contacts_Title: String { return self._s[2419]! } - public var EditTheme_ThemeTemplateAlertText: String { return self._s[2420]! } - public var Passport_Language_fr: String { return self._s[2421]! } - public var TwoFactorSetup_EmailVerification_Action: String { return self._s[2422]! } - public var Notifications_ResetAllNotifications: String { return self._s[2423]! } - public var PrivacySettings_SecurityTitle: String { return self._s[2426]! } - public var Checkout_NewCard_Title: String { return self._s[2427]! } - public var Login_HaveNotReceivedCodeInternal: String { return self._s[2428]! } - public var Conversation_ForwardChats: String { return self._s[2429]! } - public var Wallet_SecureStorageReset_PasscodeText: String { return self._s[2431]! } - public var PasscodeSettings_4DigitCode: String { return self._s[2432]! } - public var Settings_FAQ: String { return self._s[2434]! } - public var AutoDownloadSettings_DocumentsTitle: String { return self._s[2435]! } - public var Conversation_ContextMenuForward: String { return self._s[2436]! } - public var VoiceOver_Chat_YourPhoto: String { return self._s[2439]! } - public var PrivacyPolicy_Title: String { return self._s[2442]! } - public var Notifications_TextTone: String { return self._s[2443]! } - public var Profile_CreateNewContact: String { return self._s[2444]! } - public var PrivacyPhoneNumberSettings_WhoCanSeeMyPhoneNumber: String { return self._s[2445]! } - public var TwoFactorSetup_EmailVerification_Title: String { return self._s[2447]! } - public var Call_Speaker: String { return self._s[2448]! } - public var AutoNightTheme_AutomaticSection: String { return self._s[2449]! } - public var Channel_OwnershipTransfer_EnterPassword: String { return self._s[2451]! } - public var Channel_Username_InvalidCharacters: String { return self._s[2452]! } + public var Channel_Management_LabelAdministrator: String { return self._s[2501]! } + public var EditTheme_FileReadError: String { return self._s[2502]! } + public var OwnershipTransfer_ComeBackLater: String { return self._s[2503]! } + public var PrivacyLastSeenSettings_NeverShareWith_Placeholder: String { return self._s[2504]! } + public var AutoDownloadSettings_Photos: String { return self._s[2506]! } + public var Appearance_PreviewIncomingText: String { return self._s[2507]! } + public var ChatList_Context_MarkAllAsRead: String { return self._s[2508]! } + public var ChannelInfo_ConfirmLeave: String { return self._s[2509]! } + public var MediaPicker_MomentsDateRangeSameMonthYearFormat: String { return self._s[2510]! } + public var Passport_Identity_DocumentNumberPlaceholder: String { return self._s[2511]! } + public var Channel_AdminLogFilter_EventsNewMembers: String { return self._s[2512]! } + public var PasscodeSettings_AutoLock_IfAwayFor_5minutes: String { return self._s[2513]! } + public var GroupInfo_SetGroupPhotoStop: String { return self._s[2514]! } + public var Notification_SecretChatScreenshot: String { return self._s[2515]! } + public var AccessDenied_Wallpapers: String { return self._s[2516]! } + public var ChatList_Context_Mute: String { return self._s[2518]! } + public var Passport_Address_City: String { return self._s[2519]! } + public var InfoPlist_NSPhotoLibraryAddUsageDescription: String { return self._s[2520]! } + public var Appearance_ThemeCarouselClassic: String { return self._s[2521]! } + public var SocksProxySetup_SecretPlaceholder: String { return self._s[2522]! } + public var AccessDenied_LocationDisabled: String { return self._s[2523]! } + public var Group_Location_Title: String { return self._s[2524]! } + public var SocksProxySetup_HostnamePlaceholder: String { return self._s[2526]! } + public var GroupInfo_Sound: String { return self._s[2527]! } + public var SettingsSearch_Synonyms_ChatSettings_OpenLinksIn: String { return self._s[2528]! } + public var ChannelInfo_ScamChannelWarning: String { return self._s[2529]! } + public var Stickers_RemoveFromFavorites: String { return self._s[2530]! } + public var Contacts_Title: String { return self._s[2531]! } + public var EditTheme_ThemeTemplateAlertText: String { return self._s[2532]! } + public var Passport_Language_fr: String { return self._s[2533]! } + public var TwoFactorSetup_EmailVerification_Action: String { return self._s[2534]! } + public var Notifications_ResetAllNotifications: String { return self._s[2535]! } + public var IntentsSettings_SuggestedChats: String { return self._s[2537]! } + public var PrivacySettings_SecurityTitle: String { return self._s[2539]! } + public var Checkout_NewCard_Title: String { return self._s[2540]! } + public var Login_HaveNotReceivedCodeInternal: String { return self._s[2541]! } + public var Conversation_ForwardChats: String { return self._s[2542]! } + public var Wallet_SecureStorageReset_PasscodeText: String { return self._s[2544]! } + public var PasscodeSettings_4DigitCode: String { return self._s[2545]! } + public var Settings_FAQ: String { return self._s[2547]! } + public var AutoDownloadSettings_DocumentsTitle: String { return self._s[2548]! } + public var Conversation_ContextMenuForward: String { return self._s[2549]! } + public var VoiceOver_Chat_YourPhoto: String { return self._s[2552]! } + public var PrivacyPolicy_Title: String { return self._s[2555]! } + public var Notifications_TextTone: String { return self._s[2556]! } + public var Profile_CreateNewContact: String { return self._s[2557]! } + public var PrivacyPhoneNumberSettings_WhoCanSeeMyPhoneNumber: String { return self._s[2558]! } + public var TwoFactorSetup_EmailVerification_Title: String { return self._s[2560]! } + public var Call_Speaker: String { return self._s[2561]! } + public var AutoNightTheme_AutomaticSection: String { return self._s[2562]! } + public var Channel_OwnershipTransfer_EnterPassword: String { return self._s[2564]! } + public var Channel_Username_InvalidCharacters: String { return self._s[2565]! } public func Channel_AdminLog_MessageChangedChannelUsername(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2453]!, self._r[2453]!, [_0]) + return formatWithArgumentRanges(self._s[2566]!, self._r[2566]!, [_0]) } - public var AutoDownloadSettings_AutodownloadFiles: String { return self._s[2454]! } - public var PrivacySettings_LastSeenTitle: String { return self._s[2455]! } - public var Channel_AdminLog_CanInviteUsers: String { return self._s[2456]! } - public var SettingsSearch_Synonyms_Privacy_Data_ClearPaymentsInfo: String { return self._s[2457]! } - public var OwnershipTransfer_SecurityCheck: String { return self._s[2458]! } - public var Conversation_MessageDeliveryFailed: String { return self._s[2459]! } - public var Watch_ChatList_NoConversationsText: String { return self._s[2460]! } - public var Bot_Unblock: String { return self._s[2461]! } - public var TextFormat_Italic: String { return self._s[2462]! } - public var WallpaperSearch_ColorPink: String { return self._s[2463]! } - public var Settings_About_Help: String { return self._s[2464]! } - public var SearchImages_Title: String { return self._s[2465]! } - public var Weekday_Wednesday: String { return self._s[2466]! } - public var Conversation_ClousStorageInfo_Description1: String { return self._s[2467]! } - public var ExplicitContent_AlertTitle: String { return self._s[2468]! } + public var AutoDownloadSettings_AutodownloadFiles: String { return self._s[2567]! } + public var PrivacySettings_LastSeenTitle: String { return self._s[2568]! } + public var Channel_AdminLog_CanInviteUsers: String { return self._s[2569]! } + public var SettingsSearch_Synonyms_Privacy_Data_ClearPaymentsInfo: String { return self._s[2570]! } + public var OwnershipTransfer_SecurityCheck: String { return self._s[2571]! } + public var Conversation_MessageDeliveryFailed: String { return self._s[2572]! } + public var Watch_ChatList_NoConversationsText: String { return self._s[2573]! } + public var Bot_Unblock: String { return self._s[2574]! } + public var TextFormat_Italic: String { return self._s[2575]! } + public var WallpaperSearch_ColorPink: String { return self._s[2576]! } + public var Settings_About_Help: String { return self._s[2578]! } + public var SearchImages_Title: String { return self._s[2579]! } + public var Weekday_Wednesday: String { return self._s[2580]! } + public var Conversation_ClousStorageInfo_Description1: String { return self._s[2581]! } + public var ExplicitContent_AlertTitle: String { return self._s[2582]! } public func Time_PreciseDate_m5(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2469]!, self._r[2469]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[2583]!, self._r[2583]!, [_1, _2, _3]) } - public var Channel_DiscussionGroup_Create: String { return self._s[2470]! } - public var Weekday_Thursday: String { return self._s[2471]! } - public var Channel_BanUser_PermissionChangeGroupInfo: String { return self._s[2472]! } - public var Channel_Members_AddMembersHelp: String { return self._s[2473]! } + public var Channel_DiscussionGroup_Create: String { return self._s[2584]! } + public var Weekday_Thursday: String { return self._s[2585]! } + public var Channel_BanUser_PermissionChangeGroupInfo: String { return self._s[2586]! } + public var Channel_Members_AddMembersHelp: String { return self._s[2587]! } public func Checkout_SavePasswordTimeout(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2474]!, self._r[2474]!, [_0]) + return formatWithArgumentRanges(self._s[2588]!, self._r[2588]!, [_0]) } - public var Channel_DiscussionGroup_LinkGroup: String { return self._s[2475]! } - public var SettingsSearch_Synonyms_Notifications_InAppNotificationsVibrate: String { return self._s[2476]! } - public var Passport_RequestedInformation: String { return self._s[2477]! } - public var Login_PhoneAndCountryHelp: String { return self._s[2478]! } - public var Conversation_EncryptionProcessing: String { return self._s[2480]! } - public var Notifications_PermissionsSuppressWarningTitle: String { return self._s[2481]! } - public var PhotoEditor_EnhanceTool: String { return self._s[2483]! } - public var Channel_Setup_Title: String { return self._s[2484]! } - public var Conversation_SearchPlaceholder: String { return self._s[2485]! } - public var AccessDenied_LocationAlwaysDenied: String { return self._s[2486]! } - public var Checkout_ErrorGeneric: String { return self._s[2487]! } - public var Passport_Language_hu: String { return self._s[2488]! } - public var GroupPermission_EditingDisabled: String { return self._s[2489]! } - public var Wallet_Month_ShortSeptember: String { return self._s[2491]! } + public var Channel_DiscussionGroup_LinkGroup: String { return self._s[2589]! } + public var SettingsSearch_Synonyms_Notifications_InAppNotificationsVibrate: String { return self._s[2590]! } + public var Passport_RequestedInformation: String { return self._s[2591]! } + public var Login_PhoneAndCountryHelp: String { return self._s[2592]! } + public var Conversation_EncryptionProcessing: String { return self._s[2594]! } + public var Notifications_PermissionsSuppressWarningTitle: String { return self._s[2595]! } + public var PhotoEditor_EnhanceTool: String { return self._s[2597]! } + public var Channel_Setup_Title: String { return self._s[2598]! } + public var Conversation_SearchPlaceholder: String { return self._s[2599]! } + public var OldChannels_GroupEmptyFormat: String { return self._s[2600]! } + public var AccessDenied_LocationAlwaysDenied: String { return self._s[2601]! } + public var Checkout_ErrorGeneric: String { return self._s[2602]! } + public var Passport_Language_hu: String { return self._s[2603]! } + public var GroupPermission_EditingDisabled: String { return self._s[2604]! } + public var Wallet_Month_ShortSeptember: String { return self._s[2606]! } public func Passport_Identity_UploadOneOfScan(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2492]!, self._r[2492]!, [_0]) + return formatWithArgumentRanges(self._s[2607]!, self._r[2607]!, [_0]) } public func PUSH_MESSAGE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2495]!, self._r[2495]!, [_1]) + return formatWithArgumentRanges(self._s[2610]!, self._r[2610]!, [_1]) } - public var ChatList_DeleteSavedMessagesConfirmationTitle: String { return self._s[2496]! } + public var ChatList_DeleteSavedMessagesConfirmationTitle: String { return self._s[2611]! } public func UserInfo_BlockConfirmationTitle(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2497]!, self._r[2497]!, [_0]) + return formatWithArgumentRanges(self._s[2612]!, self._r[2612]!, [_0]) } - public var Conversation_CloudStorageInfo_Title: String { return self._s[2498]! } - public var Group_Location_Info: String { return self._s[2499]! } - public var PhotoEditor_CropAspectRatioSquare: String { return self._s[2500]! } - public var Permissions_PeopleNearbyAllow_v0: String { return self._s[2501]! } + public var Conversation_CloudStorageInfo_Title: String { return self._s[2613]! } + public var Group_Location_Info: String { return self._s[2614]! } + public var PhotoEditor_CropAspectRatioSquare: String { return self._s[2615]! } + public var Permissions_PeopleNearbyAllow_v0: String { return self._s[2616]! } public func Notification_Exceptions_MutedUntil(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2502]!, self._r[2502]!, [_0]) + return formatWithArgumentRanges(self._s[2617]!, self._r[2617]!, [_0]) } - public var Conversation_ClearPrivateHistory: String { return self._s[2503]! } - public var ContactInfo_PhoneLabelHome: String { return self._s[2504]! } - public var Appearance_RemoveThemeConfirmation: String { return self._s[2505]! } - public var PrivacySettings_LastSeenContacts: String { return self._s[2506]! } + public var Conversation_ClearPrivateHistory: String { return self._s[2618]! } + public var ContactInfo_PhoneLabelHome: String { return self._s[2619]! } + public var Appearance_RemoveThemeConfirmation: String { return self._s[2620]! } + public var PrivacySettings_LastSeenContacts: String { return self._s[2621]! } public func ChangePhone_ErrorOccupied(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2507]!, self._r[2507]!, [_0]) + return formatWithArgumentRanges(self._s[2622]!, self._r[2622]!, [_0]) } - public var Passport_Language_cs: String { return self._s[2508]! } - public var Message_PinnedAnimationMessage: String { return self._s[2510]! } - public var Passport_Identity_ReverseSideHelp: String { return self._s[2512]! } - public var SettingsSearch_Synonyms_Data_Storage_Title: String { return self._s[2513]! } - public var Wallet_Info_TransactionTo: String { return self._s[2515]! } - public var ChatList_DeleteForEveryoneConfirmationText: String { return self._s[2516]! } - public var SettingsSearch_Synonyms_Privacy_PasscodeAndTouchId: String { return self._s[2517]! } - public var Embed_PlayingInPIP: String { return self._s[2518]! } - public var AutoNightTheme_ScheduleSection: String { return self._s[2519]! } + public func Notification_PinnedQuizMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2623]!, self._r[2623]!, [_0]) + } + public var Passport_Language_cs: String { return self._s[2624]! } + public var Message_PinnedAnimationMessage: String { return self._s[2626]! } + public var Passport_Identity_ReverseSideHelp: String { return self._s[2628]! } + public var SettingsSearch_Synonyms_Data_Storage_Title: String { return self._s[2629]! } + public var Wallet_Info_TransactionTo: String { return self._s[2631]! } + public var ChatList_DeleteForEveryoneConfirmationText: String { return self._s[2632]! } + public var SettingsSearch_Synonyms_Privacy_PasscodeAndTouchId: String { return self._s[2633]! } + public var Embed_PlayingInPIP: String { return self._s[2634]! } + public var Appearance_ThemePreview_Chat_3_TextWithLink: String { return self._s[2635]! } + public var AutoNightTheme_ScheduleSection: String { return self._s[2636]! } public func Call_EmojiDescription(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2520]!, self._r[2520]!, [_0]) + return formatWithArgumentRanges(self._s[2637]!, self._r[2637]!, [_0]) } - public var MediaPicker_LivePhotoDescription: String { return self._s[2521]! } + public var MediaPicker_LivePhotoDescription: String { return self._s[2638]! } public func Channel_AdminLog_MessageRestrictedName(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2522]!, self._r[2522]!, [_1]) + return formatWithArgumentRanges(self._s[2639]!, self._r[2639]!, [_1]) } - public var Notification_PaymentSent: String { return self._s[2523]! } - public var PhotoEditor_CurvesGreen: String { return self._s[2524]! } - public var Notification_Exceptions_PreviewAlwaysOff: String { return self._s[2525]! } - public var AutoNightTheme_System: String { return self._s[2526]! } - public var SaveIncomingPhotosSettings_Title: String { return self._s[2527]! } - public var NotificationSettings_ShowNotificationsAllAccounts: String { return self._s[2528]! } - public var VoiceOver_Chat_PagePreview: String { return self._s[2529]! } + public var Notification_PaymentSent: String { return self._s[2640]! } + public var PhotoEditor_CurvesGreen: String { return self._s[2641]! } + public var Notification_Exceptions_PreviewAlwaysOff: String { return self._s[2642]! } + public var AutoNightTheme_System: String { return self._s[2643]! } + public var SaveIncomingPhotosSettings_Title: String { return self._s[2644]! } + public var CreatePoll_QuizTitle: String { return self._s[2645]! } + public var NotificationSettings_ShowNotificationsAllAccounts: String { return self._s[2646]! } + public var VoiceOver_Chat_PagePreview: String { return self._s[2647]! } public func PUSH_MESSAGE_SCREENSHOT(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2532]!, self._r[2532]!, [_1]) + return formatWithArgumentRanges(self._s[2650]!, self._r[2650]!, [_1]) } public func PUSH_MESSAGE_PHOTO_SECRET(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2533]!, self._r[2533]!, [_1]) + return formatWithArgumentRanges(self._s[2651]!, self._r[2651]!, [_1]) } public func ApplyLanguage_UnsufficientDataText(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2534]!, self._r[2534]!, [_1]) + return formatWithArgumentRanges(self._s[2652]!, self._r[2652]!, [_1]) } - public var NetworkUsageSettings_CallDataSection: String { return self._s[2536]! } - public var PasscodeSettings_HelpTop: String { return self._s[2537]! } - public var Conversation_WalletRequiredTitle: String { return self._s[2538]! } - public var Group_OwnershipTransfer_ErrorAdminsTooMuch: String { return self._s[2539]! } - public var Passport_Address_TypeRentalAgreement: String { return self._s[2540]! } - public var EditTheme_ShortLink: String { return self._s[2541]! } - public var ProxyServer_VoiceOver_Active: String { return self._s[2542]! } - public var ReportPeer_ReasonOther_Placeholder: String { return self._s[2543]! } - public var CheckoutInfo_ErrorPhoneInvalid: String { return self._s[2544]! } - public var Call_Accept: String { return self._s[2546]! } - public var GroupRemoved_RemoveInfo: String { return self._s[2547]! } - public var Month_GenMarch: String { return self._s[2549]! } - public var PhotoEditor_ShadowsTool: String { return self._s[2550]! } - public var LoginPassword_Title: String { return self._s[2551]! } - public var Call_End: String { return self._s[2552]! } - public var Watch_Conversation_GroupInfo: String { return self._s[2553]! } - public var VoiceOver_Chat_Contact: String { return self._s[2554]! } - public var EditTheme_Create_Preview_IncomingText: String { return self._s[2555]! } - public var CallSettings_Always: String { return self._s[2556]! } - public var CallFeedback_Success: String { return self._s[2557]! } - public var TwoStepAuth_SetupHint: String { return self._s[2558]! } + public var NetworkUsageSettings_CallDataSection: String { return self._s[2654]! } + public var PasscodeSettings_HelpTop: String { return self._s[2655]! } + public var Conversation_WalletRequiredTitle: String { return self._s[2656]! } + public var PeerInfo_AddToContacts: String { return self._s[2657]! } + public var Group_OwnershipTransfer_ErrorAdminsTooMuch: String { return self._s[2658]! } + public var Passport_Address_TypeRentalAgreement: String { return self._s[2659]! } + public var EditTheme_ShortLink: String { return self._s[2660]! } + public var Theme_Colors_ColorWallpaperWarning: String { return self._s[2661]! } + public var ProxyServer_VoiceOver_Active: String { return self._s[2662]! } + public var ReportPeer_ReasonOther_Placeholder: String { return self._s[2663]! } + public var CheckoutInfo_ErrorPhoneInvalid: String { return self._s[2664]! } + public var Call_Accept: String { return self._s[2666]! } + public var GroupRemoved_RemoveInfo: String { return self._s[2667]! } + public var Month_GenMarch: String { return self._s[2669]! } + public var PhotoEditor_ShadowsTool: String { return self._s[2670]! } + public var LoginPassword_Title: String { return self._s[2671]! } + public var Call_End: String { return self._s[2672]! } + public var Watch_Conversation_GroupInfo: String { return self._s[2673]! } + public var VoiceOver_Chat_Contact: String { return self._s[2674]! } + public var EditTheme_Create_Preview_IncomingText: String { return self._s[2675]! } + public var CallSettings_Always: String { return self._s[2676]! } + public var CallFeedback_Success: String { return self._s[2677]! } + public var TwoStepAuth_SetupHint: String { return self._s[2678]! } public func AddContact_ContactWillBeSharedAfterMutual(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2559]!, self._r[2559]!, [_1]) + return formatWithArgumentRanges(self._s[2679]!, self._r[2679]!, [_1]) } - public var ConversationProfile_UsersTooMuchError: String { return self._s[2560]! } - public var Login_PhoneTitle: String { return self._s[2561]! } - public var Passport_FieldPhoneHelp: String { return self._s[2562]! } - public var Weekday_ShortSunday: String { return self._s[2563]! } - public var Passport_InfoFAQ_URL: String { return self._s[2564]! } - public var ContactInfo_Job: String { return self._s[2566]! } - public var UserInfo_InviteBotToGroup: String { return self._s[2567]! } - public var Appearance_ThemeCarouselNightBlue: String { return self._s[2568]! } - public var TwoFactorSetup_Email_Text: String { return self._s[2569]! } - public var TwoStepAuth_PasswordRemovePassportConfirmation: String { return self._s[2570]! } - public var Invite_ChannelsTooMuch: String { return self._s[2571]! } - public var Wallet_Send_ConfirmationConfirm: String { return self._s[2572]! } - public var Wallet_TransactionInfo_OtherFeeInfo: String { return self._s[2573]! } - public var SettingsSearch_Synonyms_Notifications_InAppNotificationsPreview: String { return self._s[2574]! } - public var Wallet_Receive_AmountText: String { return self._s[2575]! } - public var Passport_DeletePersonalDetailsConfirmation: String { return self._s[2576]! } - public var CallFeedback_ReasonNoise: String { return self._s[2577]! } - public var Appearance_AppIconDefault: String { return self._s[2579]! } - public var Passport_Identity_AddInternalPassport: String { return self._s[2580]! } - public var MediaPicker_AddCaption: String { return self._s[2581]! } - public var CallSettings_TabIconDescription: String { return self._s[2582]! } + public var ConversationProfile_UsersTooMuchError: String { return self._s[2680]! } + public var PeerInfo_ButtonAddMember: String { return self._s[2681]! } + public var Login_PhoneTitle: String { return self._s[2682]! } + public var Passport_FieldPhoneHelp: String { return self._s[2683]! } + public var Weekday_ShortSunday: String { return self._s[2684]! } + public var Passport_InfoFAQ_URL: String { return self._s[2685]! } + public var ContactInfo_Job: String { return self._s[2687]! } + public var UserInfo_InviteBotToGroup: String { return self._s[2688]! } + public var Appearance_ThemeCarouselNightBlue: String { return self._s[2689]! } + public var CreatePoll_QuizTip: String { return self._s[2690]! } + public var TwoFactorSetup_Email_Text: String { return self._s[2691]! } + public var TwoStepAuth_PasswordRemovePassportConfirmation: String { return self._s[2692]! } + public var Invite_ChannelsTooMuch: String { return self._s[2693]! } + public var Wallet_Send_ConfirmationConfirm: String { return self._s[2694]! } + public var Wallet_TransactionInfo_OtherFeeInfo: String { return self._s[2695]! } + public var SettingsSearch_Synonyms_Notifications_InAppNotificationsPreview: String { return self._s[2696]! } + public var Wallet_Receive_AmountText: String { return self._s[2697]! } + public var Passport_DeletePersonalDetailsConfirmation: String { return self._s[2698]! } + public var CallFeedback_ReasonNoise: String { return self._s[2699]! } + public var Appearance_AppIconDefault: String { return self._s[2701]! } + public var Passport_Identity_AddInternalPassport: String { return self._s[2702]! } + public var MediaPicker_AddCaption: String { return self._s[2703]! } + public var CallSettings_TabIconDescription: String { return self._s[2704]! } public func VoiceOver_Chat_Caption(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2583]!, self._r[2583]!, [_0]) + return formatWithArgumentRanges(self._s[2705]!, self._r[2705]!, [_0]) } - public var ChatList_UndoArchiveHiddenTitle: String { return self._s[2584]! } - public var Privacy_GroupsAndChannels_AlwaysAllow: String { return self._s[2585]! } - public var Passport_Identity_TypePersonalDetails: String { return self._s[2586]! } - public var DialogList_SearchSectionRecent: String { return self._s[2587]! } - public var PrivacyPolicy_DeclineMessage: String { return self._s[2588]! } - public var LogoutOptions_ClearCacheText: String { return self._s[2591]! } - public var LastSeen_WithinAWeek: String { return self._s[2592]! } - public var ChannelMembers_GroupAdminsTitle: String { return self._s[2593]! } - public var Conversation_CloudStorage_ChatStatus: String { return self._s[2595]! } - public var VoiceOver_Media_PlaybackRateNormal: String { return self._s[2596]! } + public var IntentsSettings_SuggestedChatsGroups: String { return self._s[2706]! } + public func Map_SearchNoResultsDescription(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2707]!, self._r[2707]!, [_0]) + } + public var ChatList_UndoArchiveHiddenTitle: String { return self._s[2708]! } + public var Privacy_GroupsAndChannels_AlwaysAllow: String { return self._s[2709]! } + public var Passport_Identity_TypePersonalDetails: String { return self._s[2710]! } + public var DialogList_SearchSectionRecent: String { return self._s[2711]! } + public var PrivacyPolicy_DeclineMessage: String { return self._s[2712]! } + public var CreatePoll_Anonymous: String { return self._s[2713]! } + public var LogoutOptions_ClearCacheText: String { return self._s[2716]! } + public var LastSeen_WithinAWeek: String { return self._s[2717]! } + public var ChannelMembers_GroupAdminsTitle: String { return self._s[2718]! } + public var Conversation_CloudStorage_ChatStatus: String { return self._s[2720]! } + public var VoiceOver_Media_PlaybackRateNormal: String { return self._s[2721]! } public func AddContact_SharedContactExceptionInfo(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2597]!, self._r[2597]!, [_0]) + return formatWithArgumentRanges(self._s[2722]!, self._r[2722]!, [_0]) } - public var Passport_Address_TypeResidentialAddress: String { return self._s[2598]! } - public var Conversation_StatusLeftGroup: String { return self._s[2599]! } - public var SocksProxySetup_ProxyDetailsTitle: String { return self._s[2600]! } - public var SettingsSearch_Synonyms_Calls_Title: String { return self._s[2602]! } - public var GroupPermission_AddSuccess: String { return self._s[2603]! } - public var PhotoEditor_BlurToolRadial: String { return self._s[2605]! } - public var Conversation_ContextMenuCopy: String { return self._s[2606]! } - public var AccessDenied_CallMicrophone: String { return self._s[2607]! } + public var Passport_Address_TypeResidentialAddress: String { return self._s[2723]! } + public var Conversation_StatusLeftGroup: String { return self._s[2724]! } + public var SocksProxySetup_ProxyDetailsTitle: String { return self._s[2725]! } + public var SettingsSearch_Synonyms_Calls_Title: String { return self._s[2727]! } + public var GroupPermission_AddSuccess: String { return self._s[2728]! } + public var PhotoEditor_BlurToolRadial: String { return self._s[2730]! } + public var Conversation_ContextMenuCopy: String { return self._s[2731]! } + public var AccessDenied_CallMicrophone: String { return self._s[2732]! } public func Time_PreciseDate_m2(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2608]!, self._r[2608]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[2733]!, self._r[2733]!, [_1, _2, _3]) } - public var Login_InvalidFirstNameError: String { return self._s[2609]! } - public var Notifications_Badge_CountUnreadMessages_InfoOn: String { return self._s[2610]! } - public var Checkout_PaymentMethod_New: String { return self._s[2611]! } - public var ShareMenu_CopyShareLinkGame: String { return self._s[2612]! } - public var PhotoEditor_QualityTool: String { return self._s[2613]! } - public var Login_SendCodeViaSms: String { return self._s[2614]! } - public var SettingsSearch_Synonyms_Privacy_DeleteAccountIfAwayFor: String { return self._s[2615]! } - public var Chat_SlowmodeAttachmentLimitReached: String { return self._s[2616]! } - public var Wallet_Receive_CopyAddress: String { return self._s[2617]! } - public var Login_EmailNotConfiguredError: String { return self._s[2618]! } - public var SocksProxySetup_Status: String { return self._s[2619]! } - public var Conversation_ScheduleMessage_SendWhenOnline: String { return self._s[2620]! } - public var PrivacyPolicy_Accept: String { return self._s[2621]! } - public var Notifications_ExceptionsMessagePlaceholder: String { return self._s[2622]! } - public var Appearance_AppIconClassicX: String { return self._s[2623]! } + public var Login_InvalidFirstNameError: String { return self._s[2734]! } + public var Notifications_Badge_CountUnreadMessages_InfoOn: String { return self._s[2735]! } + public var Checkout_PaymentMethod_New: String { return self._s[2736]! } + public var ShareMenu_CopyShareLinkGame: String { return self._s[2737]! } + public var PhotoEditor_QualityTool: String { return self._s[2738]! } + public var Login_SendCodeViaSms: String { return self._s[2739]! } + public var SettingsSearch_Synonyms_Privacy_DeleteAccountIfAwayFor: String { return self._s[2740]! } + public var Chat_SlowmodeAttachmentLimitReached: String { return self._s[2741]! } + public var Wallet_Receive_CopyAddress: String { return self._s[2742]! } + public var Login_EmailNotConfiguredError: String { return self._s[2743]! } + public var SocksProxySetup_Status: String { return self._s[2744]! } + public var Conversation_ScheduleMessage_SendWhenOnline: String { return self._s[2745]! } + public var PrivacyPolicy_Accept: String { return self._s[2746]! } + public var Notifications_ExceptionsMessagePlaceholder: String { return self._s[2747]! } + public var Appearance_AppIconClassicX: String { return self._s[2748]! } public func PUSH_CHAT_MESSAGE_TEXT(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2624]!, self._r[2624]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[2749]!, self._r[2749]!, [_1, _2, _3]) } - public var OwnershipTransfer_SecurityRequirements: String { return self._s[2625]! } - public var InfoPlist_NSLocationAlwaysUsageDescription: String { return self._s[2627]! } - public var AutoNightTheme_Automatic: String { return self._s[2628]! } - public var Channel_Username_InvalidStartsWithNumber: String { return self._s[2629]! } - public var Privacy_ContactsSyncHelp: String { return self._s[2630]! } - public var Cache_Help: String { return self._s[2631]! } - public var Group_ErrorAccessDenied: String { return self._s[2632]! } - public var Passport_Language_fa: String { return self._s[2633]! } - public var Wallet_Intro_Text: String { return self._s[2634]! } - public var Login_ResetAccountProtected_TimerTitle: String { return self._s[2635]! } - public var VoiceOver_Chat_YourVideoMessage: String { return self._s[2636]! } - public var PrivacySettings_LastSeen: String { return self._s[2637]! } + public var OwnershipTransfer_SecurityRequirements: String { return self._s[2750]! } + public var InfoPlist_NSLocationAlwaysUsageDescription: String { return self._s[2752]! } + public var AutoNightTheme_Automatic: String { return self._s[2753]! } + public var Channel_Username_InvalidStartsWithNumber: String { return self._s[2754]! } + public var Privacy_ContactsSyncHelp: String { return self._s[2755]! } + public var Cache_Help: String { return self._s[2756]! } + public var Group_ErrorAccessDenied: String { return self._s[2757]! } + public var Passport_Language_fa: String { return self._s[2758]! } + public var Wallet_Intro_Text: String { return self._s[2759]! } + public var Login_ResetAccountProtected_TimerTitle: String { return self._s[2760]! } + public var VoiceOver_Chat_YourVideoMessage: String { return self._s[2761]! } + public var PrivacySettings_LastSeen: String { return self._s[2762]! } public func DialogList_MultipleTyping(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2638]!, self._r[2638]!, [_0, _1]) + return formatWithArgumentRanges(self._s[2763]!, self._r[2763]!, [_0, _1]) } - public var Wallet_Configuration_Apply: String { return self._s[2642]! } - public var Preview_SaveGif: String { return self._s[2643]! } - public var SettingsSearch_Synonyms_Privacy_TwoStepAuth: String { return self._s[2644]! } - public var Profile_About: String { return self._s[2645]! } - public var Channel_About_Placeholder: String { return self._s[2646]! } - public var Login_InfoTitle: String { return self._s[2647]! } + public var Wallet_Configuration_Apply: String { return self._s[2767]! } + public var Preview_SaveGif: String { return self._s[2768]! } + public var SettingsSearch_Synonyms_Privacy_TwoStepAuth: String { return self._s[2769]! } + public var Profile_About: String { return self._s[2770]! } + public var Channel_About_Placeholder: String { return self._s[2771]! } + public var Login_InfoTitle: String { return self._s[2772]! } public func TwoStepAuth_SetupPendingEmail(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2648]!, self._r[2648]!, [_0]) + return formatWithArgumentRanges(self._s[2773]!, self._r[2773]!, [_0]) } - public var EditTheme_Expand_Preview_IncomingReplyText: String { return self._s[2649]! } - public var Watch_Suggestion_CantTalk: String { return self._s[2651]! } - public var ContactInfo_Title: String { return self._s[2652]! } - public var Media_ShareThisVideo: String { return self._s[2653]! } - public var Weekday_ShortFriday: String { return self._s[2654]! } - public var AccessDenied_Contacts: String { return self._s[2656]! } - public var Notification_CallIncomingShort: String { return self._s[2657]! } - public var Group_Setup_TypePublic: String { return self._s[2658]! } - public var Notifications_MessageNotificationsExceptions: String { return self._s[2659]! } - public var Notifications_Badge_IncludeChannels: String { return self._s[2660]! } - public var Notifications_MessageNotificationsPreview: String { return self._s[2663]! } - public var ConversationProfile_ErrorCreatingConversation: String { return self._s[2664]! } - public var Group_ErrorAddTooMuchBots: String { return self._s[2665]! } - public var Privacy_GroupsAndChannels_CustomShareHelp: String { return self._s[2666]! } - public var Permissions_CellularDataAllowInSettings_v0: String { return self._s[2667]! } + public var EditTheme_Expand_Preview_IncomingReplyText: String { return self._s[2774]! } + public var Watch_Suggestion_CantTalk: String { return self._s[2776]! } + public var ContactInfo_Title: String { return self._s[2777]! } + public var Media_ShareThisVideo: String { return self._s[2778]! } + public var Weekday_ShortFriday: String { return self._s[2779]! } + public var AccessDenied_Contacts: String { return self._s[2781]! } + public var Notification_CallIncomingShort: String { return self._s[2782]! } + public var Group_Setup_TypePublic: String { return self._s[2783]! } + public var Notifications_MessageNotificationsExceptions: String { return self._s[2784]! } + public var Notifications_Badge_IncludeChannels: String { return self._s[2785]! } + public var Notifications_MessageNotificationsPreview: String { return self._s[2788]! } + public var ConversationProfile_ErrorCreatingConversation: String { return self._s[2789]! } + public var Group_ErrorAddTooMuchBots: String { return self._s[2790]! } + public var Privacy_GroupsAndChannels_CustomShareHelp: String { return self._s[2791]! } + public var Permissions_CellularDataAllowInSettings_v0: String { return self._s[2792]! } public func Wallet_SecureStorageChanged_BiometryText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2668]!, self._r[2668]!, [_0]) + return formatWithArgumentRanges(self._s[2793]!, self._r[2793]!, [_0]) } - public var DialogList_Typing: String { return self._s[2669]! } - public var CallFeedback_IncludeLogs: String { return self._s[2671]! } - public var Checkout_Phone: String { return self._s[2673]! } - public var Login_InfoFirstNamePlaceholder: String { return self._s[2676]! } - public var Privacy_Calls_Integration: String { return self._s[2677]! } - public var Notifications_PermissionsAllow: String { return self._s[2678]! } - public var TwoStepAuth_AddHintDescription: String { return self._s[2682]! } - public var Settings_ChatSettings: String { return self._s[2683]! } + public var DialogList_Typing: String { return self._s[2794]! } + public var CallFeedback_IncludeLogs: String { return self._s[2796]! } + public var Checkout_Phone: String { return self._s[2798]! } + public var Login_InfoFirstNamePlaceholder: String { return self._s[2801]! } + public var Privacy_Calls_Integration: String { return self._s[2802]! } + public var Notifications_PermissionsAllow: String { return self._s[2803]! } + public var TwoStepAuth_AddHintDescription: String { return self._s[2808]! } + public var Settings_ChatSettings: String { return self._s[2809]! } + public var Conversation_SendingOptionsTooltip: String { return self._s[2810]! } public func UserInfo_StartSecretChatConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2684]!, self._r[2684]!, [_0]) + return formatWithArgumentRanges(self._s[2812]!, self._r[2812]!, [_0]) } public func Channel_AdminLog_MessageInvitedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2685]!, self._r[2685]!, [_1, _2]) + return formatWithArgumentRanges(self._s[2813]!, self._r[2813]!, [_1, _2]) } - public var GroupRemoved_DeleteUser: String { return self._s[2687]! } + public var GroupRemoved_DeleteUser: String { return self._s[2815]! } public func Channel_AdminLog_PollStopped(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2688]!, self._r[2688]!, [_0]) + return formatWithArgumentRanges(self._s[2816]!, self._r[2816]!, [_0]) } public func PUSH_MESSAGE_PHOTO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2689]!, self._r[2689]!, [_1]) + return formatWithArgumentRanges(self._s[2817]!, self._r[2817]!, [_1]) } - public var Login_ContinueWithLocalization: String { return self._s[2690]! } - public var Watch_Message_ForwardedFrom: String { return self._s[2691]! } - public var TwoStepAuth_EnterEmailCode: String { return self._s[2693]! } - public var Conversation_Unblock: String { return self._s[2694]! } - public var PrivacySettings_DataSettings: String { return self._s[2695]! } - public var Group_PublicLink_Info: String { return self._s[2696]! } + public var Login_ContinueWithLocalization: String { return self._s[2818]! } + public var Watch_Message_ForwardedFrom: String { return self._s[2819]! } + public var TwoStepAuth_EnterEmailCode: String { return self._s[2821]! } + public var Conversation_Unblock: String { return self._s[2822]! } + public var PrivacySettings_DataSettings: String { return self._s[2823]! } + public var WallpaperPreview_PatternPaternApply: String { return self._s[2824]! } + public var Group_PublicLink_Info: String { return self._s[2825]! } public func Wallet_Time_PreciseDate_m1(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2697]!, self._r[2697]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[2826]!, self._r[2826]!, [_1, _2, _3]) } - public var Notifications_InAppNotificationsVibrate: String { return self._s[2698]! } + public var Notifications_InAppNotificationsVibrate: String { return self._s[2827]! } public func Privacy_GroupsAndChannels_InviteToChannelError(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2699]!, self._r[2699]!, [_0, _1]) + return formatWithArgumentRanges(self._s[2828]!, self._r[2828]!, [_0, _1]) } - public var Wallet_RestoreFailed_CreateWallet: String { return self._s[2701]! } - public var PrivacySettings_Passcode: String { return self._s[2703]! } - public var Call_Mute: String { return self._s[2704]! } - public var Wallet_Weekday_Yesterday: String { return self._s[2705]! } - public var Passport_Language_dz: String { return self._s[2706]! } - public var Wallet_Receive_AmountHeader: String { return self._s[2707]! } - public var Wallet_TransactionInfo_OtherFeeInfoUrl: String { return self._s[2708]! } - public var Passport_Language_tk: String { return self._s[2709]! } + public var OldChannels_ChannelsHeader: String { return self._s[2830]! } + public var Wallet_RestoreFailed_CreateWallet: String { return self._s[2831]! } + public var PrivacySettings_Passcode: String { return self._s[2833]! } + public var Call_Mute: String { return self._s[2834]! } + public var Wallet_Weekday_Yesterday: String { return self._s[2835]! } + public var Passport_Language_dz: String { return self._s[2836]! } + public var Wallet_Receive_AmountHeader: String { return self._s[2837]! } + public var Wallet_TransactionInfo_OtherFeeInfoUrl: String { return self._s[2838]! } + public var Passport_Language_tk: String { return self._s[2839]! } public func Login_EmailCodeSubject(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2710]!, self._r[2710]!, [_0]) + return formatWithArgumentRanges(self._s[2840]!, self._r[2840]!, [_0]) } - public var Settings_Search: String { return self._s[2711]! } - public var Wallet_Month_ShortFebruary: String { return self._s[2712]! } - public var InfoPlist_NSPhotoLibraryUsageDescription: String { return self._s[2713]! } - public var Wallet_Configuration_SourceJSON: String { return self._s[2714]! } - public var Conversation_ContextMenuReply: String { return self._s[2715]! } - public var WallpaperSearch_ColorBrown: String { return self._s[2716]! } - public var Chat_AttachmentMultipleForwardDisabled: String { return self._s[2717]! } - public var Tour_Title1: String { return self._s[2718]! } - public var Wallet_Alert_Cancel: String { return self._s[2719]! } - public var Conversation_ClearGroupHistory: String { return self._s[2721]! } - public var Wallet_TransactionInfo_RecipientHeader: String { return self._s[2722]! } - public var WallpaperPreview_Motion: String { return self._s[2723]! } + public var Settings_Search: String { return self._s[2841]! } + public var Wallet_Month_ShortFebruary: String { return self._s[2842]! } + public var InfoPlist_NSPhotoLibraryUsageDescription: String { return self._s[2843]! } + public var Wallet_Configuration_SourceJSON: String { return self._s[2844]! } + public var Conversation_ContextMenuReply: String { return self._s[2845]! } + public var WallpaperSearch_ColorBrown: String { return self._s[2846]! } + public var Chat_AttachmentMultipleForwardDisabled: String { return self._s[2847]! } + public var Tour_Title1: String { return self._s[2848]! } + public var Wallet_Alert_Cancel: String { return self._s[2849]! } + public var Conversation_ClearGroupHistory: String { return self._s[2851]! } + public var Wallet_TransactionInfo_RecipientHeader: String { return self._s[2852]! } + public var WallpaperPreview_Motion: String { return self._s[2853]! } public func Checkout_PasswordEntry_Text(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2724]!, self._r[2724]!, [_0]) + return formatWithArgumentRanges(self._s[2854]!, self._r[2854]!, [_0]) } - public var Wallet_Configuration_ApplyErrorTextJSONInvalidData: String { return self._s[2725]! } - public var Call_RateCall: String { return self._s[2726]! } - public var Channel_AdminLog_BanSendStickersAndGifs: String { return self._s[2727]! } - public var Passport_PasswordCompleteSetup: String { return self._s[2728]! } - public var Conversation_InputTextSilentBroadcastPlaceholder: String { return self._s[2729]! } - public var UserInfo_LastNamePlaceholder: String { return self._s[2731]! } + public var Wallet_Configuration_ApplyErrorTextJSONInvalidData: String { return self._s[2855]! } + public var Call_RateCall: String { return self._s[2856]! } + public var Channel_AdminLog_BanSendStickersAndGifs: String { return self._s[2857]! } + public var Passport_PasswordCompleteSetup: String { return self._s[2858]! } + public var Conversation_InputTextSilentBroadcastPlaceholder: String { return self._s[2859]! } + public var UserInfo_LastNamePlaceholder: String { return self._s[2861]! } public func Login_WillCallYou(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2733]!, self._r[2733]!, [_0]) + return formatWithArgumentRanges(self._s[2863]!, self._r[2863]!, [_0]) } - public var Compose_Create: String { return self._s[2734]! } - public var Contacts_InviteToTelegram: String { return self._s[2735]! } - public var GroupInfo_Notifications: String { return self._s[2736]! } - public var ChatList_DeleteSavedMessagesConfirmationAction: String { return self._s[2738]! } - public var Message_PinnedLiveLocationMessage: String { return self._s[2739]! } - public var Month_GenApril: String { return self._s[2740]! } - public var Appearance_AutoNightTheme: String { return self._s[2741]! } - public var ChatSettings_AutomaticAudioDownload: String { return self._s[2743]! } - public var Login_CodeSentSms: String { return self._s[2745]! } + public var Compose_Create: String { return self._s[2864]! } + public var Contacts_InviteToTelegram: String { return self._s[2865]! } + public var GroupInfo_Notifications: String { return self._s[2866]! } + public var ChatList_DeleteSavedMessagesConfirmationAction: String { return self._s[2868]! } + public var Message_PinnedLiveLocationMessage: String { return self._s[2869]! } + public var Month_GenApril: String { return self._s[2870]! } + public var Appearance_AutoNightTheme: String { return self._s[2871]! } + public var ChatSettings_AutomaticAudioDownload: String { return self._s[2873]! } + public var Login_CodeSentSms: String { return self._s[2875]! } public func UserInfo_UnblockConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2746]!, self._r[2746]!, [_0]) + return formatWithArgumentRanges(self._s[2876]!, self._r[2876]!, [_0]) } - public var EmptyGroupInfo_Line3: String { return self._s[2747]! } - public var LogoutOptions_ContactSupportText: String { return self._s[2748]! } - public var Passport_Language_hr: String { return self._s[2749]! } - public var Common_ActionNotAllowedError: String { return self._s[2750]! } + public var EmptyGroupInfo_Line3: String { return self._s[2877]! } + public var LogoutOptions_ContactSupportText: String { return self._s[2878]! } + public var Passport_Language_hr: String { return self._s[2879]! } + public var Common_ActionNotAllowedError: String { return self._s[2880]! } public func Channel_AdminLog_MessageRestrictedNewSetting(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2751]!, self._r[2751]!, [_0]) + return formatWithArgumentRanges(self._s[2881]!, self._r[2881]!, [_0]) } - public var GroupInfo_InviteLink_CopyLink: String { return self._s[2752]! } - public var Wallet_Info_TransactionFrom: String { return self._s[2753]! } - public var Wallet_Send_ErrorDecryptionFailed: String { return self._s[2754]! } - public var Conversation_InputTextBroadcastPlaceholder: String { return self._s[2755]! } - public var Privacy_SecretChatsTitle: String { return self._s[2756]! } - public var Notification_SecretChatMessageScreenshotSelf: String { return self._s[2758]! } - public var GroupInfo_AddUserLeftError: String { return self._s[2759]! } - public var AutoDownloadSettings_TypePrivateChats: String { return self._s[2760]! } - public var LogoutOptions_ContactSupportTitle: String { return self._s[2761]! } - public var Channel_AddBotErrorHaveRights: String { return self._s[2762]! } - public var Preview_DeleteGif: String { return self._s[2763]! } - public var GroupInfo_Permissions_Exceptions: String { return self._s[2764]! } - public var Group_ErrorNotMutualContact: String { return self._s[2765]! } - public var Notification_MessageLifetime5s: String { return self._s[2766]! } - public var Wallet_Send_OwnAddressAlertText: String { return self._s[2767]! } + public var GroupInfo_InviteLink_CopyLink: String { return self._s[2882]! } + public var Wallet_Info_TransactionFrom: String { return self._s[2883]! } + public var Wallet_Send_ErrorDecryptionFailed: String { return self._s[2884]! } + public var Conversation_InputTextBroadcastPlaceholder: String { return self._s[2885]! } + public var Privacy_SecretChatsTitle: String { return self._s[2886]! } + public var Notification_SecretChatMessageScreenshotSelf: String { return self._s[2888]! } + public var GroupInfo_AddUserLeftError: String { return self._s[2889]! } + public var AutoDownloadSettings_TypePrivateChats: String { return self._s[2890]! } + public var LogoutOptions_ContactSupportTitle: String { return self._s[2891]! } + public var Appearance_ThemePreview_Chat_7_Text: String { return self._s[2892]! } + public var Channel_AddBotErrorHaveRights: String { return self._s[2893]! } + public var Preview_DeleteGif: String { return self._s[2894]! } + public var GroupInfo_Permissions_Exceptions: String { return self._s[2895]! } + public var Group_ErrorNotMutualContact: String { return self._s[2896]! } + public var Notification_MessageLifetime5s: String { return self._s[2897]! } + public var Wallet_Send_OwnAddressAlertText: String { return self._s[2898]! } + public var OldChannels_ChannelFormat: String { return self._s[2899]! } public func Watch_LastSeen_AtDate(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2768]!, self._r[2768]!, [_0]) + return formatWithArgumentRanges(self._s[2900]!, self._r[2900]!, [_0]) } - public var VoiceOver_Chat_Video: String { return self._s[2769]! } - public var Channel_OwnershipTransfer_ErrorPublicChannelsTooMuch: String { return self._s[2771]! } - public var ReportSpam_DeleteThisChat: String { return self._s[2772]! } - public var Passport_Address_AddBankStatement: String { return self._s[2773]! } - public var Notification_CallIncoming: String { return self._s[2774]! } - public var Wallet_Words_NotDoneTitle: String { return self._s[2775]! } - public var Compose_NewGroupTitle: String { return self._s[2776]! } - public var TwoStepAuth_RecoveryCodeHelp: String { return self._s[2778]! } - public var Passport_Address_Postcode: String { return self._s[2780]! } + public var VoiceOver_Chat_Video: String { return self._s[2901]! } + public var Channel_OwnershipTransfer_ErrorPublicChannelsTooMuch: String { return self._s[2903]! } + public var ReportSpam_DeleteThisChat: String { return self._s[2904]! } + public var Passport_Address_AddBankStatement: String { return self._s[2905]! } + public var Notification_CallIncoming: String { return self._s[2906]! } + public var Wallet_Words_NotDoneTitle: String { return self._s[2907]! } + public var Compose_NewGroupTitle: String { return self._s[2908]! } + public var TwoStepAuth_RecoveryCodeHelp: String { return self._s[2910]! } + public var Passport_Address_Postcode: String { return self._s[2912]! } public func LastSeen_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2781]!, self._r[2781]!, [_0]) + return formatWithArgumentRanges(self._s[2913]!, self._r[2913]!, [_0]) } - public var Checkout_NewCard_SaveInfoHelp: String { return self._s[2782]! } - public var Wallet_Month_ShortOctober: String { return self._s[2783]! } - public var VoiceOver_Chat_YourMusic: String { return self._s[2784]! } - public var WallpaperColors_Title: String { return self._s[2785]! } - public var SocksProxySetup_ShareQRCodeInfo: String { return self._s[2786]! } - public var VoiceOver_MessageContextForward: String { return self._s[2787]! } - public var GroupPermission_Duration: String { return self._s[2788]! } + public var Checkout_NewCard_SaveInfoHelp: String { return self._s[2914]! } + public var Wallet_Month_ShortOctober: String { return self._s[2915]! } + public var VoiceOver_Chat_YourMusic: String { return self._s[2916]! } + public var WallpaperColors_Title: String { return self._s[2917]! } + public var SocksProxySetup_ShareQRCodeInfo: String { return self._s[2918]! } + public var VoiceOver_MessageContextForward: String { return self._s[2919]! } + public var GroupPermission_Duration: String { return self._s[2920]! } public func Cache_Clear(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2789]!, self._r[2789]!, [_0]) + return formatWithArgumentRanges(self._s[2921]!, self._r[2921]!, [_0]) } - public var Bot_GroupStatusDoesNotReadHistory: String { return self._s[2790]! } - public var Username_Placeholder: String { return self._s[2791]! } - public var CallFeedback_WhatWentWrong: String { return self._s[2792]! } - public var Passport_FieldAddressUploadHelp: String { return self._s[2793]! } - public var Permissions_NotificationsAllowInSettings_v0: String { return self._s[2794]! } + public var Bot_GroupStatusDoesNotReadHistory: String { return self._s[2922]! } + public var Username_Placeholder: String { return self._s[2923]! } + public var CallFeedback_WhatWentWrong: String { return self._s[2924]! } + public var Passport_FieldAddressUploadHelp: String { return self._s[2925]! } + public var Permissions_NotificationsAllowInSettings_v0: String { return self._s[2926]! } public func Channel_AdminLog_MessageChangedUnlinkedChannel(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2796]!, self._r[2796]!, [_1, _2]) + return formatWithArgumentRanges(self._s[2928]!, self._r[2928]!, [_1, _2]) } - public var Passport_PasswordDescription: String { return self._s[2797]! } - public var Channel_MessagePhotoUpdated: String { return self._s[2798]! } - public var MediaPicker_TapToUngroupDescription: String { return self._s[2799]! } - public var SettingsSearch_Synonyms_Notifications_BadgeCountUnreadMessages: String { return self._s[2800]! } - public var AttachmentMenu_PhotoOrVideo: String { return self._s[2801]! } - public var Conversation_ContextMenuMore: String { return self._s[2802]! } - public var Privacy_PaymentsClearInfo: String { return self._s[2803]! } - public var CallSettings_TabIcon: String { return self._s[2804]! } - public var KeyCommand_Find: String { return self._s[2805]! } - public var ClearCache_FreeSpaceDescription: String { return self._s[2806]! } - public var Appearance_ThemePreview_ChatList_7_Text: String { return self._s[2807]! } - public var EditTheme_Edit_Preview_IncomingText: String { return self._s[2808]! } - public var Message_PinnedGame: String { return self._s[2809]! } - public var VoiceOver_Chat_ForwardedFromYou: String { return self._s[2810]! } - public var Notifications_Badge_CountUnreadMessages_InfoOff: String { return self._s[2812]! } - public var Login_CallRequestState2: String { return self._s[2814]! } - public var CheckoutInfo_ReceiverInfoNamePlaceholder: String { return self._s[2816]! } + public var Passport_PasswordDescription: String { return self._s[2929]! } + public var Channel_MessagePhotoUpdated: String { return self._s[2930]! } + public var MediaPicker_TapToUngroupDescription: String { return self._s[2931]! } + public var SettingsSearch_Synonyms_Notifications_BadgeCountUnreadMessages: String { return self._s[2932]! } + public var AttachmentMenu_PhotoOrVideo: String { return self._s[2933]! } + public var Conversation_ContextMenuMore: String { return self._s[2934]! } + public var Privacy_PaymentsClearInfo: String { return self._s[2935]! } + public var CallSettings_TabIcon: String { return self._s[2936]! } + public var KeyCommand_Find: String { return self._s[2937]! } + public var ClearCache_FreeSpaceDescription: String { return self._s[2938]! } + public var Appearance_ThemePreview_ChatList_7_Text: String { return self._s[2939]! } + public var EditTheme_Edit_Preview_IncomingText: String { return self._s[2940]! } + public var Message_PinnedGame: String { return self._s[2941]! } + public var VoiceOver_Chat_ForwardedFromYou: String { return self._s[2942]! } + public var Notifications_Badge_CountUnreadMessages_InfoOff: String { return self._s[2944]! } + public var Login_CallRequestState2: String { return self._s[2946]! } + public var CheckoutInfo_ReceiverInfoNamePlaceholder: String { return self._s[2948]! } public func VoiceOver_Chat_PhotoFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2817]!, self._r[2817]!, [_0]) + return formatWithArgumentRanges(self._s[2949]!, self._r[2949]!, [_0]) } public func Checkout_PayPrice(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2819]!, self._r[2819]!, [_0]) + return formatWithArgumentRanges(self._s[2951]!, self._r[2951]!, [_0]) } - public var WallpaperPreview_Blurred: String { return self._s[2820]! } - public var Conversation_InstantPagePreview: String { return self._s[2821]! } + public var AuthSessions_AddDevice: String { return self._s[2952]! } + public var WallpaperPreview_Blurred: String { return self._s[2953]! } + public var Conversation_InstantPagePreview: String { return self._s[2954]! } + public var PeerInfo_ButtonUnmute: String { return self._s[2955]! } public func DialogList_SingleUploadingVideoSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2822]!, self._r[2822]!, [_0]) + return formatWithArgumentRanges(self._s[2956]!, self._r[2956]!, [_0]) } - public var SecretTimer_VideoDescription: String { return self._s[2825]! } - public var WallpaperSearch_ColorRed: String { return self._s[2826]! } - public var GroupPermission_NoPinMessages: String { return self._s[2827]! } - public var Passport_Language_es: String { return self._s[2828]! } - public var Permissions_ContactsAllow_v0: String { return self._s[2830]! } - public var Conversation_EditingMessageMediaEditCurrentVideo: String { return self._s[2831]! } + public var SecretTimer_VideoDescription: String { return self._s[2959]! } + public var WallpaperSearch_ColorRed: String { return self._s[2960]! } + public var GroupPermission_NoPinMessages: String { return self._s[2961]! } + public var Passport_Language_es: String { return self._s[2962]! } + public var Permissions_ContactsAllow_v0: String { return self._s[2964]! } + public var Conversation_EditingMessageMediaEditCurrentVideo: String { return self._s[2965]! } public func PUSH_CHAT_MESSAGE_CONTACT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2832]!, self._r[2832]!, [_1, _2]) + return formatWithArgumentRanges(self._s[2966]!, self._r[2966]!, [_1, _2]) } - public var Privacy_Forwards_CustomHelp: String { return self._s[2833]! } - public var WebPreview_GettingLinkInfo: String { return self._s[2834]! } - public var Watch_UserInfo_Unmute: String { return self._s[2835]! } - public var GroupInfo_ChannelListNamePlaceholder: String { return self._s[2836]! } - public var AccessDenied_CameraRestricted: String { return self._s[2838]! } + public var Privacy_Forwards_CustomHelp: String { return self._s[2967]! } + public var WebPreview_GettingLinkInfo: String { return self._s[2968]! } + public var Watch_UserInfo_Unmute: String { return self._s[2969]! } + public var GroupInfo_ChannelListNamePlaceholder: String { return self._s[2970]! } + public var AccessDenied_CameraRestricted: String { return self._s[2972]! } public func Conversation_Kilobytes(_ _0: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2839]!, self._r[2839]!, ["\(_0)"]) + return formatWithArgumentRanges(self._s[2973]!, self._r[2973]!, ["\(_0)"]) } - public var ChatList_ReadAll: String { return self._s[2841]! } - public var Settings_CopyUsername: String { return self._s[2842]! } - public var Contacts_SearchLabel: String { return self._s[2843]! } - public var Map_OpenInYandexNavigator: String { return self._s[2845]! } - public var PasscodeSettings_EncryptData: String { return self._s[2846]! } - public var Settings_Wallet: String { return self._s[2847]! } - public var Group_ErrorSupergroupConversionNotPossible: String { return self._s[2848]! } - public var WallpaperSearch_ColorPrefix: String { return self._s[2849]! } - public var Notifications_GroupNotificationsPreview: String { return self._s[2850]! } - public var DialogList_AdNoticeAlert: String { return self._s[2851]! } - public var Wallet_Month_GenMay: String { return self._s[2853]! } - public var CheckoutInfo_ShippingInfoAddress1: String { return self._s[2854]! } - public var CheckoutInfo_ShippingInfoAddress2: String { return self._s[2855]! } - public var Localization_LanguageCustom: String { return self._s[2856]! } - public var Passport_Identity_TypeDriversLicenseUploadScan: String { return self._s[2857]! } - public var CallFeedback_Title: String { return self._s[2858]! } - public var VoiceOver_Chat_RecordPreviewVoiceMessage: String { return self._s[2861]! } - public var Passport_Address_OneOfTypePassportRegistration: String { return self._s[2862]! } - public var Wallet_Intro_CreateErrorTitle: String { return self._s[2863]! } - public var Conversation_InfoGroup: String { return self._s[2864]! } - public var Compose_NewMessage: String { return self._s[2865]! } - public var FastTwoStepSetup_HintPlaceholder: String { return self._s[2866]! } - public var ChatSettings_AutoDownloadVideoMessages: String { return self._s[2867]! } - public var Wallet_SecureStorageReset_BiometryFaceId: String { return self._s[2868]! } - public var Channel_DiscussionGroup_UnlinkChannel: String { return self._s[2869]! } + public var ChatList_ReadAll: String { return self._s[2975]! } + public var Settings_CopyUsername: String { return self._s[2976]! } + public var Contacts_SearchLabel: String { return self._s[2977]! } + public var Map_OpenInYandexNavigator: String { return self._s[2979]! } + public var PasscodeSettings_EncryptData: String { return self._s[2980]! } + public var Settings_Wallet: String { return self._s[2981]! } + public var Group_ErrorSupergroupConversionNotPossible: String { return self._s[2982]! } + public var WallpaperSearch_ColorPrefix: String { return self._s[2983]! } + public var Notifications_GroupNotificationsPreview: String { return self._s[2984]! } + public var DialogList_AdNoticeAlert: String { return self._s[2985]! } + public var Wallet_Month_GenMay: String { return self._s[2987]! } + public var CheckoutInfo_ShippingInfoAddress1: String { return self._s[2988]! } + public var CheckoutInfo_ShippingInfoAddress2: String { return self._s[2989]! } + public var Localization_LanguageCustom: String { return self._s[2990]! } + public var Passport_Identity_TypeDriversLicenseUploadScan: String { return self._s[2991]! } + public var CallFeedback_Title: String { return self._s[2992]! } + public var VoiceOver_Chat_RecordPreviewVoiceMessage: String { return self._s[2995]! } + public var Passport_Address_OneOfTypePassportRegistration: String { return self._s[2996]! } + public var Wallet_Intro_CreateErrorTitle: String { return self._s[2997]! } + public var Conversation_InfoGroup: String { return self._s[2998]! } + public var Compose_NewMessage: String { return self._s[2999]! } + public var FastTwoStepSetup_HintPlaceholder: String { return self._s[3000]! } + public var ChatSettings_AutoDownloadVideoMessages: String { return self._s[3001]! } + public var Wallet_SecureStorageReset_BiometryFaceId: String { return self._s[3002]! } + public var Channel_DiscussionGroup_UnlinkChannel: String { return self._s[3003]! } public func Passport_Scans_ScanIndex(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2870]!, self._r[2870]!, [_0]) + return formatWithArgumentRanges(self._s[3004]!, self._r[3004]!, [_0]) } - public var Channel_AdminLog_CanDeleteMessages: String { return self._s[2871]! } - public var Login_CancelSignUpConfirmation: String { return self._s[2872]! } - public var ChangePhoneNumberCode_Help: String { return self._s[2873]! } - public var PrivacySettings_DeleteAccountHelp: String { return self._s[2874]! } - public var Channel_BlackList_Title: String { return self._s[2875]! } - public var UserInfo_PhoneCall: String { return self._s[2876]! } - public var Passport_Address_OneOfTypeBankStatement: String { return self._s[2878]! } - public var Wallet_Month_ShortJanuary: String { return self._s[2879]! } - public var State_connecting: String { return self._s[2880]! } - public var Appearance_ThemePreview_ChatList_6_Text: String { return self._s[2881]! } - public var Wallet_Month_GenMarch: String { return self._s[2882]! } - public var EditTheme_Expand_BottomInfo: String { return self._s[2883]! } + public var Channel_AdminLog_CanDeleteMessages: String { return self._s[3005]! } + public var Login_CancelSignUpConfirmation: String { return self._s[3006]! } + public var ChangePhoneNumberCode_Help: String { return self._s[3007]! } + public var PrivacySettings_DeleteAccountHelp: String { return self._s[3008]! } + public var Channel_BlackList_Title: String { return self._s[3009]! } + public var UserInfo_PhoneCall: String { return self._s[3010]! } + public var Passport_Address_OneOfTypeBankStatement: String { return self._s[3012]! } + public var Wallet_Month_ShortJanuary: String { return self._s[3013]! } + public var State_connecting: String { return self._s[3014]! } + public var Appearance_ThemePreview_ChatList_6_Text: String { return self._s[3015]! } + public var Wallet_Month_GenMarch: String { return self._s[3016]! } + public var EditTheme_Expand_BottomInfo: String { return self._s[3017]! } + public var AuthSessions_AddedDeviceTerminate: String { return self._s[3018]! } public func LastSeen_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2884]!, self._r[2884]!, [_0]) + return formatWithArgumentRanges(self._s[3019]!, self._r[3019]!, [_0]) } public func DialogList_SingleRecordingAudioSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2885]!, self._r[2885]!, [_0]) + return formatWithArgumentRanges(self._s[3020]!, self._r[3020]!, [_0]) } - public var Notifications_GroupNotifications: String { return self._s[2886]! } - public var Conversation_SendMessageErrorTooMuchScheduled: String { return self._s[2887]! } - public var Passport_Identity_EditPassport: String { return self._s[2888]! } - public var EnterPasscode_RepeatNewPasscode: String { return self._s[2890]! } - public var Localization_EnglishLanguageName: String { return self._s[2891]! } - public var Share_AuthDescription: String { return self._s[2892]! } - public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsAlert: String { return self._s[2893]! } - public var Passport_Identity_Surname: String { return self._s[2894]! } - public var Compose_TokenListPlaceholder: String { return self._s[2895]! } - public var Wallet_AccessDenied_Camera: String { return self._s[2896]! } - public var Passport_Identity_OneOfTypePassport: String { return self._s[2897]! } - public var Settings_AboutEmpty: String { return self._s[2898]! } - public var Conversation_Unmute: String { return self._s[2899]! } - public var CreateGroup_ChannelsTooMuch: String { return self._s[2901]! } - public var Wallet_Sending_Text: String { return self._s[2902]! } + public var Notifications_GroupNotifications: String { return self._s[3021]! } + public var Conversation_SendMessageErrorTooMuchScheduled: String { return self._s[3022]! } + public var Passport_Identity_EditPassport: String { return self._s[3023]! } + public var EnterPasscode_RepeatNewPasscode: String { return self._s[3025]! } + public var Localization_EnglishLanguageName: String { return self._s[3026]! } + public var Share_AuthDescription: String { return self._s[3027]! } + public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsAlert: String { return self._s[3028]! } + public var Passport_Identity_Surname: String { return self._s[3029]! } + public var Compose_TokenListPlaceholder: String { return self._s[3030]! } + public var Wallet_AccessDenied_Camera: String { return self._s[3031]! } + public var Passport_Identity_OneOfTypePassport: String { return self._s[3032]! } + public var Settings_AboutEmpty: String { return self._s[3033]! } + public var Conversation_Unmute: String { return self._s[3034]! } + public var CreateGroup_ChannelsTooMuch: String { return self._s[3036]! } + public var Wallet_Sending_Text: String { return self._s[3037]! } public func PUSH_CONTACT_JOINED(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2903]!, self._r[2903]!, [_1]) + return formatWithArgumentRanges(self._s[3038]!, self._r[3038]!, [_1]) } - public var Login_CodeSentCall: String { return self._s[2904]! } - public var ContactInfo_PhoneLabelHomeFax: String { return self._s[2906]! } - public var ChatSettings_Appearance: String { return self._s[2907]! } - public var ClearCache_StorageUsage: String { return self._s[2908]! } - public var Appearance_PickAccentColor: String { return self._s[2909]! } + public var Login_CodeSentCall: String { return self._s[3039]! } + public var ContactInfo_PhoneLabelHomeFax: String { return self._s[3041]! } + public var ChatSettings_Appearance: String { return self._s[3042]! } + public var ClearCache_StorageUsage: String { return self._s[3043]! } + public var Appearance_PickAccentColor: String { return self._s[3044]! } public func PUSH_CHAT_MESSAGE_NOTEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2910]!, self._r[2910]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3045]!, self._r[3045]!, [_1, _2]) } public func PUSH_MESSAGE_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2911]!, self._r[2911]!, [_1]) + return formatWithArgumentRanges(self._s[3046]!, self._r[3046]!, [_1]) } - public var Notification_CallMissed: String { return self._s[2912]! } - public var SettingsSearch_Synonyms_Appearance_ChatBackground_Custom: String { return self._s[2913]! } - public var Channel_AdminLogFilter_EventsInfo: String { return self._s[2914]! } - public var Wallet_Month_GenOctober: String { return self._s[2916]! } - public var ChatAdmins_AdminLabel: String { return self._s[2917]! } - public var KeyCommand_JumpToNextChat: String { return self._s[2918]! } - public var Conversation_StopPollConfirmationTitle: String { return self._s[2920]! } - public var ChangePhoneNumberCode_CodePlaceholder: String { return self._s[2921]! } - public var Month_GenJune: String { return self._s[2922]! } - public var Watch_Location_Current: String { return self._s[2923]! } - public var Wallet_Receive_CopyInvoiceUrl: String { return self._s[2924]! } - public var Conversation_TitleMute: String { return self._s[2925]! } + public var Notification_CallMissed: String { return self._s[3047]! } + public var SettingsSearch_Synonyms_Appearance_ChatBackground_Custom: String { return self._s[3048]! } + public var Channel_AdminLogFilter_EventsInfo: String { return self._s[3049]! } + public var Wallet_Month_GenOctober: String { return self._s[3051]! } + public var ChatAdmins_AdminLabel: String { return self._s[3052]! } + public var KeyCommand_JumpToNextChat: String { return self._s[3053]! } + public var Conversation_StopPollConfirmationTitle: String { return self._s[3055]! } + public var ChangePhoneNumberCode_CodePlaceholder: String { return self._s[3056]! } + public var Month_GenJune: String { return self._s[3057]! } + public var IntentsSettings_MainAccountInfo: String { return self._s[3058]! } + public var Watch_Location_Current: String { return self._s[3059]! } + public var Wallet_Receive_CopyInvoiceUrl: String { return self._s[3060]! } + public var Conversation_TitleMute: String { return self._s[3061]! } + public var Map_PlacesInThisArea: String { return self._s[3062]! } public func PUSH_CHANNEL_MESSAGE_ROUND(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2926]!, self._r[2926]!, [_1]) + return formatWithArgumentRanges(self._s[3063]!, self._r[3063]!, [_1]) } - public var GroupInfo_DeleteAndExit: String { return self._s[2927]! } + public var GroupInfo_DeleteAndExit: String { return self._s[3064]! } public func Conversation_Moderate_DeleteAllMessages(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2928]!, self._r[2928]!, [_0]) + return formatWithArgumentRanges(self._s[3065]!, self._r[3065]!, [_0]) } - public var Call_ReportPlaceholder: String { return self._s[2929]! } - public var Chat_SlowmodeSendError: String { return self._s[2930]! } - public var MaskStickerSettings_Info: String { return self._s[2931]! } - public var EditTheme_Expand_TopInfo: String { return self._s[2932]! } + public var Call_ReportPlaceholder: String { return self._s[3066]! } + public var Chat_SlowmodeSendError: String { return self._s[3067]! } + public var MaskStickerSettings_Info: String { return self._s[3068]! } + public var EditTheme_Expand_TopInfo: String { return self._s[3069]! } public func GroupInfo_AddParticipantConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2933]!, self._r[2933]!, [_0]) + return formatWithArgumentRanges(self._s[3070]!, self._r[3070]!, [_0]) } - public var Checkout_NewCard_PostcodeTitle: String { return self._s[2934]! } - public var Passport_Address_RegionPlaceholder: String { return self._s[2936]! } - public var Contacts_ShareTelegram: String { return self._s[2937]! } - public var EnterPasscode_EnterNewPasscodeNew: String { return self._s[2938]! } - public var Channel_ErrorAccessDenied: String { return self._s[2939]! } - public var UserInfo_ScamBotWarning: String { return self._s[2941]! } - public var Stickers_GroupChooseStickerPack: String { return self._s[2942]! } - public var Call_ConnectionErrorTitle: String { return self._s[2943]! } - public var UserInfo_NotificationsEnable: String { return self._s[2944]! } - public var ArchivedChats_IntroText1: String { return self._s[2945]! } - public var Tour_Text4: String { return self._s[2948]! } - public var WallpaperSearch_Recent: String { return self._s[2949]! } - public var GroupInfo_ScamGroupWarning: String { return self._s[2950]! } - public var Profile_MessageLifetime2s: String { return self._s[2952]! } - public var Appearance_ThemePreview_ChatList_5_Text: String { return self._s[2953]! } - public var Notification_MessageLifetime2s: String { return self._s[2954]! } + public var Checkout_NewCard_PostcodeTitle: String { return self._s[3071]! } + public var Passport_Address_RegionPlaceholder: String { return self._s[3073]! } + public var Contacts_ShareTelegram: String { return self._s[3074]! } + public var EnterPasscode_EnterNewPasscodeNew: String { return self._s[3075]! } + public var Map_AddressOnMap: String { return self._s[3076]! } + public var Channel_ErrorAccessDenied: String { return self._s[3077]! } + public var UserInfo_ScamBotWarning: String { return self._s[3079]! } + public var Stickers_GroupChooseStickerPack: String { return self._s[3080]! } + public var Call_ConnectionErrorTitle: String { return self._s[3081]! } + public var UserInfo_NotificationsEnable: String { return self._s[3082]! } + public var ArchivedChats_IntroText1: String { return self._s[3083]! } + public var Tour_Text4: String { return self._s[3086]! } + public var WallpaperSearch_Recent: String { return self._s[3087]! } + public var GroupInfo_ScamGroupWarning: String { return self._s[3088]! } + public var PeopleNearby_MakeVisibleTitle: String { return self._s[3089]! } + public var Profile_MessageLifetime2s: String { return self._s[3091]! } + public var Appearance_ThemePreview_ChatList_5_Text: String { return self._s[3092]! } + public var Notification_MessageLifetime2s: String { return self._s[3093]! } public func Time_PreciseDate_m10(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2955]!, self._r[2955]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[3094]!, self._r[3094]!, [_1, _2, _3]) } - public var Cache_ClearCache: String { return self._s[2956]! } - public var AutoNightTheme_UpdateLocation: String { return self._s[2957]! } - public var Permissions_NotificationsUnreachableText_v0: String { return self._s[2958]! } + public var Cache_ClearCache: String { return self._s[3095]! } + public var AutoNightTheme_UpdateLocation: String { return self._s[3096]! } + public var Permissions_NotificationsUnreachableText_v0: String { return self._s[3097]! } public func Channel_AdminLog_MessageChangedGroupUsername(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2962]!, self._r[2962]!, [_0]) + return formatWithArgumentRanges(self._s[3101]!, self._r[3101]!, [_0]) } public func Conversation_ShareMyPhoneNumber_StatusSuccess(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2964]!, self._r[2964]!, [_0]) + return formatWithArgumentRanges(self._s[3103]!, self._r[3103]!, [_0]) } - public var LocalGroup_Text: String { return self._s[2965]! } - public var Channel_AdminLog_EmptyFilterTitle: String { return self._s[2966]! } - public var SocksProxySetup_TypeSocks: String { return self._s[2967]! } - public var ChatList_UnarchiveAction: String { return self._s[2968]! } - public var AutoNightTheme_Title: String { return self._s[2969]! } - public var InstantPage_FeedbackButton: String { return self._s[2970]! } - public var Passport_FieldAddress: String { return self._s[2971]! } + public var LocalGroup_Text: String { return self._s[3104]! } + public var PeerInfo_PaneMembers: String { return self._s[3105]! } + public var Channel_AdminLog_EmptyFilterTitle: String { return self._s[3106]! } + public var SocksProxySetup_TypeSocks: String { return self._s[3107]! } + public var ChatList_UnarchiveAction: String { return self._s[3108]! } + public var AutoNightTheme_Title: String { return self._s[3109]! } + public var InstantPage_FeedbackButton: String { return self._s[3110]! } + public var Passport_FieldAddress: String { return self._s[3111]! } public func Channel_AdminLog_SetSlowmode(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2972]!, self._r[2972]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3112]!, self._r[3112]!, [_1, _2]) } - public var Month_ShortMarch: String { return self._s[2973]! } + public var Month_ShortMarch: String { return self._s[3113]! } public func PUSH_MESSAGE_INVOICE(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2974]!, self._r[2974]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3114]!, self._r[3114]!, [_1, _2]) } - public var SocksProxySetup_UsernamePlaceholder: String { return self._s[2975]! } - public var Conversation_ShareInlineBotLocationConfirmation: String { return self._s[2976]! } - public var Passport_FloodError: String { return self._s[2977]! } - public var SecretGif_Title: String { return self._s[2978]! } - public var NotificationSettings_ShowNotificationsAllAccountsInfoOn: String { return self._s[2979]! } - public var ChatList_Context_UnhideArchive: String { return self._s[2980]! } - public var Passport_Language_th: String { return self._s[2982]! } - public var Passport_Address_Address: String { return self._s[2983]! } - public var Login_InvalidLastNameError: String { return self._s[2984]! } - public var Notifications_InAppNotificationsPreview: String { return self._s[2985]! } - public var Notifications_PermissionsUnreachableTitle: String { return self._s[2986]! } - public var ChatList_Context_Archive: String { return self._s[2987]! } - public var SettingsSearch_FAQ: String { return self._s[2988]! } - public var ShareMenu_Send: String { return self._s[2989]! } - public var WallpaperSearch_ColorYellow: String { return self._s[2991]! } - public var Month_GenNovember: String { return self._s[2993]! } - public var SettingsSearch_Synonyms_Appearance_LargeEmoji: String { return self._s[2995]! } + public var SocksProxySetup_UsernamePlaceholder: String { return self._s[3115]! } + public var Conversation_ShareInlineBotLocationConfirmation: String { return self._s[3116]! } + public var Passport_FloodError: String { return self._s[3117]! } + public var SecretGif_Title: String { return self._s[3118]! } + public var NotificationSettings_ShowNotificationsAllAccountsInfoOn: String { return self._s[3119]! } + public var ChatList_Context_UnhideArchive: String { return self._s[3120]! } + public var Passport_Language_th: String { return self._s[3122]! } + public var Passport_Address_Address: String { return self._s[3123]! } + public var Login_InvalidLastNameError: String { return self._s[3124]! } + public var Notifications_InAppNotificationsPreview: String { return self._s[3125]! } + public var Notifications_PermissionsUnreachableTitle: String { return self._s[3126]! } + public var ChatList_Context_Archive: String { return self._s[3127]! } + public var SettingsSearch_FAQ: String { return self._s[3128]! } + public var ShareMenu_Send: String { return self._s[3129]! } + public var ChatState_Connecting: String { return self._s[3130]! } + public var WallpaperSearch_ColorYellow: String { return self._s[3132]! } + public var Month_GenNovember: String { return self._s[3134]! } + public var SettingsSearch_Synonyms_Appearance_LargeEmoji: String { return self._s[3136]! } public func Conversation_ShareMyPhoneNumberConfirmation(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2996]!, self._r[2996]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3137]!, self._r[3137]!, [_1, _2]) } - public var Conversation_SwipeToReplyHintText: String { return self._s[2997]! } - public var Checkout_Email: String { return self._s[2998]! } - public var NotificationsSound_Tritone: String { return self._s[2999]! } - public var StickerPacksSettings_ManagingHelp: String { return self._s[3001]! } - public var Wallet_ContextMenuCopy: String { return self._s[3003]! } + public var Conversation_SwipeToReplyHintText: String { return self._s[3138]! } + public var Checkout_Email: String { return self._s[3139]! } + public var NotificationsSound_Tritone: String { return self._s[3140]! } + public var StickerPacksSettings_ManagingHelp: String { return self._s[3142]! } + public var Wallet_ContextMenuCopy: String { return self._s[3144]! } public func Wallet_Time_PreciseDate_m6(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3005]!, self._r[3005]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[3146]!, self._r[3146]!, [_1, _2, _3]) } + public var Appearance_TextSize_Automatic: String { return self._s[3147]! } public func PUSH_PINNED_ROUND(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3006]!, self._r[3006]!, [_1]) + return formatWithArgumentRanges(self._s[3148]!, self._r[3148]!, [_1]) } - public var ChangePhoneNumberNumber_Help: String { return self._s[3007]! } + public func StickerPackActionInfo_AddedText(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3149]!, self._r[3149]!, [_0]) + } + public var ChangePhoneNumberNumber_Help: String { return self._s[3150]! } public func Checkout_LiabilityAlert(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3008]!, self._r[3008]!, [_1, _1, _1, _2]) + return formatWithArgumentRanges(self._s[3151]!, self._r[3151]!, [_1, _1, _1, _2]) } - public var ChatList_UndoArchiveTitle: String { return self._s[3009]! } - public var Notification_Exceptions_Add: String { return self._s[3010]! } - public var DialogList_You: String { return self._s[3011]! } - public var MediaPicker_Send: String { return self._s[3014]! } - public var SettingsSearch_Synonyms_Stickers_Title: String { return self._s[3015]! } - public var Appearance_ThemePreview_ChatList_4_Text: String { return self._s[3016]! } - public var Call_AudioRouteSpeaker: String { return self._s[3017]! } - public var Watch_UserInfo_Title: String { return self._s[3018]! } - public var VoiceOver_Chat_PollFinalResults: String { return self._s[3019]! } - public var Appearance_AccentColor: String { return self._s[3021]! } + public var ChatList_UndoArchiveTitle: String { return self._s[3152]! } + public var Notification_Exceptions_Add: String { return self._s[3153]! } + public var DialogList_You: String { return self._s[3154]! } + public var MediaPicker_Send: String { return self._s[3157]! } + public var SettingsSearch_Synonyms_Stickers_Title: String { return self._s[3158]! } + public var Appearance_ThemePreview_ChatList_4_Text: String { return self._s[3159]! } + public var Call_AudioRouteSpeaker: String { return self._s[3160]! } + public var Watch_UserInfo_Title: String { return self._s[3161]! } + public var VoiceOver_Chat_PollFinalResults: String { return self._s[3162]! } + public var Appearance_AccentColor: String { return self._s[3164]! } public func Login_EmailPhoneSubject(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3022]!, self._r[3022]!, [_0]) + return formatWithArgumentRanges(self._s[3165]!, self._r[3165]!, [_0]) } - public var Permissions_ContactsAllowInSettings_v0: String { return self._s[3023]! } + public var Permissions_ContactsAllowInSettings_v0: String { return self._s[3166]! } public func PUSH_CHANNEL_MESSAGE_GAME(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3024]!, self._r[3024]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3167]!, self._r[3167]!, [_1, _2]) } - public var Conversation_ClousStorageInfo_Description2: String { return self._s[3025]! } - public var WebSearch_RecentClearConfirmation: String { return self._s[3026]! } - public var Notification_CallOutgoing: String { return self._s[3027]! } - public var PrivacySettings_PasscodeAndFaceId: String { return self._s[3028]! } - public var Channel_DiscussionGroup_MakeHistoryPublic: String { return self._s[3029]! } - public var Call_RecordingDisabledMessage: String { return self._s[3030]! } - public var Message_Game: String { return self._s[3031]! } - public var Conversation_PressVolumeButtonForSound: String { return self._s[3032]! } - public var PrivacyLastSeenSettings_CustomHelp: String { return self._s[3033]! } - public var Channel_DiscussionGroup_PrivateGroup: String { return self._s[3034]! } - public var Channel_EditAdmin_PermissionAddAdmins: String { return self._s[3035]! } - public var Date_DialogDateFormat: String { return self._s[3036]! } - public var WallpaperColors_SetCustomColor: String { return self._s[3037]! } - public var Notifications_InAppNotifications: String { return self._s[3038]! } + public var Conversation_ClousStorageInfo_Description2: String { return self._s[3168]! } + public var WebSearch_RecentClearConfirmation: String { return self._s[3169]! } + public var Notification_CallOutgoing: String { return self._s[3170]! } + public var PrivacySettings_PasscodeAndFaceId: String { return self._s[3171]! } + public var Channel_DiscussionGroup_MakeHistoryPublic: String { return self._s[3172]! } + public var Call_RecordingDisabledMessage: String { return self._s[3173]! } + public var Message_Game: String { return self._s[3174]! } + public var Conversation_PressVolumeButtonForSound: String { return self._s[3175]! } + public var PrivacyLastSeenSettings_CustomHelp: String { return self._s[3176]! } + public var Channel_DiscussionGroup_PrivateGroup: String { return self._s[3177]! } + public var Channel_EditAdmin_PermissionAddAdmins: String { return self._s[3178]! } + public var Date_DialogDateFormat: String { return self._s[3180]! } + public var WallpaperColors_SetCustomColor: String { return self._s[3181]! } + public var Notifications_InAppNotifications: String { return self._s[3182]! } public func Channel_Management_RemovedBy(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3039]!, self._r[3039]!, [_0]) + return formatWithArgumentRanges(self._s[3183]!, self._r[3183]!, [_0]) } public func Settings_ApplyProxyAlert(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3040]!, self._r[3040]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3184]!, self._r[3184]!, [_1, _2]) } - public var NewContact_Title: String { return self._s[3041]! } + public var NewContact_Title: String { return self._s[3185]! } public func AutoDownloadSettings_UpToForAll(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3042]!, self._r[3042]!, [_0]) + return formatWithArgumentRanges(self._s[3186]!, self._r[3186]!, [_0]) } - public var Conversation_ViewContactDetails: String { return self._s[3043]! } + public var Conversation_ViewContactDetails: String { return self._s[3187]! } public func PUSH_CHANNEL_MESSAGE_CONTACT(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3045]!, self._r[3045]!, [_1]) + return formatWithArgumentRanges(self._s[3189]!, self._r[3189]!, [_1]) } - public var Checkout_NewCard_CardholderNameTitle: String { return self._s[3046]! } - public var Passport_Identity_ExpiryDateNone: String { return self._s[3047]! } - public var PrivacySettings_Title: String { return self._s[3048]! } - public var Conversation_SilentBroadcastTooltipOff: String { return self._s[3051]! } - public var GroupRemoved_UsersSectionTitle: String { return self._s[3052]! } - public var VoiceOver_Chat_ContactEmail: String { return self._s[3053]! } - public var Contacts_PhoneNumber: String { return self._s[3054]! } - public var TwoFactorSetup_Password_PlaceholderConfirmPassword: String { return self._s[3056]! } - public var Map_ShowPlaces: String { return self._s[3057]! } - public var ChatAdmins_Title: String { return self._s[3058]! } - public var InstantPage_Reference: String { return self._s[3060]! } - public var Wallet_Info_Updating: String { return self._s[3061]! } - public var ReportGroupLocation_Text: String { return self._s[3062]! } + public var Checkout_NewCard_CardholderNameTitle: String { return self._s[3190]! } + public var Passport_Identity_ExpiryDateNone: String { return self._s[3191]! } + public var PrivacySettings_Title: String { return self._s[3192]! } + public var Conversation_SilentBroadcastTooltipOff: String { return self._s[3195]! } + public var GroupRemoved_UsersSectionTitle: String { return self._s[3196]! } + public var VoiceOver_Chat_ContactEmail: String { return self._s[3197]! } + public var Contacts_PhoneNumber: String { return self._s[3198]! } + public var PeerInfo_ButtonMute: String { return self._s[3199]! } + public var TwoFactorSetup_Password_PlaceholderConfirmPassword: String { return self._s[3201]! } + public var Map_ShowPlaces: String { return self._s[3202]! } + public var ChatAdmins_Title: String { return self._s[3203]! } + public var InstantPage_Reference: String { return self._s[3205]! } + public var Wallet_Info_Updating: String { return self._s[3206]! } + public var ReportGroupLocation_Text: String { return self._s[3207]! } public func PUSH_CHAT_MESSAGE_FWD(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3063]!, self._r[3063]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3208]!, self._r[3208]!, [_1, _2]) } - public var Camera_FlashOff: String { return self._s[3064]! } - public var Watch_UserInfo_Block: String { return self._s[3065]! } - public var ChatSettings_Stickers: String { return self._s[3066]! } - public var ChatSettings_DownloadInBackground: String { return self._s[3067]! } - public var Appearance_ThemeCarouselTintedNight: String { return self._s[3068]! } + public var Camera_FlashOff: String { return self._s[3209]! } + public var Watch_UserInfo_Block: String { return self._s[3210]! } + public var ChatSettings_Stickers: String { return self._s[3211]! } + public var ChatSettings_DownloadInBackground: String { return self._s[3212]! } + public var Appearance_ThemeCarouselTintedNight: String { return self._s[3213]! } public func UserInfo_BlockConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3069]!, self._r[3069]!, [_0]) + return formatWithArgumentRanges(self._s[3214]!, self._r[3214]!, [_0]) } - public var Settings_ViewPhoto: String { return self._s[3070]! } - public var Login_CheckOtherSessionMessages: String { return self._s[3071]! } - public var AutoDownloadSettings_Cellular: String { return self._s[3072]! } - public var Wallet_Created_ExportErrorTitle: String { return self._s[3073]! } - public var SettingsSearch_Synonyms_Notifications_GroupNotificationsExceptions: String { return self._s[3074]! } - public var VoiceOver_MessageContextShare: String { return self._s[3075]! } + public var Settings_ViewPhoto: String { return self._s[3215]! } + public var Login_CheckOtherSessionMessages: String { return self._s[3216]! } + public var AutoDownloadSettings_Cellular: String { return self._s[3217]! } + public var Wallet_Created_ExportErrorTitle: String { return self._s[3218]! } + public var SettingsSearch_Synonyms_Notifications_GroupNotificationsExceptions: String { return self._s[3219]! } + public var VoiceOver_MessageContextShare: String { return self._s[3220]! } public func Target_InviteToGroupConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3077]!, self._r[3077]!, [_0]) + return formatWithArgumentRanges(self._s[3222]!, self._r[3222]!, [_0]) } - public var Privacy_DeleteDrafts: String { return self._s[3078]! } - public var Wallpaper_SetCustomBackgroundInfo: String { return self._s[3079]! } + public var Privacy_DeleteDrafts: String { return self._s[3223]! } + public var Wallpaper_SetCustomBackgroundInfo: String { return self._s[3224]! } public func LastSeen_AtDate(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3080]!, self._r[3080]!, [_0]) + return formatWithArgumentRanges(self._s[3225]!, self._r[3225]!, [_0]) } - public var DialogList_SavedMessagesHelp: String { return self._s[3081]! } - public var Wallet_SecureStorageNotAvailable_Title: String { return self._s[3082]! } - public var DialogList_SavedMessages: String { return self._s[3083]! } - public var GroupInfo_UpgradeButton: String { return self._s[3084]! } - public var Appearance_ThemePreview_ChatList_3_Text: String { return self._s[3086]! } - public var DialogList_Pin: String { return self._s[3087]! } + public var DialogList_SavedMessagesHelp: String { return self._s[3226]! } + public var Wallet_SecureStorageNotAvailable_Title: String { return self._s[3227]! } + public var DialogList_SavedMessages: String { return self._s[3228]! } + public var GroupInfo_UpgradeButton: String { return self._s[3229]! } + public var Appearance_ThemePreview_ChatList_3_Text: String { return self._s[3231]! } + public var DialogList_Pin: String { return self._s[3232]! } public func ForwardedAuthors2(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3088]!, self._r[3088]!, [_0, _1]) + return formatWithArgumentRanges(self._s[3233]!, self._r[3233]!, [_0, _1]) } public func Login_PhoneGenericEmailSubject(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3089]!, self._r[3089]!, [_0]) + return formatWithArgumentRanges(self._s[3234]!, self._r[3234]!, [_0]) } - public var Notification_Exceptions_AlwaysOn: String { return self._s[3090]! } - public var UserInfo_NotificationsDisable: String { return self._s[3091]! } - public var Paint_Outlined: String { return self._s[3092]! } - public var Activity_PlayingGame: String { return self._s[3093]! } - public var SearchImages_NoImagesFound: String { return self._s[3094]! } - public var SocksProxySetup_ProxyType: String { return self._s[3095]! } - public var AppleWatch_ReplyPresetsHelp: String { return self._s[3097]! } - public var Conversation_ContextMenuCancelSending: String { return self._s[3098]! } - public var Settings_AppLanguage: String { return self._s[3099]! } - public var TwoStepAuth_ResetAccountHelp: String { return self._s[3100]! } - public var Common_ChoosePhoto: String { return self._s[3101]! } - public var CallFeedback_ReasonEcho: String { return self._s[3102]! } + public var Notification_Exceptions_AlwaysOn: String { return self._s[3235]! } + public var UserInfo_NotificationsDisable: String { return self._s[3236]! } + public var Conversation_ContextMenuCancelEditing: String { return self._s[3237]! } + public var Paint_Outlined: String { return self._s[3238]! } + public var Activity_PlayingGame: String { return self._s[3239]! } + public var SearchImages_NoImagesFound: String { return self._s[3240]! } + public var SocksProxySetup_ProxyType: String { return self._s[3241]! } + public var AppleWatch_ReplyPresetsHelp: String { return self._s[3243]! } + public var Conversation_ContextMenuCancelSending: String { return self._s[3244]! } + public var Settings_AppLanguage: String { return self._s[3245]! } + public var TwoStepAuth_ResetAccountHelp: String { return self._s[3246]! } + public var Common_ChoosePhoto: String { return self._s[3247]! } + public var AuthSessions_AddDevice_InvalidQRCode: String { return self._s[3248]! } + public var CallFeedback_ReasonEcho: String { return self._s[3249]! } public func PUSH_PINNED_AUDIO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3103]!, self._r[3103]!, [_1]) + return formatWithArgumentRanges(self._s[3250]!, self._r[3250]!, [_1]) } - public var Privacy_Calls_AlwaysAllow: String { return self._s[3104]! } - public var Activity_UploadingVideo: String { return self._s[3105]! } - public var Conversation_WalletRequiredNotNow: String { return self._s[3106]! } - public var ChannelInfo_DeleteChannelConfirmation: String { return self._s[3107]! } - public var NetworkUsageSettings_Wifi: String { return self._s[3108]! } - public var VoiceOver_Editing_ClearText: String { return self._s[3109]! } - public var PUSH_SENDER_YOU: String { return self._s[3110]! } - public var Channel_BanUser_PermissionReadMessages: String { return self._s[3111]! } - public var Checkout_PayWithTouchId: String { return self._s[3112]! } - public var Wallpaper_ResetWallpapersConfirmation: String { return self._s[3113]! } + public var Privacy_Calls_AlwaysAllow: String { return self._s[3251]! } + public var PollResults_Collapse: String { return self._s[3252]! } + public var Activity_UploadingVideo: String { return self._s[3253]! } + public var Conversation_WalletRequiredNotNow: String { return self._s[3254]! } + public var ChannelInfo_DeleteChannelConfirmation: String { return self._s[3255]! } + public var NetworkUsageSettings_Wifi: String { return self._s[3256]! } + public var VoiceOver_Editing_ClearText: String { return self._s[3257]! } + public var PUSH_SENDER_YOU: String { return self._s[3258]! } + public var Channel_BanUser_PermissionReadMessages: String { return self._s[3259]! } + public var Checkout_PayWithTouchId: String { return self._s[3260]! } + public var Wallpaper_ResetWallpapersConfirmation: String { return self._s[3261]! } public func PUSH_LOCKED_MESSAGE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3115]!, self._r[3115]!, [_1]) + return formatWithArgumentRanges(self._s[3263]!, self._r[3263]!, [_1]) } - public var Notifications_ExceptionsNone: String { return self._s[3116]! } + public var Notifications_ExceptionsNone: String { return self._s[3264]! } public func Message_ForwardedMessageShort(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3117]!, self._r[3117]!, [_0]) + return formatWithArgumentRanges(self._s[3265]!, self._r[3265]!, [_0]) } public func PUSH_PINNED_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3118]!, self._r[3118]!, [_1]) + return formatWithArgumentRanges(self._s[3266]!, self._r[3266]!, [_1]) } - public var AuthSessions_IncompleteAttempts: String { return self._s[3120]! } - public var Passport_Address_Region: String { return self._s[3123]! } - public var ChatList_DeleteChat: String { return self._s[3124]! } - public var LogoutOptions_ClearCacheTitle: String { return self._s[3125]! } - public var PhotoEditor_TiltShift: String { return self._s[3126]! } - public var Settings_FAQ_URL: String { return self._s[3127]! } - public var TwoFactorSetup_EmailVerification_ChangeAction: String { return self._s[3128]! } - public var Passport_Language_sl: String { return self._s[3129]! } - public var Settings_PrivacySettings: String { return self._s[3131]! } - public var SharedMedia_TitleLink: String { return self._s[3132]! } - public var Passport_Identity_TypePassportUploadScan: String { return self._s[3133]! } - public var Settings_SetProfilePhoto: String { return self._s[3134]! } - public var Channel_About_Help: String { return self._s[3135]! } - public var Contacts_PermissionsEnable: String { return self._s[3136]! } - public var Wallet_Sending_Title: String { return self._s[3137]! } - public var SettingsSearch_Synonyms_Notifications_GroupNotificationsAlert: String { return self._s[3138]! } - public var AttachmentMenu_SendAsFiles: String { return self._s[3139]! } - public var CallFeedback_ReasonInterruption: String { return self._s[3141]! } - public var Passport_Address_AddTemporaryRegistration: String { return self._s[3142]! } - public var AutoDownloadSettings_AutodownloadVideos: String { return self._s[3143]! } - public var ChatSettings_AutoDownloadSettings_Delimeter: String { return self._s[3144]! } - public var PrivacySettings_DeleteAccountTitle: String { return self._s[3145]! } - public var AccessDenied_VideoMessageCamera: String { return self._s[3147]! } - public var Map_OpenInYandexMaps: String { return self._s[3149]! } - public var CreateGroup_ErrorLocatedGroupsTooMuch: String { return self._s[3150]! } - public var VoiceOver_MessageContextReply: String { return self._s[3151]! } - public var PhotoEditor_SaturationTool: String { return self._s[3152]! } + public var AuthSessions_IncompleteAttempts: String { return self._s[3268]! } + public var Passport_Address_Region: String { return self._s[3271]! } + public var ChatList_DeleteChat: String { return self._s[3272]! } + public var LogoutOptions_ClearCacheTitle: String { return self._s[3273]! } + public var PhotoEditor_TiltShift: String { return self._s[3274]! } + public var Settings_FAQ_URL: String { return self._s[3275]! } + public var TwoFactorSetup_EmailVerification_ChangeAction: String { return self._s[3276]! } + public var Passport_Language_sl: String { return self._s[3277]! } + public var Settings_PrivacySettings: String { return self._s[3279]! } + public var SharedMedia_TitleLink: String { return self._s[3280]! } + public var Passport_Identity_TypePassportUploadScan: String { return self._s[3281]! } + public var Settings_SetProfilePhoto: String { return self._s[3282]! } + public var Channel_About_Help: String { return self._s[3283]! } + public var Contacts_PermissionsEnable: String { return self._s[3284]! } + public var Wallet_Sending_Title: String { return self._s[3285]! } + public var PeerInfo_PaneMedia: String { return self._s[3286]! } + public var SettingsSearch_Synonyms_Notifications_GroupNotificationsAlert: String { return self._s[3287]! } + public var AttachmentMenu_SendAsFiles: String { return self._s[3288]! } + public var CallFeedback_ReasonInterruption: String { return self._s[3290]! } + public var Passport_Address_AddTemporaryRegistration: String { return self._s[3291]! } + public var AutoDownloadSettings_AutodownloadVideos: String { return self._s[3292]! } + public var ChatSettings_AutoDownloadSettings_Delimeter: String { return self._s[3293]! } + public var OldChannels_Title: String { return self._s[3294]! } + public var PrivacySettings_DeleteAccountTitle: String { return self._s[3295]! } + public var AccessDenied_VideoMessageCamera: String { return self._s[3297]! } + public var Map_OpenInYandexMaps: String { return self._s[3299]! } + public var CreateGroup_ErrorLocatedGroupsTooMuch: String { return self._s[3300]! } + public var VoiceOver_MessageContextReply: String { return self._s[3301]! } + public var PhotoEditor_SaturationTool: String { return self._s[3303]! } public func PUSH_MESSAGE_STICKER(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3153]!, self._r[3153]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3304]!, self._r[3304]!, [_1, _2]) } - public var PrivacyPhoneNumberSettings_CustomHelp: String { return self._s[3154]! } - public var Notification_Exceptions_NewException_NotificationHeader: String { return self._s[3155]! } - public var Group_OwnershipTransfer_ErrorLocatedGroupsTooMuch: String { return self._s[3156]! } - public var Appearance_TextSize: String { return self._s[3157]! } + public var PrivacyPhoneNumberSettings_CustomHelp: String { return self._s[3305]! } + public var Notification_Exceptions_NewException_NotificationHeader: String { return self._s[3306]! } + public var Group_OwnershipTransfer_ErrorLocatedGroupsTooMuch: String { return self._s[3307]! } public func LOCAL_MESSAGE_FWDS(_ _1: String, _ _2: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3158]!, self._r[3158]!, [_1, "\(_2)"]) + return formatWithArgumentRanges(self._s[3308]!, self._r[3308]!, [_1, "\(_2)"]) } - public var Appearance_ThemePreview_ChatList_2_Text: String { return self._s[3159]! } - public var Channel_Username_InvalidTooShort: String { return self._s[3161]! } - public var SettingsSearch_Synonyms_Wallet: String { return self._s[3162]! } + public var Appearance_ThemePreview_ChatList_2_Text: String { return self._s[3309]! } + public var Channel_Username_InvalidTooShort: String { return self._s[3311]! } + public var SettingsSearch_Synonyms_Wallet: String { return self._s[3312]! } public func Group_OwnershipTransfer_DescriptionInfo(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3163]!, self._r[3163]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3313]!, self._r[3313]!, [_1, _2]) } + public var Forward_ErrorPublicPollDisabledInChannels: String { return self._s[3314]! } public func PUSH_CHAT_MESSAGE_GAME(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3164]!, self._r[3164]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[3315]!, self._r[3315]!, [_1, _2, _3]) } - public var GroupInfo_PublicLinkAdd: String { return self._s[3165]! } - public var Passport_PassportInformation: String { return self._s[3168]! } - public var Theme_Unsupported: String { return self._s[3169]! } - public var WatchRemote_AlertTitle: String { return self._s[3170]! } - public var Privacy_GroupsAndChannels_NeverAllow: String { return self._s[3171]! } - public var ConvertToSupergroup_HelpText: String { return self._s[3173]! } + public var WallpaperPreview_PatternTitle: String { return self._s[3316]! } + public var GroupInfo_PublicLinkAdd: String { return self._s[3317]! } + public var Passport_PassportInformation: String { return self._s[3320]! } + public var Theme_Unsupported: String { return self._s[3321]! } + public var WatchRemote_AlertTitle: String { return self._s[3322]! } + public var Privacy_GroupsAndChannels_NeverAllow: String { return self._s[3323]! } + public var ConvertToSupergroup_HelpText: String { return self._s[3325]! } public func Time_MonthOfYear_m7(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3174]!, self._r[3174]!, [_0]) + return formatWithArgumentRanges(self._s[3326]!, self._r[3326]!, [_0]) } public func PUSH_PHONE_CALL_REQUEST(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3175]!, self._r[3175]!, [_1]) + return formatWithArgumentRanges(self._s[3327]!, self._r[3327]!, [_1]) } - public var Privacy_GroupsAndChannels_CustomHelp: String { return self._s[3176]! } - public var Wallet_Navigation_Done: String { return self._s[3178]! } - public var TwoStepAuth_RecoveryCodeInvalid: String { return self._s[3179]! } - public var AccessDenied_CameraDisabled: String { return self._s[3180]! } + public var Privacy_GroupsAndChannels_CustomHelp: String { return self._s[3328]! } + public var Wallet_Navigation_Done: String { return self._s[3330]! } + public var TwoStepAuth_RecoveryCodeInvalid: String { return self._s[3331]! } + public var AccessDenied_CameraDisabled: String { return self._s[3332]! } public func Channel_Username_UsernameIsAvailable(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3181]!, self._r[3181]!, [_0]) + return formatWithArgumentRanges(self._s[3333]!, self._r[3333]!, [_0]) } - public var ClearCache_Forever: String { return self._s[3182]! } - public var PhotoEditor_ContrastTool: String { return self._s[3185]! } + public var ClearCache_Forever: String { return self._s[3334]! } + public var AuthSessions_AddDeviceIntro_Title: String { return self._s[3335]! } + public var CreatePoll_Quiz: String { return self._s[3336]! } + public var PhotoEditor_ContrastTool: String { return self._s[3339]! } public func PUSH_PINNED_DOC(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3186]!, self._r[3186]!, [_1]) + return formatWithArgumentRanges(self._s[3340]!, self._r[3340]!, [_1]) } - public var DialogList_Draft: String { return self._s[3187]! } - public var Wallet_Configuration_BlockchainIdInfo: String { return self._s[3188]! } - public var Privacy_TopPeersDelete: String { return self._s[3190]! } - public var LoginPassword_PasswordPlaceholder: String { return self._s[3191]! } - public var Passport_Identity_TypeIdentityCardUploadScan: String { return self._s[3192]! } - public var WebSearch_RecentSectionClear: String { return self._s[3193]! } - public var EditTheme_ErrorInvalidCharacters: String { return self._s[3194]! } - public var Watch_ChatList_NoConversationsTitle: String { return self._s[3196]! } - public var Common_Done: String { return self._s[3198]! } - public var AuthSessions_EmptyText: String { return self._s[3199]! } - public var Wallet_Configuration_BlockchainNameChangedTitle: String { return self._s[3200]! } - public var Conversation_ShareBotContactConfirmation: String { return self._s[3201]! } - public var Tour_Title5: String { return self._s[3202]! } - public var Wallet_Settings_Title: String { return self._s[3203]! } + public var DialogList_Draft: String { return self._s[3341]! } + public var Wallet_Configuration_BlockchainIdInfo: String { return self._s[3342]! } + public func PeopleNearby_VisibleUntil(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3343]!, self._r[3343]!, [_0]) + } + public var Privacy_TopPeersDelete: String { return self._s[3345]! } + public var LoginPassword_PasswordPlaceholder: String { return self._s[3346]! } + public var Passport_Identity_TypeIdentityCardUploadScan: String { return self._s[3347]! } + public var WebSearch_RecentSectionClear: String { return self._s[3348]! } + public var EditTheme_ErrorInvalidCharacters: String { return self._s[3349]! } + public var Watch_ChatList_NoConversationsTitle: String { return self._s[3351]! } + public var PeerInfo_ButtonMore: String { return self._s[3353]! } + public var Common_Done: String { return self._s[3354]! } + public var Shortcut_SwitchAccount: String { return self._s[3355]! } + public var AuthSessions_EmptyText: String { return self._s[3356]! } + public var Wallet_Configuration_BlockchainNameChangedTitle: String { return self._s[3357]! } + public var Conversation_ShareBotContactConfirmation: String { return self._s[3358]! } + public var Tour_Title5: String { return self._s[3359]! } + public var Wallet_Settings_Title: String { return self._s[3360]! } public func Map_DirectionsDriveEta(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3204]!, self._r[3204]!, [_0]) + return formatWithArgumentRanges(self._s[3361]!, self._r[3361]!, [_0]) } - public var ApplyLanguage_UnsufficientDataTitle: String { return self._s[3205]! } - public var Conversation_LinkDialogSave: String { return self._s[3206]! } - public var GroupInfo_ActionRestrict: String { return self._s[3207]! } - public var Checkout_Title: String { return self._s[3208]! } - public var Channel_DiscussionGroup_HeaderLabel: String { return self._s[3210]! } - public var Channel_AdminLog_CanChangeInfo: String { return self._s[3212]! } - public var Notification_RenamedGroup: String { return self._s[3213]! } - public var PeopleNearby_Groups: String { return self._s[3214]! } - public var Checkout_PayWithFaceId: String { return self._s[3215]! } - public var Channel_BanList_BlockedTitle: String { return self._s[3216]! } - public var SettingsSearch_Synonyms_Notifications_InAppNotificationsSound: String { return self._s[3218]! } - public var Checkout_WebConfirmation_Title: String { return self._s[3219]! } - public var Notifications_MessageNotificationsAlert: String { return self._s[3220]! } + public var ApplyLanguage_UnsufficientDataTitle: String { return self._s[3362]! } + public var Conversation_LinkDialogSave: String { return self._s[3363]! } + public var GroupInfo_ActionRestrict: String { return self._s[3364]! } + public var Checkout_Title: String { return self._s[3365]! } + public var Channel_DiscussionGroup_HeaderLabel: String { return self._s[3367]! } + public var Channel_AdminLog_CanChangeInfo: String { return self._s[3369]! } + public var Notification_RenamedGroup: String { return self._s[3370]! } + public var PeopleNearby_Groups: String { return self._s[3371]! } + public var Checkout_PayWithFaceId: String { return self._s[3372]! } + public var Channel_BanList_BlockedTitle: String { return self._s[3373]! } + public var SettingsSearch_Synonyms_Notifications_InAppNotificationsSound: String { return self._s[3375]! } + public var Checkout_WebConfirmation_Title: String { return self._s[3376]! } + public var Notifications_MessageNotificationsAlert: String { return self._s[3377]! } public func Activity_RemindAboutGroup(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3221]!, self._r[3221]!, [_0]) + return formatWithArgumentRanges(self._s[3378]!, self._r[3378]!, [_0]) } - public var Profile_AddToExisting: String { return self._s[3223]! } + public var Profile_AddToExisting: String { return self._s[3380]! } public func Profile_CreateEncryptedChatOutdatedError(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3224]!, self._r[3224]!, [_0, _1]) + return formatWithArgumentRanges(self._s[3381]!, self._r[3381]!, [_0, _1]) } - public var Cache_Files: String { return self._s[3226]! } - public var Permissions_PrivacyPolicy: String { return self._s[3227]! } - public var SocksProxySetup_ConnectAndSave: String { return self._s[3228]! } - public var UserInfo_NotificationsDefaultDisabled: String { return self._s[3229]! } - public var AutoDownloadSettings_TypeContacts: String { return self._s[3231]! } - public var Appearance_ThemePreview_ChatList_1_Text: String { return self._s[3233]! } - public var Calls_NoCallsPlaceholder: String { return self._s[3234]! } + public var Cache_Files: String { return self._s[3383]! } + public var Permissions_PrivacyPolicy: String { return self._s[3384]! } + public var SocksProxySetup_ConnectAndSave: String { return self._s[3385]! } + public var UserInfo_NotificationsDefaultDisabled: String { return self._s[3386]! } + public var AutoDownloadSettings_TypeContacts: String { return self._s[3388]! } + public var Appearance_ThemePreview_ChatList_1_Text: String { return self._s[3390]! } + public var Calls_NoCallsPlaceholder: String { return self._s[3391]! } public func Wallet_Receive_ShareInvoiceUrlInfo(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3235]!, self._r[3235]!, [_0]) + return formatWithArgumentRanges(self._s[3392]!, self._r[3392]!, [_0]) } - public var Channel_Username_RevokeExistingUsernamesInfo: String { return self._s[3236]! } - public var VoiceOver_AttachMedia: String { return self._s[3238]! } - public var Notifications_ExceptionsGroupPlaceholder: String { return self._s[3239]! } + public var Channel_Username_RevokeExistingUsernamesInfo: String { return self._s[3393]! } + public var VoiceOver_AttachMedia: String { return self._s[3396]! } + public var Notifications_ExceptionsGroupPlaceholder: String { return self._s[3397]! } public func PUSH_CHAT_MESSAGE_INVOICE(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3240]!, self._r[3240]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[3398]!, self._r[3398]!, [_1, _2, _3]) } - public var SettingsSearch_Synonyms_Notifications_GroupNotificationsSound: String { return self._s[3241]! } - public var Conversation_SetReminder_Title: String { return self._s[3242]! } - public var Passport_FieldAddressHelp: String { return self._s[3243]! } - public var Privacy_GroupsAndChannels_InviteToChannelMultipleError: String { return self._s[3244]! } - public var PUSH_REMINDER_TITLE: String { return self._s[3245]! } + public var SettingsSearch_Synonyms_Notifications_GroupNotificationsSound: String { return self._s[3399]! } + public var Conversation_SetReminder_Title: String { return self._s[3400]! } + public var Passport_FieldAddressHelp: String { return self._s[3401]! } + public var Privacy_GroupsAndChannels_InviteToChannelMultipleError: String { return self._s[3402]! } + public var PUSH_REMINDER_TITLE: String { return self._s[3403]! } public func Login_TermsOfService_ProceedBot(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3246]!, self._r[3246]!, [_0]) + return formatWithArgumentRanges(self._s[3404]!, self._r[3404]!, [_0]) } - public var Channel_AdminLog_EmptyTitle: String { return self._s[3247]! } - public var Privacy_Calls_NeverAllow_Title: String { return self._s[3248]! } - public var Login_UnknownError: String { return self._s[3249]! } - public var Group_UpgradeNoticeText2: String { return self._s[3252]! } - public var Watch_Compose_AddContact: String { return self._s[3253]! } - public var ClearCache_StorageServiceFiles: String { return self._s[3254]! } - public var Web_Error: String { return self._s[3255]! } - public var Gif_Search: String { return self._s[3256]! } - public var Profile_MessageLifetime1h: String { return self._s[3257]! } - public var CheckoutInfo_ReceiverInfoEmailPlaceholder: String { return self._s[3258]! } - public var Channel_Username_CheckingUsername: String { return self._s[3259]! } - public var CallFeedback_ReasonSilentRemote: String { return self._s[3260]! } - public var AutoDownloadSettings_TypeChannels: String { return self._s[3261]! } - public var Channel_AboutItem: String { return self._s[3262]! } - public var Privacy_GroupsAndChannels_AlwaysAllow_Placeholder: String { return self._s[3264]! } - public var VoiceOver_Chat_VoiceMessage: String { return self._s[3265]! } - public var GroupInfo_SharedMedia: String { return self._s[3266]! } + public var Channel_AdminLog_EmptyTitle: String { return self._s[3405]! } + public var Privacy_Calls_NeverAllow_Title: String { return self._s[3406]! } + public var Login_UnknownError: String { return self._s[3407]! } + public var Group_UpgradeNoticeText2: String { return self._s[3410]! } + public var Watch_Compose_AddContact: String { return self._s[3411]! } + public var ClearCache_StorageServiceFiles: String { return self._s[3412]! } + public var Web_Error: String { return self._s[3413]! } + public var Gif_Search: String { return self._s[3414]! } + public var Profile_MessageLifetime1h: String { return self._s[3415]! } + public var CheckoutInfo_ReceiverInfoEmailPlaceholder: String { return self._s[3416]! } + public var Channel_Username_CheckingUsername: String { return self._s[3417]! } + public var CallFeedback_ReasonSilentRemote: String { return self._s[3418]! } + public var AutoDownloadSettings_TypeChannels: String { return self._s[3419]! } + public var Channel_AboutItem: String { return self._s[3420]! } + public var Privacy_GroupsAndChannels_AlwaysAllow_Placeholder: String { return self._s[3422]! } + public var VoiceOver_Chat_VoiceMessage: String { return self._s[3423]! } + public var GroupInfo_SharedMedia: String { return self._s[3424]! } public func Channel_AdminLog_MessagePromotedName(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3267]!, self._r[3267]!, [_1]) + return formatWithArgumentRanges(self._s[3425]!, self._r[3425]!, [_1]) } - public var Call_PhoneCallInProgressMessage: String { return self._s[3268]! } + public var Call_PhoneCallInProgressMessage: String { return self._s[3426]! } public func PUSH_CHANNEL_ALBUM(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3269]!, self._r[3269]!, [_1]) + return formatWithArgumentRanges(self._s[3427]!, self._r[3427]!, [_1]) } - public var ChatList_UndoArchiveRevealedText: String { return self._s[3270]! } - public var GroupInfo_InviteLink_RevokeAlert_Text: String { return self._s[3271]! } - public var Conversation_SearchByName_Placeholder: String { return self._s[3272]! } - public var CreatePoll_AddOption: String { return self._s[3273]! } - public var GroupInfo_Permissions_SearchPlaceholder: String { return self._s[3274]! } - public var Group_UpgradeNoticeHeader: String { return self._s[3275]! } - public var Channel_Management_AddModerator: String { return self._s[3276]! } - public var AutoDownloadSettings_MaxFileSize: String { return self._s[3277]! } - public var StickerPacksSettings_ShowStickersButton: String { return self._s[3278]! } - public var Wallet_Info_RefreshErrorNetworkText: String { return self._s[3279]! } - public var NotificationsSound_Hello: String { return self._s[3281]! } - public var SocksProxySetup_SavedProxies: String { return self._s[3282]! } - public var Channel_Stickers_Placeholder: String { return self._s[3284]! } + public var ChatList_UndoArchiveRevealedText: String { return self._s[3428]! } + public var GroupInfo_InviteLink_RevokeAlert_Text: String { return self._s[3429]! } + public var Conversation_SearchByName_Placeholder: String { return self._s[3430]! } + public var CreatePoll_AddOption: String { return self._s[3431]! } + public var GroupInfo_Permissions_SearchPlaceholder: String { return self._s[3432]! } + public var Group_UpgradeNoticeHeader: String { return self._s[3433]! } + public var Channel_Management_AddModerator: String { return self._s[3434]! } + public var AutoDownloadSettings_MaxFileSize: String { return self._s[3435]! } + public var StickerPacksSettings_ShowStickersButton: String { return self._s[3436]! } + public var Wallet_Info_RefreshErrorNetworkText: String { return self._s[3437]! } + public var Theme_Colors_Background: String { return self._s[3438]! } + public var NotificationsSound_Hello: String { return self._s[3440]! } + public var SocksProxySetup_SavedProxies: String { return self._s[3441]! } + public var Channel_Stickers_Placeholder: String { return self._s[3443]! } public func Login_EmailCodeBody(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3285]!, self._r[3285]!, [_0]) + return formatWithArgumentRanges(self._s[3444]!, self._r[3444]!, [_0]) } - public var PrivacyPolicy_DeclineDeclineAndDelete: String { return self._s[3286]! } - public var Channel_Management_AddModeratorHelp: String { return self._s[3287]! } - public var ContactInfo_BirthdayLabel: String { return self._s[3288]! } - public var ChangePhoneNumberCode_RequestingACall: String { return self._s[3289]! } - public var AutoDownloadSettings_Channels: String { return self._s[3290]! } - public var Passport_Language_mn: String { return self._s[3291]! } - public var Notifications_ResetAllNotificationsHelp: String { return self._s[3294]! } - public var GroupInfo_Permissions_SlowmodeValue_Off: String { return self._s[3295]! } - public var Passport_Language_ja: String { return self._s[3297]! } - public var Settings_About_Title: String { return self._s[3298]! } - public var Settings_NotificationsAndSounds: String { return self._s[3299]! } - public var ChannelInfo_DeleteGroup: String { return self._s[3300]! } - public var Settings_BlockedUsers: String { return self._s[3301]! } + public var PrivacyPolicy_DeclineDeclineAndDelete: String { return self._s[3445]! } + public var Channel_Management_AddModeratorHelp: String { return self._s[3446]! } + public var ContactInfo_BirthdayLabel: String { return self._s[3447]! } + public var ChangePhoneNumberCode_RequestingACall: String { return self._s[3448]! } + public var AutoDownloadSettings_Channels: String { return self._s[3449]! } + public var Passport_Language_mn: String { return self._s[3450]! } + public var Notifications_ResetAllNotificationsHelp: String { return self._s[3453]! } + public var GroupInfo_Permissions_SlowmodeValue_Off: String { return self._s[3454]! } + public var Passport_Language_ja: String { return self._s[3456]! } + public var Settings_About_Title: String { return self._s[3457]! } + public var Settings_NotificationsAndSounds: String { return self._s[3458]! } + public var ChannelInfo_DeleteGroup: String { return self._s[3459]! } + public var Settings_BlockedUsers: String { return self._s[3460]! } public func Time_MonthOfYear_m4(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3302]!, self._r[3302]!, [_0]) + return formatWithArgumentRanges(self._s[3461]!, self._r[3461]!, [_0]) } - public var EditTheme_Create_Preview_OutgoingText: String { return self._s[3303]! } - public var Wallet_Weekday_Today: String { return self._s[3304]! } - public var AutoDownloadSettings_PreloadVideo: String { return self._s[3305]! } - public var Widget_ApplicationLocked: String { return self._s[3306]! } - public var Passport_Address_AddResidentialAddress: String { return self._s[3307]! } - public var Channel_Username_Title: String { return self._s[3308]! } + public var EditTheme_Create_Preview_OutgoingText: String { return self._s[3462]! } + public var Wallet_Weekday_Today: String { return self._s[3463]! } + public var AutoDownloadSettings_PreloadVideo: String { return self._s[3464]! } + public var Widget_ApplicationLocked: String { return self._s[3465]! } + public var Passport_Address_AddResidentialAddress: String { return self._s[3466]! } + public var Channel_Username_Title: String { return self._s[3467]! } public func Notification_RemovedGroupPhoto(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3309]!, self._r[3309]!, [_0]) + return formatWithArgumentRanges(self._s[3468]!, self._r[3468]!, [_0]) } - public var AttachmentMenu_File: String { return self._s[3311]! } - public var AppleWatch_Title: String { return self._s[3312]! } - public var Activity_RecordingVideoMessage: String { return self._s[3313]! } + public var AttachmentMenu_File: String { return self._s[3470]! } + public var AppleWatch_Title: String { return self._s[3471]! } + public var Activity_RecordingVideoMessage: String { return self._s[3472]! } public func Channel_DiscussionGroup_PublicChannelLink(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3314]!, self._r[3314]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3473]!, self._r[3473]!, [_1, _2]) } - public var Weekday_Saturday: String { return self._s[3315]! } - public var WallpaperPreview_SwipeColorsTopText: String { return self._s[3316]! } - public var Profile_CreateEncryptedChatError: String { return self._s[3317]! } - public var Common_Next: String { return self._s[3319]! } - public var Channel_Stickers_YourStickers: String { return self._s[3321]! } - public var Message_Theme: String { return self._s[3322]! } - public var Call_AudioRouteHeadphones: String { return self._s[3323]! } - public var TwoStepAuth_EnterPasswordForgot: String { return self._s[3325]! } - public var Watch_Contacts_NoResults: String { return self._s[3327]! } - public var PhotoEditor_TintTool: String { return self._s[3330]! } - public var LoginPassword_ResetAccount: String { return self._s[3332]! } - public var Settings_SavedMessages: String { return self._s[3333]! } - public var SettingsSearch_Synonyms_Appearance_Animations: String { return self._s[3334]! } - public var Bot_GenericSupportStatus: String { return self._s[3335]! } - public var StickerPack_Add: String { return self._s[3336]! } - public var Checkout_TotalAmount: String { return self._s[3337]! } - public var Your_cards_number_is_invalid: String { return self._s[3338]! } - public var SettingsSearch_Synonyms_Appearance_AutoNightTheme: String { return self._s[3339]! } - public var VoiceOver_Chat_VideoMessage: String { return self._s[3340]! } + public var Theme_Colors_Messages: String { return self._s[3474]! } + public var Weekday_Saturday: String { return self._s[3475]! } + public var WallpaperPreview_SwipeColorsTopText: String { return self._s[3476]! } + public var Profile_CreateEncryptedChatError: String { return self._s[3477]! } + public var Common_Next: String { return self._s[3479]! } + public var Channel_Stickers_YourStickers: String { return self._s[3481]! } + public var Message_Theme: String { return self._s[3482]! } + public var Call_AudioRouteHeadphones: String { return self._s[3483]! } + public var TwoStepAuth_EnterPasswordForgot: String { return self._s[3485]! } + public var Watch_Contacts_NoResults: String { return self._s[3487]! } + public var PhotoEditor_TintTool: String { return self._s[3490]! } + public var LoginPassword_ResetAccount: String { return self._s[3492]! } + public var Settings_SavedMessages: String { return self._s[3493]! } + public var SettingsSearch_Synonyms_Appearance_Animations: String { return self._s[3494]! } + public var Bot_GenericSupportStatus: String { return self._s[3495]! } + public var StickerPack_Add: String { return self._s[3496]! } + public var Checkout_TotalAmount: String { return self._s[3497]! } + public var Your_cards_number_is_invalid: String { return self._s[3498]! } + public var SettingsSearch_Synonyms_Appearance_AutoNightTheme: String { return self._s[3499]! } + public var VoiceOver_Chat_VideoMessage: String { return self._s[3500]! } public func ChangePhoneNumberCode_CallTimer(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3341]!, self._r[3341]!, [_0]) + return formatWithArgumentRanges(self._s[3501]!, self._r[3501]!, [_0]) } public func GroupPermission_AddedInfo(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3342]!, self._r[3342]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3502]!, self._r[3502]!, [_1, _2]) } - public var ChatSettings_ConnectionType_UseSocks5: String { return self._s[3343]! } + public var ChatSettings_ConnectionType_UseSocks5: String { return self._s[3503]! } public func PUSH_CHAT_PHOTO_EDITED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3345]!, self._r[3345]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3505]!, self._r[3505]!, [_1, _2]) } public func Conversation_RestrictedTextTimed(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3346]!, self._r[3346]!, [_0]) + return formatWithArgumentRanges(self._s[3506]!, self._r[3506]!, [_0]) } - public var GroupInfo_InviteLink_ShareLink: String { return self._s[3347]! } - public var StickerPack_Share: String { return self._s[3348]! } - public var Passport_DeleteAddress: String { return self._s[3349]! } - public var Settings_Passport: String { return self._s[3350]! } - public var SharedMedia_EmptyFilesText: String { return self._s[3351]! } - public var Conversation_DeleteMessagesForMe: String { return self._s[3352]! } - public var PasscodeSettings_AutoLock_IfAwayFor_1hour: String { return self._s[3353]! } - public var Contacts_PermissionsText: String { return self._s[3354]! } - public var Group_Setup_HistoryVisible: String { return self._s[3355]! } - public var Wallet_Month_ShortDecember: String { return self._s[3357]! } - public var Channel_EditAdmin_PermissionEnabledByDefault: String { return self._s[3358]! } - public var Passport_Address_AddRentalAgreement: String { return self._s[3359]! } - public var SocksProxySetup_Title: String { return self._s[3360]! } - public var Notification_Mute1h: String { return self._s[3361]! } + public var GroupInfo_InviteLink_ShareLink: String { return self._s[3507]! } + public var StickerPack_Share: String { return self._s[3508]! } + public var Passport_DeleteAddress: String { return self._s[3509]! } + public var Settings_Passport: String { return self._s[3510]! } + public var SharedMedia_EmptyFilesText: String { return self._s[3511]! } + public var Conversation_DeleteMessagesForMe: String { return self._s[3512]! } + public var PasscodeSettings_AutoLock_IfAwayFor_1hour: String { return self._s[3513]! } + public var Contacts_PermissionsText: String { return self._s[3514]! } + public var Group_Setup_HistoryVisible: String { return self._s[3515]! } + public var Wallet_Month_ShortDecember: String { return self._s[3517]! } + public var Channel_EditAdmin_PermissionEnabledByDefault: String { return self._s[3518]! } + public var Passport_Address_AddRentalAgreement: String { return self._s[3519]! } + public var SocksProxySetup_Title: String { return self._s[3520]! } + public var Notification_Mute1h: String { return self._s[3521]! } public func Passport_Email_CodeHelp(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3362]!, self._r[3362]!, [_0]) + return formatWithArgumentRanges(self._s[3522]!, self._r[3522]!, [_0]) } - public var NotificationSettings_ShowNotificationsAllAccountsInfoOff: String { return self._s[3363]! } + public var NotificationSettings_ShowNotificationsAllAccountsInfoOff: String { return self._s[3523]! } public func PUSH_PINNED_GEOLIVE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3364]!, self._r[3364]!, [_1]) + return formatWithArgumentRanges(self._s[3524]!, self._r[3524]!, [_1]) } - public var FastTwoStepSetup_PasswordSection: String { return self._s[3365]! } - public var NetworkUsageSettings_ResetStatsConfirmation: String { return self._s[3368]! } - public var InfoPlist_NSFaceIDUsageDescription: String { return self._s[3370]! } - public var DialogList_NoMessagesText: String { return self._s[3371]! } - public var Privacy_ContactsResetConfirmation: String { return self._s[3372]! } - public var Privacy_Calls_P2PHelp: String { return self._s[3373]! } - public var Channel_DiscussionGroup_SearchPlaceholder: String { return self._s[3375]! } - public var Your_cards_expiration_year_is_invalid: String { return self._s[3376]! } - public var Common_TakePhotoOrVideo: String { return self._s[3377]! } - public var Wallet_Words_Text: String { return self._s[3378]! } - public var Call_StatusBusy: String { return self._s[3379]! } - public var Conversation_PinnedMessage: String { return self._s[3380]! } - public var AutoDownloadSettings_VoiceMessagesTitle: String { return self._s[3381]! } - public var Wallet_Configuration_BlockchainNameChangedProceed: String { return self._s[3382]! } - public var TwoStepAuth_SetupPasswordConfirmFailed: String { return self._s[3383]! } - public var Undo_ChatCleared: String { return self._s[3384]! } - public var AppleWatch_ReplyPresets: String { return self._s[3385]! } - public var Passport_DiscardMessageDescription: String { return self._s[3387]! } - public var Login_NetworkError: String { return self._s[3388]! } + public var FastTwoStepSetup_PasswordSection: String { return self._s[3525]! } + public var NetworkUsageSettings_ResetStatsConfirmation: String { return self._s[3528]! } + public var InfoPlist_NSFaceIDUsageDescription: String { return self._s[3530]! } + public var DialogList_NoMessagesText: String { return self._s[3531]! } + public var Privacy_ContactsResetConfirmation: String { return self._s[3532]! } + public var Privacy_Calls_P2PHelp: String { return self._s[3533]! } + public var Channel_DiscussionGroup_SearchPlaceholder: String { return self._s[3535]! } + public var Your_cards_expiration_year_is_invalid: String { return self._s[3536]! } + public var Common_TakePhotoOrVideo: String { return self._s[3537]! } + public var Wallet_Words_Text: String { return self._s[3538]! } + public var Call_StatusBusy: String { return self._s[3539]! } + public var Conversation_PinnedMessage: String { return self._s[3540]! } + public var AutoDownloadSettings_VoiceMessagesTitle: String { return self._s[3541]! } + public var Wallet_Configuration_BlockchainNameChangedProceed: String { return self._s[3542]! } + public var TwoStepAuth_SetupPasswordConfirmFailed: String { return self._s[3543]! } + public var Undo_ChatCleared: String { return self._s[3544]! } + public var AppleWatch_ReplyPresets: String { return self._s[3545]! } + public var Passport_DiscardMessageDescription: String { return self._s[3547]! } + public var Login_NetworkError: String { return self._s[3548]! } public func Notification_PinnedRoundMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3389]!, self._r[3389]!, [_0]) + return formatWithArgumentRanges(self._s[3549]!, self._r[3549]!, [_0]) } public func Channel_AdminLog_MessageRemovedChannelUsername(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3390]!, self._r[3390]!, [_0]) + return formatWithArgumentRanges(self._s[3550]!, self._r[3550]!, [_0]) } - public var SocksProxySetup_PasswordPlaceholder: String { return self._s[3391]! } - public var Wallet_WordCheck_ViewWords: String { return self._s[3393]! } - public var Login_ResetAccountProtected_LimitExceeded: String { return self._s[3394]! } + public var SocksProxySetup_PasswordPlaceholder: String { return self._s[3551]! } + public var Wallet_WordCheck_ViewWords: String { return self._s[3553]! } + public var Login_ResetAccountProtected_LimitExceeded: String { return self._s[3554]! } public func Watch_LastSeen_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3396]!, self._r[3396]!, [_0]) + return formatWithArgumentRanges(self._s[3556]!, self._r[3556]!, [_0]) } - public var Call_ConnectionErrorMessage: String { return self._s[3397]! } - public var VoiceOver_Chat_Music: String { return self._s[3398]! } - public var SettingsSearch_Synonyms_Notifications_MessageNotificationsSound: String { return self._s[3399]! } - public var Compose_GroupTokenListPlaceholder: String { return self._s[3401]! } - public var ConversationMedia_Title: String { return self._s[3402]! } - public var EncryptionKey_Title: String { return self._s[3404]! } - public var TwoStepAuth_EnterPasswordTitle: String { return self._s[3405]! } - public var Notification_Exceptions_AddException: String { return self._s[3406]! } - public var PrivacySettings_BlockedPeersEmpty: String { return self._s[3407]! } - public var Profile_MessageLifetime1m: String { return self._s[3408]! } + public var Call_ConnectionErrorMessage: String { return self._s[3557]! } + public var VoiceOver_Chat_Music: String { return self._s[3558]! } + public var SettingsSearch_Synonyms_Notifications_MessageNotificationsSound: String { return self._s[3559]! } + public var Compose_GroupTokenListPlaceholder: String { return self._s[3561]! } + public var ConversationMedia_Title: String { return self._s[3562]! } + public var EncryptionKey_Title: String { return self._s[3564]! } + public var TwoStepAuth_EnterPasswordTitle: String { return self._s[3565]! } + public var Notification_Exceptions_AddException: String { return self._s[3566]! } + public var PrivacySettings_BlockedPeersEmpty: String { return self._s[3567]! } + public var Profile_MessageLifetime1m: String { return self._s[3568]! } public func Channel_AdminLog_MessageUnkickedName(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3409]!, self._r[3409]!, [_1]) + return formatWithArgumentRanges(self._s[3569]!, self._r[3569]!, [_1]) } - public var Month_GenMay: String { return self._s[3410]! } + public var Month_GenMay: String { return self._s[3570]! } public func LiveLocationUpdated_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3411]!, self._r[3411]!, [_0]) + return formatWithArgumentRanges(self._s[3571]!, self._r[3571]!, [_0]) } - public var PeopleNearby_Users: String { return self._s[3412]! } - public var Wallet_Send_AddressInfo: String { return self._s[3413]! } - public var ChannelMembers_WhoCanAddMembersAllHelp: String { return self._s[3414]! } - public var AutoDownloadSettings_ResetSettings: String { return self._s[3415]! } + public var PeopleNearby_Users: String { return self._s[3572]! } + public var Wallet_Send_AddressInfo: String { return self._s[3573]! } + public var ChannelMembers_WhoCanAddMembersAllHelp: String { return self._s[3574]! } + public var AutoDownloadSettings_ResetSettings: String { return self._s[3575]! } public func Wallet_Updated_AtDate(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3417]!, self._r[3417]!, [_0]) - } - public var Conversation_EmptyPlaceholder: String { return self._s[3418]! } - public var Passport_Address_AddPassportRegistration: String { return self._s[3419]! } - public var Notifications_ChannelNotificationsAlert: String { return self._s[3420]! } - public var ChatSettings_AutoDownloadUsingCellular: String { return self._s[3421]! } - public var Camera_TapAndHoldForVideo: String { return self._s[3422]! } - public var Channel_JoinChannel: String { return self._s[3424]! } - public var Appearance_Animations: String { return self._s[3427]! } - public func Notification_MessageLifetimeChanged(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3428]!, self._r[3428]!, [_1, _2]) - } - public var Stickers_GroupStickers: String { return self._s[3430]! } - public var Appearance_ShareTheme: String { return self._s[3431]! } - public var TwoFactorSetup_Hint_Placeholder: String { return self._s[3432]! } - public var ConvertToSupergroup_HelpTitle: String { return self._s[3434]! } - public var Passport_Address_Street: String { return self._s[3435]! } - public var Conversation_AddContact: String { return self._s[3436]! } - public var Login_PhonePlaceholder: String { return self._s[3437]! } - public var Channel_Members_InviteLink: String { return self._s[3439]! } - public var Bot_Stop: String { return self._s[3440]! } - public var SettingsSearch_Synonyms_Proxy_UseForCalls: String { return self._s[3442]! } - public var Notification_PassportValueAddress: String { return self._s[3443]! } - public var Month_ShortJuly: String { return self._s[3444]! } - public var Passport_Address_TypeTemporaryRegistrationUploadScan: String { return self._s[3445]! } - public var Channel_AdminLog_BanSendMedia: String { return self._s[3446]! } - public var Passport_Identity_ReverseSide: String { return self._s[3447]! } - public var Watch_Stickers_Recents: String { return self._s[3450]! } - public var PrivacyLastSeenSettings_EmpryUsersPlaceholder: String { return self._s[3452]! } - public var Map_SendThisLocation: String { return self._s[3453]! } - public func Time_MonthOfYear_m1(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3454]!, self._r[3454]!, [_0]) - } - public func InviteText_SingleContact(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3455]!, self._r[3455]!, [_0]) - } - public var ConvertToSupergroup_Note: String { return self._s[3456]! } - public var Wallet_Intro_NotNow: String { return self._s[3457]! } - public func FileSize_MB(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3458]!, self._r[3458]!, [_0]) - } - public var NetworkUsageSettings_GeneralDataSection: String { return self._s[3459]! } - public func Compatibility_SecretMediaVersionTooLow(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3460]!, self._r[3460]!, [_0, _1]) - } - public var Login_CallRequestState3: String { return self._s[3462]! } - public var Wallpaper_SearchShort: String { return self._s[3463]! } - public var SettingsSearch_Synonyms_Appearance_ColorTheme: String { return self._s[3465]! } - public var PasscodeSettings_UnlockWithFaceId: String { return self._s[3466]! } - public var Channel_BotDoesntSupportGroups: String { return self._s[3467]! } - public func PUSH_CHAT_MESSAGE_GEOLIVE(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3468]!, self._r[3468]!, [_1, _2]) - } - public var Channel_AdminLogFilter_Title: String { return self._s[3469]! } - public var Notifications_GroupNotificationsExceptions: String { return self._s[3473]! } - public func FileSize_B(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3474]!, self._r[3474]!, [_0]) - } - public var Passport_CorrectErrors: String { return self._s[3475]! } - public var VoiceOver_Chat_YourAnonymousPoll: String { return self._s[3476]! } - public func Channel_MessageTitleUpdated(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3477]!, self._r[3477]!, [_0]) - } - public var Map_SendMyCurrentLocation: String { return self._s[3478]! } - public var Channel_DiscussionGroup: String { return self._s[3479]! } - public var TwoFactorSetup_Email_SkipConfirmationSkip: String { return self._s[3480]! } - public func PUSH_PINNED_CONTACT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3481]!, self._r[3481]!, [_1, _2]) - } - public var SharedMedia_SearchNoResults: String { return self._s[3482]! } - public var Permissions_NotificationsText_v0: String { return self._s[3483]! } - public var Channel_EditAdmin_PermissionDeleteMessagesOfOthers: String { return self._s[3484]! } - public var Appearance_AppIcon: String { return self._s[3485]! } - public var Appearance_ThemePreview_ChatList_3_AuthorName: String { return self._s[3486]! } - public var LoginPassword_FloodError: String { return self._s[3487]! } - public var Wallet_Send_OwnAddressAlertProceed: String { return self._s[3489]! } - public var Group_Setup_HistoryHiddenHelp: String { return self._s[3490]! } - public func TwoStepAuth_PendingEmailHelp(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3491]!, self._r[3491]!, [_0]) - } - public var Passport_Language_bn: String { return self._s[3492]! } - public func DialogList_SingleUploadingPhotoSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3493]!, self._r[3493]!, [_0]) - } - public var ChatList_Context_Pin: String { return self._s[3494]! } - public func Notification_PinnedAudioMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3495]!, self._r[3495]!, [_0]) - } - public func Channel_AdminLog_MessageChangedGroupStickerPack(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3496]!, self._r[3496]!, [_0]) - } - public var Wallet_Navigation_Close: String { return self._s[3497]! } - public var GroupInfo_InvitationLinkGroupFull: String { return self._s[3501]! } - public var Group_EditAdmin_PermissionChangeInfo: String { return self._s[3503]! } - public var Wallet_Month_GenDecember: String { return self._s[3504]! } - public var Contacts_PermissionsAllow: String { return self._s[3505]! } - public var ReportPeer_ReasonCopyright: String { return self._s[3506]! } - public var Channel_EditAdmin_PermissinAddAdminOn: String { return self._s[3507]! } - public var WallpaperPreview_Pattern: String { return self._s[3508]! } - public var Paint_Duplicate: String { return self._s[3509]! } - public var Passport_Address_Country: String { return self._s[3510]! } - public var Notification_RenamedChannel: String { return self._s[3512]! } - public var ChatList_Context_Unmute: String { return self._s[3513]! } - public var CheckoutInfo_ErrorPostcodeInvalid: String { return self._s[3514]! } - public var Group_MessagePhotoUpdated: String { return self._s[3515]! } - public var Channel_BanUser_PermissionSendMedia: String { return self._s[3516]! } - public var Conversation_ContextMenuBan: String { return self._s[3517]! } - public var TwoStepAuth_EmailSent: String { return self._s[3518]! } - public var MessagePoll_NoVotes: String { return self._s[3519]! } - public var Wallet_Send_ErrorNotEnoughFundsTitle: String { return self._s[3520]! } - public var Passport_Language_is: String { return self._s[3521]! } - public var PeopleNearby_UsersEmpty: String { return self._s[3523]! } - public var Tour_Text5: String { return self._s[3524]! } - public func Call_GroupFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3526]!, self._r[3526]!, [_1, _2]) - } - public var Undo_SecretChatDeleted: String { return self._s[3527]! } - public var SocksProxySetup_ShareQRCode: String { return self._s[3528]! } - public func VoiceOver_Chat_Size(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3529]!, self._r[3529]!, [_0]) - } - public var LogoutOptions_ChangePhoneNumberText: String { return self._s[3530]! } - public var Paint_Edit: String { return self._s[3532]! } - public var ScheduledMessages_ReminderNotification: String { return self._s[3534]! } - public var Undo_DeletedGroup: String { return self._s[3536]! } - public var LoginPassword_ForgotPassword: String { return self._s[3537]! } - public var Wallet_WordImport_IncorrectTitle: String { return self._s[3538]! } - public var GroupInfo_GroupNamePlaceholder: String { return self._s[3539]! } - public func Notification_Kicked(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3540]!, self._r[3540]!, [_0, _1]) - } - public var AppWallet_TransactionInfo_FeeInfoURL: String { return self._s[3541]! } - public var Conversation_InputTextCaptionPlaceholder: String { return self._s[3542]! } - public var AutoDownloadSettings_VideoMessagesTitle: String { return self._s[3543]! } - public var Passport_Language_uz: String { return self._s[3544]! } - public var Conversation_PinMessageAlertGroup: String { return self._s[3545]! } - public var SettingsSearch_Synonyms_Privacy_GroupsAndChannels: String { return self._s[3546]! } - public var Map_StopLiveLocation: String { return self._s[3548]! } - public var VoiceOver_MessageContextSend: String { return self._s[3550]! } - public var PasscodeSettings_Help: String { return self._s[3551]! } - public var NotificationsSound_Input: String { return self._s[3552]! } - public var Share_Title: String { return self._s[3555]! } - public var LogoutOptions_Title: String { return self._s[3556]! } - public var Wallet_Send_AddressText: String { return self._s[3557]! } - public var Login_TermsOfServiceAgree: String { return self._s[3558]! } - public var Compose_NewEncryptedChatTitle: String { return self._s[3559]! } - public var Channel_AdminLog_TitleSelectedEvents: String { return self._s[3560]! } - public var Channel_EditAdmin_PermissionEditMessages: String { return self._s[3561]! } - public var EnterPasscode_EnterTitle: String { return self._s[3562]! } - public func Call_PrivacyErrorMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3563]!, self._r[3563]!, [_0]) - } - public var Settings_CopyPhoneNumber: String { return self._s[3564]! } - public var Conversation_AddToContacts: String { return self._s[3565]! } - public func VoiceOver_Chat_ReplyFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3566]!, self._r[3566]!, [_0]) - } - public var NotificationsSound_Keys: String { return self._s[3567]! } - public func Call_ParticipantVersionOutdatedError(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3568]!, self._r[3568]!, [_0]) - } - public var Notification_MessageLifetime1w: String { return self._s[3569]! } - public var Message_Video: String { return self._s[3570]! } - public var AutoDownloadSettings_CellularTitle: String { return self._s[3571]! } - public func PUSH_CHANNEL_MESSAGE_PHOTO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3572]!, self._r[3572]!, [_1]) - } - public var Wallet_Receive_AmountInfo: String { return self._s[3575]! } - public func Notification_JoinedChat(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3576]!, self._r[3576]!, [_0]) - } - public func PrivacySettings_LastSeenContactsPlus(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[3577]!, self._r[3577]!, [_0]) } - public var Passport_Language_mk: String { return self._s[3578]! } - public func Wallet_Time_PreciseDate_m2(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3579]!, self._r[3579]!, [_1, _2, _3]) + public var Conversation_EmptyPlaceholder: String { return self._s[3578]! } + public var Passport_Address_AddPassportRegistration: String { return self._s[3579]! } + public var Notifications_ChannelNotificationsAlert: String { return self._s[3580]! } + public var ChatSettings_AutoDownloadUsingCellular: String { return self._s[3581]! } + public var Camera_TapAndHoldForVideo: String { return self._s[3582]! } + public var Channel_JoinChannel: String { return self._s[3584]! } + public var Appearance_Animations: String { return self._s[3587]! } + public func Notification_MessageLifetimeChanged(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3588]!, self._r[3588]!, [_1, _2]) } - public var CreatePoll_CancelConfirmation: String { return self._s[3580]! } - public var Conversation_SilentBroadcastTooltipOn: String { return self._s[3582]! } - public var PrivacyPolicy_Decline: String { return self._s[3583]! } - public var Passport_Identity_DoesNotExpire: String { return self._s[3584]! } - public var Channel_AdminLogFilter_EventsRestrictions: String { return self._s[3585]! } - public var Permissions_SiriAllow_v0: String { return self._s[3587]! } - public var Wallet_Month_ShortAugust: String { return self._s[3588]! } - public var Appearance_ThemeCarouselNight: String { return self._s[3589]! } - public func LOCAL_CHAT_MESSAGE_FWDS(_ _1: String, _ _2: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3590]!, self._r[3590]!, [_1, "\(_2)"]) + public var Stickers_GroupStickers: String { return self._s[3590]! } + public var Appearance_ShareTheme: String { return self._s[3591]! } + public var TwoFactorSetup_Hint_Placeholder: String { return self._s[3592]! } + public var ConvertToSupergroup_HelpTitle: String { return self._s[3594]! } + public var StickerPackActionInfo_RemovedTitle: String { return self._s[3595]! } + public var Passport_Address_Street: String { return self._s[3596]! } + public var Conversation_AddContact: String { return self._s[3597]! } + public var Login_PhonePlaceholder: String { return self._s[3598]! } + public var Channel_Members_InviteLink: String { return self._s[3600]! } + public var Bot_Stop: String { return self._s[3601]! } + public var SettingsSearch_Synonyms_Proxy_UseForCalls: String { return self._s[3603]! } + public var Notification_PassportValueAddress: String { return self._s[3604]! } + public var Month_ShortJuly: String { return self._s[3605]! } + public var Passport_Address_TypeTemporaryRegistrationUploadScan: String { return self._s[3606]! } + public var Channel_AdminLog_BanSendMedia: String { return self._s[3607]! } + public var Passport_Identity_ReverseSide: String { return self._s[3608]! } + public var Watch_Stickers_Recents: String { return self._s[3611]! } + public var PrivacyLastSeenSettings_EmpryUsersPlaceholder: String { return self._s[3613]! } + public var Map_SendThisLocation: String { return self._s[3614]! } + public func Time_MonthOfYear_m1(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3615]!, self._r[3615]!, [_0]) } - public func Notification_RenamedChat(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3591]!, self._r[3591]!, [_0]) + public func InviteText_SingleContact(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3616]!, self._r[3616]!, [_0]) } - public var Paint_Regular: String { return self._s[3592]! } - public var ChatSettings_AutoDownloadReset: String { return self._s[3593]! } - public var SocksProxySetup_ShareLink: String { return self._s[3594]! } - public var Wallet_Qr_Title: String { return self._s[3595]! } - public var BlockedUsers_SelectUserTitle: String { return self._s[3596]! } - public var VoiceOver_Chat_RecordModeVoiceMessage: String { return self._s[3598]! } - public var Wallet_Settings_Configuration: String { return self._s[3599]! } - public var GroupInfo_InviteByLink: String { return self._s[3600]! } - public var MessageTimer_Custom: String { return self._s[3601]! } - public var UserInfo_NotificationsDefaultEnabled: String { return self._s[3602]! } - public var Passport_Address_TypeTemporaryRegistration: String { return self._s[3604]! } - public var Conversation_SendMessage_SetReminder: String { return self._s[3605]! } - public var VoiceOver_Chat_Selected: String { return self._s[3606]! } - public var ChatSettings_AutoDownloadUsingWiFi: String { return self._s[3607]! } - public var Channel_Username_InvalidTaken: String { return self._s[3608]! } - public var Conversation_ClousStorageInfo_Description3: String { return self._s[3609]! } - public var Wallet_WordCheck_TryAgain: String { return self._s[3610]! } - public var Wallet_Info_TransactionPendingHeader: String { return self._s[3611]! } - public var Settings_ChatBackground: String { return self._s[3612]! } - public var Channel_Subscribers_Title: String { return self._s[3613]! } - public var Wallet_Receive_InvoiceUrlHeader: String { return self._s[3614]! } - public var ApplyLanguage_ChangeLanguageTitle: String { return self._s[3615]! } - public var Watch_ConnectionDescription: String { return self._s[3616]! } - public var Wallet_Configuration_ApplyErrorTitle: String { return self._s[3619]! } - public var ChatList_ArchivedChatsTitle: String { return self._s[3621]! } - public var Wallpaper_ResetWallpapers: String { return self._s[3622]! } - public var Wallet_Send_TransactionInProgress: String { return self._s[3623]! } - public var EditProfile_Title: String { return self._s[3624]! } - public var NotificationsSound_Bamboo: String { return self._s[3626]! } - public var Channel_AdminLog_MessagePreviousMessage: String { return self._s[3628]! } - public var Login_SmsRequestState2: String { return self._s[3629]! } - public var Passport_Language_ar: String { return self._s[3630]! } - public func Message_AuthorPinnedGame(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3631]!, self._r[3631]!, [_0]) + public var ConvertToSupergroup_Note: String { return self._s[3617]! } + public var Wallet_Intro_NotNow: String { return self._s[3618]! } + public func FileSize_MB(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3619]!, self._r[3619]!, [_0]) } - public var SettingsSearch_Synonyms_EditProfile_Title: String { return self._s[3632]! } - public var Wallet_Created_Text: String { return self._s[3633]! } - public var Conversation_MessageDialogEdit: String { return self._s[3635]! } - public var Wallet_Created_Proceed: String { return self._s[3636]! } - public var Wallet_Words_Done: String { return self._s[3637]! } - public var VoiceOver_Media_PlaybackPause: String { return self._s[3638]! } - public func PUSH_AUTH_UNKNOWN(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3639]!, self._r[3639]!, [_1]) + public var NetworkUsageSettings_GeneralDataSection: String { return self._s[3620]! } + public func Compatibility_SecretMediaVersionTooLow(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3621]!, self._r[3621]!, [_0, _1]) } - public var Common_Close: String { return self._s[3640]! } - public var GroupInfo_PublicLink: String { return self._s[3641]! } - public var Channel_OwnershipTransfer_ErrorPrivacyRestricted: String { return self._s[3642]! } - public var SettingsSearch_Synonyms_Notifications_GroupNotificationsPreview: String { return self._s[3643]! } - public func Channel_AdminLog_MessageToggleInvitesOff(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3647]!, self._r[3647]!, [_0]) + public var Login_CallRequestState3: String { return self._s[3623]! } + public var Wallpaper_SearchShort: String { return self._s[3624]! } + public var SettingsSearch_Synonyms_Appearance_ColorTheme: String { return self._s[3626]! } + public var PasscodeSettings_UnlockWithFaceId: String { return self._s[3627]! } + public var Channel_BotDoesntSupportGroups: String { return self._s[3628]! } + public func PUSH_CHAT_MESSAGE_GEOLIVE(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3629]!, self._r[3629]!, [_1, _2]) } - public var UserInfo_About_Placeholder: String { return self._s[3648]! } - public func Conversation_FileHowToText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3649]!, self._r[3649]!, [_0]) + public var Channel_AdminLogFilter_Title: String { return self._s[3630]! } + public var Appearance_ThemePreview_Chat_4_Text: String { return self._s[3632]! } + public var Notifications_GroupNotificationsExceptions: String { return self._s[3635]! } + public func FileSize_B(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3636]!, self._r[3636]!, [_0]) } - public var GroupInfo_Permissions_SectionTitle: String { return self._s[3650]! } - public var Channel_Info_Banned: String { return self._s[3652]! } - public func Time_MonthOfYear_m11(_ _0: String) -> (String, [(Int, NSRange)]) { + public var Passport_CorrectErrors: String { return self._s[3637]! } + public var VoiceOver_Chat_YourAnonymousPoll: String { return self._s[3638]! } + public func Channel_MessageTitleUpdated(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3639]!, self._r[3639]!, [_0]) + } + public var Map_SendMyCurrentLocation: String { return self._s[3640]! } + public var Channel_DiscussionGroup: String { return self._s[3641]! } + public var TwoFactorSetup_Email_SkipConfirmationSkip: String { return self._s[3642]! } + public func PUSH_PINNED_CONTACT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3643]!, self._r[3643]!, [_1, _2]) + } + public var SharedMedia_SearchNoResults: String { return self._s[3644]! } + public var Permissions_NotificationsText_v0: String { return self._s[3645]! } + public var Channel_EditAdmin_PermissionDeleteMessagesOfOthers: String { return self._s[3646]! } + public var Appearance_AppIcon: String { return self._s[3647]! } + public var Appearance_ThemePreview_ChatList_3_AuthorName: String { return self._s[3648]! } + public var LoginPassword_FloodError: String { return self._s[3649]! } + public var Wallet_Send_OwnAddressAlertProceed: String { return self._s[3651]! } + public var Group_Setup_HistoryHiddenHelp: String { return self._s[3652]! } + public func TwoStepAuth_PendingEmailHelp(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[3653]!, self._r[3653]!, [_0]) } - public var Appearance_Other: String { return self._s[3654]! } - public var Passport_Language_my: String { return self._s[3655]! } - public var Group_Setup_BasicHistoryHiddenHelp: String { return self._s[3656]! } - public func Time_PreciseDate_m9(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3657]!, self._r[3657]!, [_1, _2, _3]) + public var Passport_Language_bn: String { return self._s[3654]! } + public func DialogList_SingleUploadingPhotoSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3655]!, self._r[3655]!, [_0]) } - public var SettingsSearch_Synonyms_Privacy_PasscodeAndFaceId: String { return self._s[3658]! } - public var Preview_CopyAddress: String { return self._s[3659]! } - public func DialogList_SinglePlayingGameSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3660]!, self._r[3660]!, [_0]) + public var ChatList_Context_Pin: String { return self._s[3656]! } + public func Notification_PinnedAudioMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3657]!, self._r[3657]!, [_0]) } - public var KeyCommand_JumpToPreviousChat: String { return self._s[3661]! } - public var UserInfo_BotSettings: String { return self._s[3662]! } - public var LiveLocation_MenuStopAll: String { return self._s[3664]! } - public var Passport_PasswordCreate: String { return self._s[3665]! } - public var StickerSettings_MaskContextInfo: String { return self._s[3666]! } - public var Message_PinnedLocationMessage: String { return self._s[3667]! } - public var Map_Satellite: String { return self._s[3668]! } - public var Watch_Message_Unsupported: String { return self._s[3669]! } - public var Username_TooManyPublicUsernamesError: String { return self._s[3670]! } - public var TwoStepAuth_EnterPasswordInvalid: String { return self._s[3671]! } - public func Notification_PinnedTextMessage(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3672]!, self._r[3672]!, [_0, _1]) + public func Channel_AdminLog_MessageChangedGroupStickerPack(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3658]!, self._r[3658]!, [_0]) } - public func Conversation_OpenBotLinkText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3673]!, self._r[3673]!, [_0]) + public var Wallet_Navigation_Close: String { return self._s[3659]! } + public var GroupInfo_InvitationLinkGroupFull: String { return self._s[3663]! } + public var Group_EditAdmin_PermissionChangeInfo: String { return self._s[3665]! } + public var Wallet_Month_GenDecember: String { return self._s[3666]! } + public var Contacts_PermissionsAllow: String { return self._s[3667]! } + public var ReportPeer_ReasonCopyright: String { return self._s[3668]! } + public var Channel_EditAdmin_PermissinAddAdminOn: String { return self._s[3669]! } + public var WallpaperPreview_Pattern: String { return self._s[3670]! } + public var Paint_Duplicate: String { return self._s[3671]! } + public var Passport_Address_Country: String { return self._s[3672]! } + public var Notification_RenamedChannel: String { return self._s[3674]! } + public var ChatList_Context_Unmute: String { return self._s[3675]! } + public var CheckoutInfo_ErrorPostcodeInvalid: String { return self._s[3676]! } + public var Group_MessagePhotoUpdated: String { return self._s[3677]! } + public var Channel_BanUser_PermissionSendMedia: String { return self._s[3678]! } + public var Conversation_ContextMenuBan: String { return self._s[3679]! } + public var TwoStepAuth_EmailSent: String { return self._s[3680]! } + public var MessagePoll_NoVotes: String { return self._s[3681]! } + public var Wallet_Send_ErrorNotEnoughFundsTitle: String { return self._s[3682]! } + public var Passport_Language_is: String { return self._s[3684]! } + public var PeopleNearby_UsersEmpty: String { return self._s[3686]! } + public var Tour_Text5: String { return self._s[3687]! } + public func Call_GroupFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3690]!, self._r[3690]!, [_1, _2]) } - public var Wallet_WordImport_Continue: String { return self._s[3674]! } - public func TwoFactorSetup_EmailVerification_Text(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3675]!, self._r[3675]!, [_0]) + public var Undo_SecretChatDeleted: String { return self._s[3691]! } + public var SocksProxySetup_ShareQRCode: String { return self._s[3692]! } + public func VoiceOver_Chat_Size(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3693]!, self._r[3693]!, [_0]) } - public var Notifications_ChannelNotificationsHelp: String { return self._s[3676]! } - public var Privacy_Calls_P2PContacts: String { return self._s[3677]! } - public var NotificationsSound_None: String { return self._s[3678]! } - public var Wallet_TransactionInfo_StorageFeeHeader: String { return self._s[3679]! } - public var Channel_DiscussionGroup_UnlinkGroup: String { return self._s[3681]! } - public var AccessDenied_VoiceMicrophone: String { return self._s[3682]! } - public func ApplyLanguage_ChangeLanguageAlreadyActive(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3683]!, self._r[3683]!, [_1]) + public var Forward_ErrorDisabledForChat: String { return self._s[3694]! } + public var LogoutOptions_ChangePhoneNumberText: String { return self._s[3695]! } + public var Paint_Edit: String { return self._s[3697]! } + public var ScheduledMessages_ReminderNotification: String { return self._s[3699]! } + public var Undo_DeletedGroup: String { return self._s[3701]! } + public var LoginPassword_ForgotPassword: String { return self._s[3702]! } + public var Wallet_WordImport_IncorrectTitle: String { return self._s[3703]! } + public var GroupInfo_GroupNamePlaceholder: String { return self._s[3704]! } + public func Notification_Kicked(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3705]!, self._r[3705]!, [_0, _1]) } - public var Cache_Indexing: String { return self._s[3684]! } - public var DialogList_RecentTitlePeople: String { return self._s[3686]! } - public var DialogList_EncryptionRejected: String { return self._s[3687]! } - public var GroupInfo_Administrators: String { return self._s[3688]! } - public var Passport_ScanPassportHelp: String { return self._s[3689]! } - public var Application_Name: String { return self._s[3690]! } - public var Channel_AdminLogFilter_ChannelEventsInfo: String { return self._s[3691]! } - public var Appearance_ThemeCarouselDay: String { return self._s[3693]! } - public var Passport_Identity_TranslationHelp: String { return self._s[3694]! } - public func VoiceOver_Chat_VideoMessageFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3695]!, self._r[3695]!, [_0]) + public var AppWallet_TransactionInfo_FeeInfoURL: String { return self._s[3706]! } + public var Conversation_InputTextCaptionPlaceholder: String { return self._s[3707]! } + public var AutoDownloadSettings_VideoMessagesTitle: String { return self._s[3708]! } + public var Passport_Language_uz: String { return self._s[3709]! } + public var Conversation_PinMessageAlertGroup: String { return self._s[3710]! } + public var SettingsSearch_Synonyms_Privacy_GroupsAndChannels: String { return self._s[3711]! } + public var Map_StopLiveLocation: String { return self._s[3713]! } + public var VoiceOver_MessageContextSend: String { return self._s[3715]! } + public var PasscodeSettings_Help: String { return self._s[3716]! } + public var NotificationsSound_Input: String { return self._s[3717]! } + public var Share_Title: String { return self._s[3720]! } + public var LogoutOptions_Title: String { return self._s[3721]! } + public var Wallet_Send_AddressText: String { return self._s[3722]! } + public var Login_TermsOfServiceAgree: String { return self._s[3723]! } + public var Compose_NewEncryptedChatTitle: String { return self._s[3724]! } + public var Channel_AdminLog_TitleSelectedEvents: String { return self._s[3725]! } + public var Channel_EditAdmin_PermissionEditMessages: String { return self._s[3726]! } + public var EnterPasscode_EnterTitle: String { return self._s[3727]! } + public func Call_PrivacyErrorMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3728]!, self._r[3728]!, [_0]) } - public func Notification_JoinedGroupByLink(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3696]!, self._r[3696]!, [_0]) + public var Settings_CopyPhoneNumber: String { return self._s[3729]! } + public var Conversation_AddToContacts: String { return self._s[3730]! } + public func VoiceOver_Chat_ReplyFrom(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3731]!, self._r[3731]!, [_0]) } - public func DialogList_EncryptedChatStartedOutgoing(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3697]!, self._r[3697]!, [_0]) + public var NotificationsSound_Keys: String { return self._s[3732]! } + public func Call_ParticipantVersionOutdatedError(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3733]!, self._r[3733]!, [_0]) } - public var Channel_EditAdmin_PermissionDeleteMessages: String { return self._s[3698]! } - public var Privacy_ChatsTitle: String { return self._s[3699]! } - public var DialogList_ClearHistoryConfirmation: String { return self._s[3700]! } - public var SettingsSearch_Synonyms_Data_Storage_ClearCache: String { return self._s[3701]! } - public var Watch_Suggestion_HoldOn: String { return self._s[3702]! } - public var Group_EditAdmin_TransferOwnership: String { return self._s[3703]! } - public var WebBrowser_Title: String { return self._s[3704]! } - public var Group_LinkedChannel: String { return self._s[3705]! } - public var VoiceOver_Chat_SeenByRecipient: String { return self._s[3706]! } - public var SocksProxySetup_RequiredCredentials: String { return self._s[3707]! } - public var Passport_Address_TypeRentalAgreementUploadScan: String { return self._s[3708]! } - public var TwoStepAuth_EmailSkipAlert: String { return self._s[3709]! } - public var ScheduledMessages_RemindersTitle: String { return self._s[3711]! } - public var Channel_Setup_TypePublic: String { return self._s[3713]! } - public func Channel_AdminLog_MessageToggleInvitesOn(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3714]!, self._r[3714]!, [_0]) - } - public var Channel_TypeSetup_Title: String { return self._s[3716]! } - public var Map_OpenInMaps: String { return self._s[3718]! } - public func PUSH_PINNED_NOTEXT(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3719]!, self._r[3719]!, [_1]) - } - public var NotificationsSound_Tremolo: String { return self._s[3721]! } - public func Date_ChatDateHeaderYear(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3722]!, self._r[3722]!, [_1, _2, _3]) - } - public var ConversationProfile_UnknownAddMemberError: String { return self._s[3723]! } - public var Channel_OwnershipTransfer_PasswordPlaceholder: String { return self._s[3724]! } - public var Passport_PasswordHelp: String { return self._s[3725]! } - public var Login_CodeExpiredError: String { return self._s[3726]! } - public var Channel_EditAdmin_PermissionChangeInfo: String { return self._s[3727]! } - public var Conversation_TitleUnmute: String { return self._s[3728]! } - public var Passport_Identity_ScansHelp: String { return self._s[3729]! } - public var Passport_Language_lo: String { return self._s[3730]! } - public var Camera_FlashAuto: String { return self._s[3731]! } - public var Conversation_OpenBotLinkOpen: String { return self._s[3732]! } - public var Common_Cancel: String { return self._s[3733]! } - public var DialogList_SavedMessagesTooltip: String { return self._s[3734]! } - public var TwoStepAuth_SetupPasswordTitle: String { return self._s[3735]! } - public var Appearance_TintAllColors: String { return self._s[3736]! } - public func PUSH_MESSAGE_FWD(_ _1: String) -> (String, [(Int, NSRange)]) { + public var Notification_MessageLifetime1w: String { return self._s[3734]! } + public var Message_Video: String { return self._s[3735]! } + public var AutoDownloadSettings_CellularTitle: String { return self._s[3736]! } + public func PUSH_CHANNEL_MESSAGE_PHOTO(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[3737]!, self._r[3737]!, [_1]) } - public var Conversation_ReportSpamConfirmation: String { return self._s[3738]! } - public var ChatSettings_Title: String { return self._s[3740]! } - public var Passport_PasswordReset: String { return self._s[3741]! } - public var SocksProxySetup_TypeNone: String { return self._s[3742]! } - public var EditTheme_Title: String { return self._s[3744]! } - public var PhoneNumberHelp_Help: String { return self._s[3745]! } - public var Checkout_EnterPassword: String { return self._s[3746]! } - public var Share_AuthTitle: String { return self._s[3748]! } - public var Activity_UploadingDocument: String { return self._s[3749]! } - public var State_Connecting: String { return self._s[3750]! } - public var Profile_MessageLifetime1w: String { return self._s[3751]! } - public var Conversation_ContextMenuReport: String { return self._s[3752]! } - public var CheckoutInfo_ReceiverInfoPhone: String { return self._s[3753]! } - public var AutoNightTheme_ScheduledTo: String { return self._s[3754]! } - public func VoiceOver_Chat_AnonymousPollFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3755]!, self._r[3755]!, [_0]) + public var Wallet_Receive_AmountInfo: String { return self._s[3740]! } + public func Notification_JoinedChat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3741]!, self._r[3741]!, [_0]) } - public var AuthSessions_Terminate: String { return self._s[3756]! } - public var Wallet_WordImport_CanNotRemember: String { return self._s[3757]! } - public var Checkout_NewCard_CardholderNamePlaceholder: String { return self._s[3759]! } - public var KeyCommand_JumpToPreviousUnreadChat: String { return self._s[3760]! } - public var PhotoEditor_Set: String { return self._s[3761]! } - public var EmptyGroupInfo_Title: String { return self._s[3762]! } - public var Login_PadPhoneHelp: String { return self._s[3763]! } - public var AutoDownloadSettings_TypeGroupChats: String { return self._s[3765]! } - public var PrivacyPolicy_DeclineLastWarning: String { return self._s[3767]! } - public var NotificationsSound_Complete: String { return self._s[3768]! } - public var SettingsSearch_Synonyms_Privacy_Data_Title: String { return self._s[3769]! } - public var Group_Info_AdminLog: String { return self._s[3770]! } - public var GroupPermission_NotAvailableInPublicGroups: String { return self._s[3771]! } - public func Wallet_Time_PreciseDate_m11(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3772]!, self._r[3772]!, [_1, _2, _3]) + public func PrivacySettings_LastSeenContactsPlus(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3742]!, self._r[3742]!, [_0]) } - public var Channel_AdminLog_InfoPanelAlertText: String { return self._s[3773]! } - public var Conversation_Admin: String { return self._s[3775]! } - public var Conversation_GifTooltip: String { return self._s[3776]! } - public var Passport_NotLoggedInMessage: String { return self._s[3777]! } - public func AutoDownloadSettings_OnFor(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3779]!, self._r[3779]!, [_0]) + public var Passport_Language_mk: String { return self._s[3743]! } + public func Wallet_Time_PreciseDate_m2(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3744]!, self._r[3744]!, [_1, _2, _3]) } - public var Profile_MessageLifetimeForever: String { return self._s[3780]! } - public var SharedMedia_EmptyTitle: String { return self._s[3782]! } - public var Channel_Edit_PrivatePublicLinkAlert: String { return self._s[3784]! } - public var Username_Help: String { return self._s[3785]! } - public var DialogList_LanguageTooltip: String { return self._s[3787]! } - public var Map_LoadError: String { return self._s[3788]! } - public var Login_PhoneNumberAlreadyAuthorized: String { return self._s[3789]! } - public var Channel_AdminLog_AddMembers: String { return self._s[3790]! } - public var ArchivedChats_IntroTitle2: String { return self._s[3791]! } - public var Notification_Exceptions_NewException: String { return self._s[3792]! } - public var TwoStepAuth_EmailTitle: String { return self._s[3793]! } - public var WatchRemote_AlertText: String { return self._s[3794]! } - public func Wallet_Send_ConfirmationText(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3795]!, self._r[3795]!, [_1, _2, _3]) + public var CreatePoll_CancelConfirmation: String { return self._s[3745]! } + public var MessagePoll_LabelAnonymousQuiz: String { return self._s[3746]! } + public var Conversation_SilentBroadcastTooltipOn: String { return self._s[3748]! } + public var PrivacyPolicy_Decline: String { return self._s[3749]! } + public var Passport_Identity_DoesNotExpire: String { return self._s[3750]! } + public var Channel_AdminLogFilter_EventsRestrictions: String { return self._s[3751]! } + public var AuthSessions_AddDeviceIntro_Action: String { return self._s[3752]! } + public var Permissions_SiriAllow_v0: String { return self._s[3754]! } + public var Wallet_Month_ShortAugust: String { return self._s[3755]! } + public var Appearance_ThemeCarouselNight: String { return self._s[3756]! } + public func LOCAL_CHAT_MESSAGE_FWDS(_ _1: String, _ _2: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3757]!, self._r[3757]!, [_1, "\(_2)"]) } - public var ChatSettings_ConnectionType_Title: String { return self._s[3799]! } - public var WebBrowser_DefaultBrowser: String { return self._s[3800]! } - public func Settings_CheckPhoneNumberTitle(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3801]!, self._r[3801]!, [_0]) + public func Notification_RenamedChat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3758]!, self._r[3758]!, [_0]) } - public var SettingsSearch_Synonyms_Calls_CallTab: String { return self._s[3802]! } - public var Passport_Address_CountryPlaceholder: String { return self._s[3803]! } - public func DialogList_AwaitingEncryption(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3804]!, self._r[3804]!, [_0]) + public var Paint_Regular: String { return self._s[3759]! } + public var ChatSettings_AutoDownloadReset: String { return self._s[3760]! } + public var SocksProxySetup_ShareLink: String { return self._s[3761]! } + public var Wallet_Qr_Title: String { return self._s[3762]! } + public var BlockedUsers_SelectUserTitle: String { return self._s[3763]! } + public var VoiceOver_Chat_RecordModeVoiceMessage: String { return self._s[3765]! } + public var Wallet_Settings_Configuration: String { return self._s[3766]! } + public var GroupInfo_InviteByLink: String { return self._s[3767]! } + public var MessageTimer_Custom: String { return self._s[3768]! } + public var UserInfo_NotificationsDefaultEnabled: String { return self._s[3769]! } + public var Conversation_StopQuizConfirmationTitle: String { return self._s[3770]! } + public var Passport_Address_TypeTemporaryRegistration: String { return self._s[3772]! } + public var Conversation_SendMessage_SetReminder: String { return self._s[3773]! } + public var VoiceOver_Chat_Selected: String { return self._s[3774]! } + public var ChatSettings_AutoDownloadUsingWiFi: String { return self._s[3775]! } + public var Channel_Username_InvalidTaken: String { return self._s[3776]! } + public var Conversation_ClousStorageInfo_Description3: String { return self._s[3777]! } + public var Wallet_WordCheck_TryAgain: String { return self._s[3778]! } + public var Wallet_Info_TransactionPendingHeader: String { return self._s[3779]! } + public var Settings_ChatBackground: String { return self._s[3780]! } + public var Channel_Subscribers_Title: String { return self._s[3781]! } + public var Wallet_Receive_InvoiceUrlHeader: String { return self._s[3782]! } + public var ApplyLanguage_ChangeLanguageTitle: String { return self._s[3783]! } + public var Watch_ConnectionDescription: String { return self._s[3784]! } + public var OldChannels_NoticeText: String { return self._s[3787]! } + public var Wallet_Configuration_ApplyErrorTitle: String { return self._s[3788]! } + public var IntentsSettings_SuggestBy: String { return self._s[3790]! } + public var Theme_ThemeChangedText: String { return self._s[3791]! } + public var ChatList_ArchivedChatsTitle: String { return self._s[3792]! } + public var Wallpaper_ResetWallpapers: String { return self._s[3793]! } + public var Wallet_Send_TransactionInProgress: String { return self._s[3794]! } + public var EditProfile_Title: String { return self._s[3795]! } + public var NotificationsSound_Bamboo: String { return self._s[3797]! } + public var Channel_AdminLog_MessagePreviousMessage: String { return self._s[3799]! } + public var Login_SmsRequestState2: String { return self._s[3800]! } + public var Passport_Language_ar: String { return self._s[3801]! } + public func Message_AuthorPinnedGame(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3802]!, self._r[3802]!, [_0]) } - public func Time_PreciseDate_m6(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3805]!, self._r[3805]!, [_1, _2, _3]) + public var SettingsSearch_Synonyms_EditProfile_Title: String { return self._s[3803]! } + public var Wallet_Created_Text: String { return self._s[3804]! } + public var Conversation_MessageDialogEdit: String { return self._s[3806]! } + public var Wallet_Created_Proceed: String { return self._s[3807]! } + public var Wallet_Words_Done: String { return self._s[3808]! } + public var VoiceOver_Media_PlaybackPause: String { return self._s[3809]! } + public func PUSH_AUTH_UNKNOWN(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3810]!, self._r[3810]!, [_1]) } - public var Group_AdminLog_EmptyText: String { return self._s[3806]! } - public var SettingsSearch_Synonyms_Appearance_Title: String { return self._s[3807]! } - public var Conversation_PrivateChannelTooltip: String { return self._s[3809]! } - public var Wallet_Created_ExportErrorText: String { return self._s[3810]! } - public var ChatList_UndoArchiveText1: String { return self._s[3811]! } - public var AccessDenied_VideoMicrophone: String { return self._s[3812]! } - public var Conversation_ContextMenuStickerPackAdd: String { return self._s[3813]! } - public var Cache_ClearNone: String { return self._s[3814]! } - public var SocksProxySetup_FailedToConnect: String { return self._s[3815]! } - public var Permissions_NotificationsTitle_v0: String { return self._s[3816]! } - public func Channel_AdminLog_MessageEdited(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3817]!, self._r[3817]!, [_0]) + public var Common_Close: String { return self._s[3811]! } + public var GroupInfo_PublicLink: String { return self._s[3812]! } + public var Channel_OwnershipTransfer_ErrorPrivacyRestricted: String { return self._s[3813]! } + public var SettingsSearch_Synonyms_Notifications_GroupNotificationsPreview: String { return self._s[3814]! } + public func Channel_AdminLog_MessageToggleInvitesOff(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3818]!, self._r[3818]!, [_0]) } - public var Passport_Identity_Country: String { return self._s[3818]! } - public func ChatSettings_AutoDownloadSettings_TypeFile(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3819]!, self._r[3819]!, [_0]) - } - public func Notification_CreatedChat(_ _0: String) -> (String, [(Int, NSRange)]) { + public var UserInfo_About_Placeholder: String { return self._s[3819]! } + public func Conversation_FileHowToText(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[3820]!, self._r[3820]!, [_0]) } - public var Exceptions_AddToExceptions: String { return self._s[3821]! } - public var AccessDenied_Settings: String { return self._s[3822]! } - public var Passport_Address_TypeUtilityBillUploadScan: String { return self._s[3823]! } - public var Month_ShortMay: String { return self._s[3824]! } - public var Compose_NewGroup: String { return self._s[3826]! } - public var Group_Setup_TypePrivate: String { return self._s[3828]! } - public var Login_PadPhoneHelpTitle: String { return self._s[3830]! } - public var Appearance_ThemeDayClassic: String { return self._s[3831]! } - public var Channel_AdminLog_MessagePreviousCaption: String { return self._s[3832]! } - public var AutoDownloadSettings_OffForAll: String { return self._s[3833]! } - public var Privacy_GroupsAndChannels_WhoCanAddMe: String { return self._s[3834]! } - public var Conversation_typing: String { return self._s[3836]! } - public var Undo_ScheduledMessagesCleared: String { return self._s[3837]! } - public var Paint_Masks: String { return self._s[3838]! } - public var Contacts_DeselectAll: String { return self._s[3839]! } - public func Wallet_Updated_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3840]!, self._r[3840]!, [_0]) + public var GroupInfo_Permissions_SectionTitle: String { return self._s[3821]! } + public var Channel_Info_Banned: String { return self._s[3823]! } + public func Time_MonthOfYear_m11(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3824]!, self._r[3824]!, [_0]) } - public var Username_InvalidTaken: String { return self._s[3841]! } - public var Call_StatusNoAnswer: String { return self._s[3842]! } - public var TwoStepAuth_EmailAddSuccess: String { return self._s[3843]! } - public var SettingsSearch_Synonyms_Privacy_BlockedUsers: String { return self._s[3844]! } - public var Passport_Identity_Selfie: String { return self._s[3845]! } - public var Login_InfoLastNamePlaceholder: String { return self._s[3846]! } - public var Privacy_SecretChatsLinkPreviewsHelp: String { return self._s[3847]! } - public var Conversation_ClearSecretHistory: String { return self._s[3848]! } - public var PeopleNearby_Description: String { return self._s[3850]! } - public var NetworkUsageSettings_Title: String { return self._s[3851]! } - public var Your_cards_security_code_is_invalid: String { return self._s[3853]! } + public var Appearance_Other: String { return self._s[3825]! } + public var Passport_Language_my: String { return self._s[3826]! } + public var Group_Setup_BasicHistoryHiddenHelp: String { return self._s[3827]! } + public func Time_PreciseDate_m9(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3828]!, self._r[3828]!, [_1, _2, _3]) + } + public var SettingsSearch_Synonyms_Privacy_PasscodeAndFaceId: String { return self._s[3829]! } + public var IntentsSettings_SuggestedAndSpotlightChatsInfo: String { return self._s[3830]! } + public var Preview_CopyAddress: String { return self._s[3831]! } + public func DialogList_SinglePlayingGameSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3832]!, self._r[3832]!, [_0]) + } + public var KeyCommand_JumpToPreviousChat: String { return self._s[3833]! } + public var UserInfo_BotSettings: String { return self._s[3834]! } + public var LiveLocation_MenuStopAll: String { return self._s[3836]! } + public var Passport_PasswordCreate: String { return self._s[3837]! } + public var StickerSettings_MaskContextInfo: String { return self._s[3838]! } + public var Message_PinnedLocationMessage: String { return self._s[3839]! } + public var Map_Satellite: String { return self._s[3840]! } + public var Watch_Message_Unsupported: String { return self._s[3841]! } + public var Username_TooManyPublicUsernamesError: String { return self._s[3842]! } + public var TwoStepAuth_EnterPasswordInvalid: String { return self._s[3843]! } + public func Notification_PinnedTextMessage(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3844]!, self._r[3844]!, [_0, _1]) + } + public func Conversation_OpenBotLinkText(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3845]!, self._r[3845]!, [_0]) + } + public var Wallet_WordImport_Continue: String { return self._s[3846]! } + public func TwoFactorSetup_EmailVerification_Text(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3847]!, self._r[3847]!, [_0]) + } + public var Notifications_ChannelNotificationsHelp: String { return self._s[3848]! } + public var Privacy_Calls_P2PContacts: String { return self._s[3849]! } + public var NotificationsSound_None: String { return self._s[3850]! } + public var Wallet_TransactionInfo_StorageFeeHeader: String { return self._s[3851]! } + public var Channel_DiscussionGroup_UnlinkGroup: String { return self._s[3853]! } + public var AccessDenied_VoiceMicrophone: String { return self._s[3854]! } + public func ApplyLanguage_ChangeLanguageAlreadyActive(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3855]!, self._r[3855]!, [_1]) + } + public var Cache_Indexing: String { return self._s[3856]! } + public var DialogList_RecentTitlePeople: String { return self._s[3858]! } + public var DialogList_EncryptionRejected: String { return self._s[3859]! } + public var GroupInfo_Administrators: String { return self._s[3860]! } + public var Passport_ScanPassportHelp: String { return self._s[3861]! } + public var Application_Name: String { return self._s[3862]! } + public var Channel_AdminLogFilter_ChannelEventsInfo: String { return self._s[3863]! } + public var PeopleNearby_MakeVisible: String { return self._s[3865]! } + public var Appearance_ThemeCarouselDay: String { return self._s[3866]! } + public var Passport_Identity_TranslationHelp: String { return self._s[3867]! } + public func VoiceOver_Chat_VideoMessageFrom(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3868]!, self._r[3868]!, [_0]) + } + public func Notification_JoinedGroupByLink(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3869]!, self._r[3869]!, [_0]) + } + public func DialogList_EncryptedChatStartedOutgoing(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3870]!, self._r[3870]!, [_0]) + } + public var Channel_EditAdmin_PermissionDeleteMessages: String { return self._s[3871]! } + public var Privacy_ChatsTitle: String { return self._s[3872]! } + public var DialogList_ClearHistoryConfirmation: String { return self._s[3873]! } + public var SettingsSearch_Synonyms_Data_Storage_ClearCache: String { return self._s[3874]! } + public var Watch_Suggestion_HoldOn: String { return self._s[3875]! } + public var Group_EditAdmin_TransferOwnership: String { return self._s[3876]! } + public var WebBrowser_Title: String { return self._s[3877]! } + public var Group_LinkedChannel: String { return self._s[3878]! } + public var VoiceOver_Chat_SeenByRecipient: String { return self._s[3879]! } + public var SocksProxySetup_RequiredCredentials: String { return self._s[3880]! } + public var Passport_Address_TypeRentalAgreementUploadScan: String { return self._s[3881]! } + public var Appearance_TextSize_UseSystem: String { return self._s[3882]! } + public var TwoStepAuth_EmailSkipAlert: String { return self._s[3883]! } + public var ScheduledMessages_RemindersTitle: String { return self._s[3885]! } + public var Channel_Setup_TypePublic: String { return self._s[3887]! } + public func Channel_AdminLog_MessageToggleInvitesOn(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3888]!, self._r[3888]!, [_0]) + } + public var Channel_TypeSetup_Title: String { return self._s[3890]! } + public var MessagePoll_ViewResults: String { return self._s[3891]! } + public var Map_OpenInMaps: String { return self._s[3893]! } + public func PUSH_PINNED_NOTEXT(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3894]!, self._r[3894]!, [_1]) + } + public var NotificationsSound_Tremolo: String { return self._s[3896]! } + public func Date_ChatDateHeaderYear(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3897]!, self._r[3897]!, [_1, _2, _3]) + } + public var ConversationProfile_UnknownAddMemberError: String { return self._s[3898]! } + public var Channel_OwnershipTransfer_PasswordPlaceholder: String { return self._s[3899]! } + public var Passport_PasswordHelp: String { return self._s[3900]! } + public var Login_CodeExpiredError: String { return self._s[3901]! } + public var Channel_EditAdmin_PermissionChangeInfo: String { return self._s[3902]! } + public var Conversation_TitleUnmute: String { return self._s[3903]! } + public var Passport_Identity_ScansHelp: String { return self._s[3904]! } + public var Passport_Language_lo: String { return self._s[3905]! } + public var Camera_FlashAuto: String { return self._s[3906]! } + public var Conversation_OpenBotLinkOpen: String { return self._s[3907]! } + public var Common_Cancel: String { return self._s[3908]! } + public var DialogList_SavedMessagesTooltip: String { return self._s[3909]! } + public var TwoStepAuth_SetupPasswordTitle: String { return self._s[3910]! } + public var Appearance_TintAllColors: String { return self._s[3911]! } + public func PUSH_MESSAGE_FWD(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3912]!, self._r[3912]!, [_1]) + } + public var Conversation_ReportSpamConfirmation: String { return self._s[3913]! } + public var ChatSettings_Title: String { return self._s[3915]! } + public var Passport_PasswordReset: String { return self._s[3916]! } + public var SocksProxySetup_TypeNone: String { return self._s[3917]! } + public var EditTheme_Title: String { return self._s[3920]! } + public var PhoneNumberHelp_Help: String { return self._s[3921]! } + public var Checkout_EnterPassword: String { return self._s[3922]! } + public var Activity_UploadingDocument: String { return self._s[3924]! } + public var Share_AuthTitle: String { return self._s[3925]! } + public var State_Connecting: String { return self._s[3926]! } + public var Profile_MessageLifetime1w: String { return self._s[3927]! } + public var Conversation_ContextMenuReport: String { return self._s[3928]! } + public var CheckoutInfo_ReceiverInfoPhone: String { return self._s[3929]! } + public var AutoNightTheme_ScheduledTo: String { return self._s[3930]! } + public func VoiceOver_Chat_AnonymousPollFrom(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3931]!, self._r[3931]!, [_0]) + } + public var AuthSessions_Terminate: String { return self._s[3932]! } + public var Wallet_WordImport_CanNotRemember: String { return self._s[3933]! } + public var PeerInfo_PaneAudio: String { return self._s[3934]! } + public var Checkout_NewCard_CardholderNamePlaceholder: String { return self._s[3936]! } + public var KeyCommand_JumpToPreviousUnreadChat: String { return self._s[3937]! } + public var PhotoEditor_Set: String { return self._s[3938]! } + public var EmptyGroupInfo_Title: String { return self._s[3939]! } + public var Login_PadPhoneHelp: String { return self._s[3940]! } + public var AutoDownloadSettings_TypeGroupChats: String { return self._s[3942]! } + public var PrivacyPolicy_DeclineLastWarning: String { return self._s[3944]! } + public var NotificationsSound_Complete: String { return self._s[3945]! } + public var SettingsSearch_Synonyms_Privacy_Data_Title: String { return self._s[3946]! } + public var Group_Info_AdminLog: String { return self._s[3947]! } + public var GroupPermission_NotAvailableInPublicGroups: String { return self._s[3948]! } + public func Wallet_Time_PreciseDate_m11(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3949]!, self._r[3949]!, [_1, _2, _3]) + } + public var Channel_AdminLog_InfoPanelAlertText: String { return self._s[3950]! } + public var Group_Location_CreateInThisPlace: String { return self._s[3952]! } + public var Conversation_Admin: String { return self._s[3953]! } + public var Conversation_GifTooltip: String { return self._s[3954]! } + public var Passport_NotLoggedInMessage: String { return self._s[3955]! } + public func AutoDownloadSettings_OnFor(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3957]!, self._r[3957]!, [_0]) + } + public var Profile_MessageLifetimeForever: String { return self._s[3958]! } + public var SharedMedia_EmptyTitle: String { return self._s[3960]! } + public var Channel_Edit_PrivatePublicLinkAlert: String { return self._s[3962]! } + public var Username_Help: String { return self._s[3963]! } + public var DialogList_LanguageTooltip: String { return self._s[3965]! } + public var Map_LoadError: String { return self._s[3966]! } + public var Login_PhoneNumberAlreadyAuthorized: String { return self._s[3967]! } + public var Channel_AdminLog_AddMembers: String { return self._s[3968]! } + public var ArchivedChats_IntroTitle2: String { return self._s[3969]! } + public var Notification_Exceptions_NewException: String { return self._s[3970]! } + public var TwoStepAuth_EmailTitle: String { return self._s[3971]! } + public var WatchRemote_AlertText: String { return self._s[3972]! } + public func Wallet_Send_ConfirmationText(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3973]!, self._r[3973]!, [_1, _2, _3]) + } + public var ChatSettings_ConnectionType_Title: String { return self._s[3977]! } + public func PUSH_PINNED_QUIZ(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3978]!, self._r[3978]!, [_1, _2]) + } + public func Settings_CheckPhoneNumberTitle(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3979]!, self._r[3979]!, [_0]) + } + public var SettingsSearch_Synonyms_Calls_CallTab: String { return self._s[3980]! } + public var WebBrowser_DefaultBrowser: String { return self._s[3981]! } + public var Passport_Address_CountryPlaceholder: String { return self._s[3982]! } + public func DialogList_AwaitingEncryption(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3983]!, self._r[3983]!, [_0]) + } + public func Time_PreciseDate_m6(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3984]!, self._r[3984]!, [_1, _2, _3]) + } + public var Group_AdminLog_EmptyText: String { return self._s[3985]! } + public var SettingsSearch_Synonyms_Appearance_Title: String { return self._s[3986]! } + public var Conversation_PrivateChannelTooltip: String { return self._s[3988]! } + public var Wallet_Created_ExportErrorText: String { return self._s[3989]! } + public var ChatList_UndoArchiveText1: String { return self._s[3990]! } + public var AccessDenied_VideoMicrophone: String { return self._s[3991]! } + public var Conversation_ContextMenuStickerPackAdd: String { return self._s[3992]! } + public var Cache_ClearNone: String { return self._s[3993]! } + public var SocksProxySetup_FailedToConnect: String { return self._s[3994]! } + public var Permissions_NotificationsTitle_v0: String { return self._s[3995]! } + public func Channel_AdminLog_MessageEdited(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3996]!, self._r[3996]!, [_0]) + } + public var Passport_Identity_Country: String { return self._s[3997]! } + public func ChatSettings_AutoDownloadSettings_TypeFile(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3998]!, self._r[3998]!, [_0]) + } + public func Notification_CreatedChat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3999]!, self._r[3999]!, [_0]) + } + public var Exceptions_AddToExceptions: String { return self._s[4000]! } + public var AccessDenied_Settings: String { return self._s[4001]! } + public var Passport_Address_TypeUtilityBillUploadScan: String { return self._s[4002]! } + public var Month_ShortMay: String { return self._s[4003]! } + public var Compose_NewGroup: String { return self._s[4005]! } + public var Group_Setup_TypePrivate: String { return self._s[4007]! } + public var Login_PadPhoneHelpTitle: String { return self._s[4009]! } + public var Appearance_ThemeDayClassic: String { return self._s[4010]! } + public var Channel_AdminLog_MessagePreviousCaption: String { return self._s[4011]! } + public var AutoDownloadSettings_OffForAll: String { return self._s[4012]! } + public var Privacy_GroupsAndChannels_WhoCanAddMe: String { return self._s[4013]! } + public var Conversation_typing: String { return self._s[4015]! } + public var Undo_ScheduledMessagesCleared: String { return self._s[4016]! } + public var Paint_Masks: String { return self._s[4017]! } + public var Contacts_DeselectAll: String { return self._s[4018]! } + public func Wallet_Updated_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4019]!, self._r[4019]!, [_0]) + } + public var CreatePoll_MultipleChoiceQuizAlert: String { return self._s[4020]! } + public var Username_InvalidTaken: String { return self._s[4021]! } + public var Call_StatusNoAnswer: String { return self._s[4022]! } + public var TwoStepAuth_EmailAddSuccess: String { return self._s[4023]! } + public var SettingsSearch_Synonyms_Privacy_BlockedUsers: String { return self._s[4024]! } + public var Passport_Identity_Selfie: String { return self._s[4025]! } + public var Login_InfoLastNamePlaceholder: String { return self._s[4026]! } + public var Privacy_SecretChatsLinkPreviewsHelp: String { return self._s[4027]! } + public var Conversation_ClearSecretHistory: String { return self._s[4028]! } + public var PeopleNearby_Description: String { return self._s[4030]! } + public var NetworkUsageSettings_Title: String { return self._s[4031]! } + public var Your_cards_security_code_is_invalid: String { return self._s[4033]! } public func Notification_LeftChannel(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3855]!, self._r[3855]!, [_0]) + return formatWithArgumentRanges(self._s[4035]!, self._r[4035]!, [_0]) } public func Call_CallInProgressMessage(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3856]!, self._r[3856]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4036]!, self._r[4036]!, [_1, _2]) } - public var SaveIncomingPhotosSettings_From: String { return self._s[3858]! } - public var VoiceOver_Navigation_Search: String { return self._s[3859]! } - public var Map_LiveLocationTitle: String { return self._s[3860]! } - public var Login_InfoAvatarAdd: String { return self._s[3861]! } - public var Passport_Identity_FilesView: String { return self._s[3862]! } - public var UserInfo_GenericPhoneLabel: String { return self._s[3863]! } - public var Privacy_Calls_NeverAllow: String { return self._s[3864]! } - public var VoiceOver_Chat_File: String { return self._s[3865]! } - public var Wallet_Settings_DeleteWalletInfo: String { return self._s[3866]! } + public var SaveIncomingPhotosSettings_From: String { return self._s[4038]! } + public var VoiceOver_Navigation_Search: String { return self._s[4039]! } + public var Map_LiveLocationTitle: String { return self._s[4040]! } + public var Login_InfoAvatarAdd: String { return self._s[4041]! } + public var Passport_Identity_FilesView: String { return self._s[4042]! } + public var UserInfo_GenericPhoneLabel: String { return self._s[4043]! } + public var Privacy_Calls_NeverAllow: String { return self._s[4044]! } + public var VoiceOver_Chat_File: String { return self._s[4045]! } + public var Wallet_Settings_DeleteWalletInfo: String { return self._s[4046]! } public func Contacts_AddPhoneNumber(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3867]!, self._r[3867]!, [_0]) + return formatWithArgumentRanges(self._s[4047]!, self._r[4047]!, [_0]) } - public var ContactInfo_PhoneNumberHidden: String { return self._s[3868]! } - public var TwoStepAuth_ConfirmationText: String { return self._s[3869]! } - public var ChatSettings_AutomaticVideoMessageDownload: String { return self._s[3870]! } + public var ContactInfo_PhoneNumberHidden: String { return self._s[4048]! } + public var TwoStepAuth_ConfirmationText: String { return self._s[4049]! } + public var ChatSettings_AutomaticVideoMessageDownload: String { return self._s[4050]! } public func PUSH_CHAT_MESSAGE_VIDEOS(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3871]!, self._r[3871]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[4051]!, self._r[4051]!, [_1, _2, _3]) } - public var Channel_AdminLogFilter_AdminsAll: String { return self._s[3872]! } - public var Wallet_Intro_CreateErrorText: String { return self._s[3873]! } - public var Tour_Title2: String { return self._s[3874]! } - public var Wallet_Sent_ViewWallet: String { return self._s[3875]! } - public var Conversation_FileOpenIn: String { return self._s[3876]! } - public var Checkout_ErrorPrecheckoutFailed: String { return self._s[3877]! } - public var Wallet_Send_ErrorInvalidAddress: String { return self._s[3878]! } - public var Wallpaper_Set: String { return self._s[3879]! } - public var Passport_Identity_Translations: String { return self._s[3881]! } + public var Channel_AdminLogFilter_AdminsAll: String { return self._s[4052]! } + public var Wallet_Intro_CreateErrorText: String { return self._s[4053]! } + public var Tour_Title2: String { return self._s[4054]! } + public var Wallet_Sent_ViewWallet: String { return self._s[4055]! } + public var Conversation_FileOpenIn: String { return self._s[4056]! } + public var Checkout_ErrorPrecheckoutFailed: String { return self._s[4057]! } + public var Wallet_Send_ErrorInvalidAddress: String { return self._s[4058]! } + public var Wallpaper_Set: String { return self._s[4059]! } + public var Passport_Identity_Translations: String { return self._s[4061]! } public func Channel_AdminLog_MessageChangedChannelAbout(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3882]!, self._r[3882]!, [_0]) + return formatWithArgumentRanges(self._s[4062]!, self._r[4062]!, [_0]) } - public var Channel_LeaveChannel: String { return self._s[3883]! } + public var Channel_LeaveChannel: String { return self._s[4063]! } public func PINNED_INVOICE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3884]!, self._r[3884]!, [_1]) + return formatWithArgumentRanges(self._s[4064]!, self._r[4064]!, [_1]) } - public var SettingsSearch_Synonyms_Proxy_AddProxy: String { return self._s[3886]! } - public var PhotoEditor_HighlightsTint: String { return self._s[3887]! } - public var Passport_Email_Delete: String { return self._s[3888]! } - public var Conversation_Mute: String { return self._s[3890]! } - public var Channel_AddBotAsAdmin: String { return self._s[3891]! } - public var Channel_AdminLog_CanSendMessages: String { return self._s[3893]! } - public var Wallet_Configuration_BlockchainNameChangedText: String { return self._s[3894]! } - public var Channel_Management_LabelOwner: String { return self._s[3896]! } + public var SettingsSearch_Synonyms_Proxy_AddProxy: String { return self._s[4066]! } + public var PhotoEditor_HighlightsTint: String { return self._s[4067]! } + public var MessagePoll_LabelPoll: String { return self._s[4068]! } + public var Passport_Email_Delete: String { return self._s[4069]! } + public var Conversation_Mute: String { return self._s[4071]! } + public var Channel_AddBotAsAdmin: String { return self._s[4072]! } + public var Channel_AdminLog_CanSendMessages: String { return self._s[4074]! } + public var Wallet_Configuration_BlockchainNameChangedText: String { return self._s[4075]! } + public var ChatSettings_IntentsSettings: String { return self._s[4077]! } + public var Channel_Management_LabelOwner: String { return self._s[4078]! } public func Notification_PassportValuesSentMessage(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3897]!, self._r[3897]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4079]!, self._r[4079]!, [_1, _2]) } - public var Calls_CallTabDescription: String { return self._s[3898]! } - public var Passport_Identity_NativeNameHelp: String { return self._s[3899]! } - public var Common_No: String { return self._s[3900]! } - public var Weekday_Sunday: String { return self._s[3901]! } - public var Notification_Reply: String { return self._s[3902]! } - public var Conversation_ViewMessage: String { return self._s[3903]! } + public var Calls_CallTabDescription: String { return self._s[4080]! } + public var Passport_Identity_NativeNameHelp: String { return self._s[4081]! } + public var Common_No: String { return self._s[4082]! } + public var Weekday_Sunday: String { return self._s[4083]! } + public var Notification_Reply: String { return self._s[4084]! } + public var Conversation_ViewMessage: String { return self._s[4085]! } public func Checkout_SavePasswordTimeoutAndFaceId(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3904]!, self._r[3904]!, [_0]) + return formatWithArgumentRanges(self._s[4086]!, self._r[4086]!, [_0]) } public func Map_LiveLocationPrivateDescription(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3905]!, self._r[3905]!, [_0]) + return formatWithArgumentRanges(self._s[4087]!, self._r[4087]!, [_0]) } public func Wallet_Time_PreciseDate_m7(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3906]!, self._r[3906]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[4088]!, self._r[4088]!, [_1, _2, _3]) } - public var SettingsSearch_Synonyms_EditProfile_AddAccount: String { return self._s[3907]! } - public var Wallet_Send_Title: String { return self._s[3908]! } - public var Message_PinnedDocumentMessage: String { return self._s[3909]! } - public var Wallet_Info_RefreshErrorText: String { return self._s[3910]! } - public var DialogList_TabTitle: String { return self._s[3912]! } - public var ChatSettings_AutoPlayTitle: String { return self._s[3913]! } - public var Passport_FieldEmail: String { return self._s[3914]! } - public var Conversation_UnpinMessageAlert: String { return self._s[3915]! } - public var Passport_Address_TypeBankStatement: String { return self._s[3916]! } - public var Wallet_SecureStorageReset_Title: String { return self._s[3917]! } - public var Passport_Identity_ExpiryDate: String { return self._s[3918]! } - public var Privacy_Calls_P2P: String { return self._s[3919]! } + public var SettingsSearch_Synonyms_EditProfile_AddAccount: String { return self._s[4089]! } + public var Wallet_Send_Title: String { return self._s[4090]! } + public var Message_PinnedDocumentMessage: String { return self._s[4091]! } + public var Wallet_Info_RefreshErrorText: String { return self._s[4092]! } + public var DialogList_TabTitle: String { return self._s[4094]! } + public var ChatSettings_AutoPlayTitle: String { return self._s[4095]! } + public var Passport_FieldEmail: String { return self._s[4096]! } + public var Conversation_UnpinMessageAlert: String { return self._s[4097]! } + public var Passport_Address_TypeBankStatement: String { return self._s[4098]! } + public var Wallet_SecureStorageReset_Title: String { return self._s[4099]! } + public var Passport_Identity_ExpiryDate: String { return self._s[4100]! } + public var Privacy_Calls_P2P: String { return self._s[4101]! } public func CancelResetAccount_Success(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3921]!, self._r[3921]!, [_0]) + return formatWithArgumentRanges(self._s[4103]!, self._r[4103]!, [_0]) } - public var SocksProxySetup_UseForCallsHelp: String { return self._s[3922]! } + public var SocksProxySetup_UseForCallsHelp: String { return self._s[4104]! } public func PUSH_CHAT_ALBUM(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3923]!, self._r[3923]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4105]!, self._r[4105]!, [_1, _2]) } - public var Stickers_ClearRecent: String { return self._s[3924]! } - public var EnterPasscode_ChangeTitle: String { return self._s[3925]! } - public var TwoFactorSetup_Email_Title: String { return self._s[3926]! } - public var Passport_InfoText: String { return self._s[3927]! } - public var Checkout_NewCard_SaveInfoEnableHelp: String { return self._s[3928]! } + public var Stickers_ClearRecent: String { return self._s[4106]! } + public var EnterPasscode_ChangeTitle: String { return self._s[4107]! } + public var TwoFactorSetup_Email_Title: String { return self._s[4108]! } + public var Passport_InfoText: String { return self._s[4109]! } + public var Checkout_NewCard_SaveInfoEnableHelp: String { return self._s[4110]! } public func Login_InvalidPhoneEmailSubject(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3929]!, self._r[3929]!, [_0]) + return formatWithArgumentRanges(self._s[4111]!, self._r[4111]!, [_0]) } public func Time_PreciseDate_m3(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3930]!, self._r[3930]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[4112]!, self._r[4112]!, [_1, _2, _3]) } - public var SettingsSearch_Synonyms_Notifications_BadgeIncludeMutedChannels: String { return self._s[3931]! } - public var ScheduledMessages_PollUnavailable: String { return self._s[3932]! } - public var VoiceOver_Navigation_Compose: String { return self._s[3933]! } - public var Passport_Identity_EditDriversLicense: String { return self._s[3934]! } - public var Conversation_TapAndHoldToRecord: String { return self._s[3936]! } - public var SettingsSearch_Synonyms_Notifications_BadgeIncludeMutedChats: String { return self._s[3937]! } + public var SettingsSearch_Synonyms_Notifications_BadgeIncludeMutedChannels: String { return self._s[4113]! } + public var ScheduledMessages_PollUnavailable: String { return self._s[4114]! } + public var VoiceOver_Navigation_Compose: String { return self._s[4115]! } + public var Passport_Identity_EditDriversLicense: String { return self._s[4116]! } + public var Conversation_TapAndHoldToRecord: String { return self._s[4118]! } + public var SettingsSearch_Synonyms_Notifications_BadgeIncludeMutedChats: String { return self._s[4119]! } public func Notification_CallTimeFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3938]!, self._r[3938]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4120]!, self._r[4120]!, [_1, _2]) } - public var Channel_EditAdmin_PermissionInviteViaLink: String { return self._s[3940]! } - public var ChatSettings_OpenLinksIn: String { return self._s[3941]! } + public var Channel_EditAdmin_PermissionInviteViaLink: String { return self._s[4123]! } + public var ChatSettings_OpenLinksIn: String { return self._s[4124]! } + public var Map_HomeAndWorkTitle: String { return self._s[4125]! } public func Generic_OpenHiddenLinkAlert(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3943]!, self._r[3943]!, [_0]) + return formatWithArgumentRanges(self._s[4127]!, self._r[4127]!, [_0]) } - public var DialogList_Unread: String { return self._s[3944]! } + public var DialogList_Unread: String { return self._s[4128]! } public func PUSH_CHAT_MESSAGE_GIF(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3945]!, self._r[3945]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4129]!, self._r[4129]!, [_1, _2]) } - public var User_DeletedAccount: String { return self._s[3946]! } - public var OwnershipTransfer_SetupTwoStepAuth: String { return self._s[3947]! } + public var User_DeletedAccount: String { return self._s[4130]! } + public var OwnershipTransfer_SetupTwoStepAuth: String { return self._s[4131]! } public func Watch_Time_ShortYesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3948]!, self._r[3948]!, [_0]) + return formatWithArgumentRanges(self._s[4132]!, self._r[4132]!, [_0]) } - public var UserInfo_NotificationsDefault: String { return self._s[3949]! } - public var SharedMedia_CategoryMedia: String { return self._s[3950]! } - public var SocksProxySetup_ProxyStatusUnavailable: String { return self._s[3951]! } - public var Channel_AdminLog_MessageRestrictedForever: String { return self._s[3952]! } - public var Watch_ChatList_Compose: String { return self._s[3953]! } - public var Notifications_MessageNotificationsExceptionsHelp: String { return self._s[3954]! } - public var AutoDownloadSettings_Delimeter: String { return self._s[3955]! } - public var Watch_Microphone_Access: String { return self._s[3956]! } - public var Group_Setup_HistoryHeader: String { return self._s[3957]! } - public var Map_SetThisLocation: String { return self._s[3958]! } - public var Appearance_ThemePreview_Chat_2_ReplyName: String { return self._s[3959]! } - public var Activity_UploadingPhoto: String { return self._s[3960]! } - public var Conversation_Edit: String { return self._s[3962]! } - public var Group_ErrorSendRestrictedMedia: String { return self._s[3963]! } - public var Login_TermsOfServiceDecline: String { return self._s[3964]! } - public var Message_PinnedContactMessage: String { return self._s[3965]! } + public var UserInfo_NotificationsDefault: String { return self._s[4133]! } + public var SharedMedia_CategoryMedia: String { return self._s[4134]! } + public var SocksProxySetup_ProxyStatusUnavailable: String { return self._s[4135]! } + public var Channel_AdminLog_MessageRestrictedForever: String { return self._s[4136]! } + public var Watch_ChatList_Compose: String { return self._s[4137]! } + public var Notifications_MessageNotificationsExceptionsHelp: String { return self._s[4138]! } + public var AutoDownloadSettings_Delimeter: String { return self._s[4139]! } + public var Watch_Microphone_Access: String { return self._s[4140]! } + public var Group_Setup_HistoryHeader: String { return self._s[4141]! } + public var Map_SetThisLocation: String { return self._s[4142]! } + public var Appearance_ThemePreview_Chat_2_ReplyName: String { return self._s[4143]! } + public var Activity_UploadingPhoto: String { return self._s[4144]! } + public var Conversation_Edit: String { return self._s[4146]! } + public var Group_ErrorSendRestrictedMedia: String { return self._s[4147]! } + public var Login_TermsOfServiceDecline: String { return self._s[4148]! } + public var Message_PinnedContactMessage: String { return self._s[4149]! } public func Channel_AdminLog_MessageRestrictedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3966]!, self._r[3966]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4150]!, self._r[4150]!, [_1, _2]) } public func Login_PhoneBannedEmailBody(_ _1: String, _ _2: String, _ _3: String, _ _4: String, _ _5: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3967]!, self._r[3967]!, [_1, _2, _3, _4, _5]) + return formatWithArgumentRanges(self._s[4151]!, self._r[4151]!, [_1, _2, _3, _4, _5]) } - public var Appearance_LargeEmoji: String { return self._s[3968]! } - public var TwoStepAuth_AdditionalPassword: String { return self._s[3970]! } - public var EditTheme_Edit_Preview_IncomingReplyText: String { return self._s[3971]! } + public var Appearance_LargeEmoji: String { return self._s[4152]! } + public var TwoStepAuth_AdditionalPassword: String { return self._s[4154]! } + public var EditTheme_Edit_Preview_IncomingReplyText: String { return self._s[4155]! } public func PUSH_CHAT_DELETE_YOU(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3972]!, self._r[3972]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4156]!, self._r[4156]!, [_1, _2]) } - public var Passport_Phone_EnterOtherNumber: String { return self._s[3973]! } - public var Message_PinnedPhotoMessage: String { return self._s[3974]! } - public var Passport_FieldPhone: String { return self._s[3975]! } - public var TwoStepAuth_RecoveryEmailAddDescription: String { return self._s[3976]! } - public var ChatSettings_AutoPlayGifs: String { return self._s[3977]! } - public var InfoPlist_NSCameraUsageDescription: String { return self._s[3979]! } - public var Conversation_Call: String { return self._s[3980]! } - public var Common_TakePhoto: String { return self._s[3982]! } - public var Group_EditAdmin_RankTitle: String { return self._s[3983]! } - public var Wallet_Receive_CommentHeader: String { return self._s[3984]! } - public var Channel_NotificationLoading: String { return self._s[3985]! } + public var Passport_Phone_EnterOtherNumber: String { return self._s[4157]! } + public var Message_PinnedPhotoMessage: String { return self._s[4158]! } + public var Passport_FieldPhone: String { return self._s[4159]! } + public var TwoStepAuth_RecoveryEmailAddDescription: String { return self._s[4160]! } + public var ChatSettings_AutoPlayGifs: String { return self._s[4161]! } + public var InfoPlist_NSCameraUsageDescription: String { return self._s[4163]! } + public var Conversation_Call: String { return self._s[4164]! } + public var Common_TakePhoto: String { return self._s[4166]! } + public var Group_EditAdmin_RankTitle: String { return self._s[4167]! } + public var Wallet_Receive_CommentHeader: String { return self._s[4168]! } + public var Channel_NotificationLoading: String { return self._s[4169]! } public func Notification_Exceptions_Sound(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3986]!, self._r[3986]!, [_0]) + return formatWithArgumentRanges(self._s[4170]!, self._r[4170]!, [_0]) } public func ScheduledMessages_ScheduledDate(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3987]!, self._r[3987]!, [_0]) + return formatWithArgumentRanges(self._s[4171]!, self._r[4171]!, [_0]) } public func PUSH_CHANNEL_MESSAGE_VIDEO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3988]!, self._r[3988]!, [_1]) + return formatWithArgumentRanges(self._s[4172]!, self._r[4172]!, [_1]) } - public var Permissions_SiriTitle_v0: String { return self._s[3989]! } + public var Permissions_SiriTitle_v0: String { return self._s[4173]! } public func VoiceOver_Chat_VoiceMessageFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3990]!, self._r[3990]!, [_0]) + return formatWithArgumentRanges(self._s[4174]!, self._r[4174]!, [_0]) } public func Login_ResetAccountProtected_Text(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3991]!, self._r[3991]!, [_0]) + return formatWithArgumentRanges(self._s[4175]!, self._r[4175]!, [_0]) } - public var Channel_MessagePhotoRemoved: String { return self._s[3992]! } - public var Wallet_Info_ReceiveGrams: String { return self._s[3993]! } - public var ClearCache_FreeSpace: String { return self._s[3994]! } - public var Common_edit: String { return self._s[3995]! } - public var PrivacySettings_AuthSessions: String { return self._s[3996]! } - public var Month_ShortJune: String { return self._s[3997]! } - public var PrivacyLastSeenSettings_AlwaysShareWith_Placeholder: String { return self._s[3998]! } - public var Call_ReportSend: String { return self._s[3999]! } - public var Watch_LastSeen_JustNow: String { return self._s[4000]! } - public var Notifications_MessageNotifications: String { return self._s[4001]! } - public var WallpaperSearch_ColorGreen: String { return self._s[4002]! } - public var BroadcastListInfo_AddRecipient: String { return self._s[4004]! } - public var Group_Status: String { return self._s[4005]! } + public var Channel_MessagePhotoRemoved: String { return self._s[4176]! } + public var Wallet_Info_ReceiveGrams: String { return self._s[4177]! } + public var ClearCache_FreeSpace: String { return self._s[4178]! } + public var Appearance_BubbleCorners_Apply: String { return self._s[4179]! } + public var Common_edit: String { return self._s[4180]! } + public var PrivacySettings_AuthSessions: String { return self._s[4181]! } + public var Month_ShortJune: String { return self._s[4182]! } + public var PrivacyLastSeenSettings_AlwaysShareWith_Placeholder: String { return self._s[4183]! } + public var Call_ReportSend: String { return self._s[4184]! } + public var Watch_LastSeen_JustNow: String { return self._s[4185]! } + public var Notifications_MessageNotifications: String { return self._s[4186]! } + public var WallpaperSearch_ColorGreen: String { return self._s[4187]! } + public var BroadcastListInfo_AddRecipient: String { return self._s[4189]! } + public var Group_Status: String { return self._s[4190]! } public func AutoNightTheme_LocationHelp(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4006]!, self._r[4006]!, [_0, _1]) + return formatWithArgumentRanges(self._s[4191]!, self._r[4191]!, [_0, _1]) } - public var TextFormat_AddLinkTitle: String { return self._s[4007]! } - public var ShareMenu_ShareTo: String { return self._s[4008]! } - public var Conversation_Moderate_Ban: String { return self._s[4009]! } + public var TextFormat_AddLinkTitle: String { return self._s[4192]! } + public var ShareMenu_ShareTo: String { return self._s[4193]! } + public var Conversation_Moderate_Ban: String { return self._s[4194]! } public func Conversation_DeleteMessagesFor(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4010]!, self._r[4010]!, [_0]) + return formatWithArgumentRanges(self._s[4195]!, self._r[4195]!, [_0]) } - public var SharedMedia_ViewInChat: String { return self._s[4011]! } - public var Map_LiveLocationFor8Hours: String { return self._s[4012]! } + public var SharedMedia_ViewInChat: String { return self._s[4196]! } + public var Map_LiveLocationFor8Hours: String { return self._s[4197]! } public func PUSH_PINNED_PHOTO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4013]!, self._r[4013]!, [_1]) + return formatWithArgumentRanges(self._s[4198]!, self._r[4198]!, [_1]) } public func PUSH_PINNED_POLL(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4014]!, self._r[4014]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4199]!, self._r[4199]!, [_1, _2]) } public func Map_AccurateTo(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4016]!, self._r[4016]!, [_0]) + return formatWithArgumentRanges(self._s[4201]!, self._r[4201]!, [_0]) } - public var Map_OpenInHereMaps: String { return self._s[4017]! } - public var Appearance_ReduceMotion: String { return self._s[4018]! } + public var Map_OpenInHereMaps: String { return self._s[4202]! } + public var Appearance_ReduceMotion: String { return self._s[4203]! } public func PUSH_MESSAGE_TEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4019]!, self._r[4019]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4204]!, self._r[4204]!, [_1, _2]) } - public var Channel_Setup_TypePublicHelp: String { return self._s[4020]! } - public var Passport_Identity_EditInternalPassport: String { return self._s[4021]! } - public var PhotoEditor_Skip: String { return self._s[4022]! } - public func Conversation_LiveLocationMembersCount(_ value: Int32) -> String { + public var Channel_Setup_TypePublicHelp: String { return self._s[4205]! } + public var Passport_Identity_EditInternalPassport: String { return self._s[4206]! } + public var PhotoEditor_Skip: String { return self._s[4207]! } + public func OldChannels_GroupFormat(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[0 * 6 + Int(form.rawValue)]!, stringValue) } - public func Contacts_ImportersCount(_ value: Int32) -> String { + public func MessagePoll_VotedCount(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[1 * 6 + Int(form.rawValue)]!, stringValue) } - public func StickerPack_RemoveStickerCount(_ value: Int32) -> String { + public func QuickSend_Photos(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[2 * 6 + Int(form.rawValue)]!, stringValue) } - public func ForwardedContacts(_ value: Int32) -> String { + public func LastSeen_HoursAgo(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[3 * 6 + Int(form.rawValue)]!, stringValue) } - public func GroupInfo_ShowMoreMembers(_ value: Int32) -> String { + public func Call_ShortSeconds(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[4 * 6 + Int(form.rawValue)]!, stringValue) } - public func VoiceOver_Chat_ContactEmailCount(_ value: Int32) -> String { + public func Conversation_SelectedMessages(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[5 * 6 + Int(form.rawValue)]!, stringValue) } - public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String { + public func MuteExpires_Days(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[6 * 6 + Int(form.rawValue)]!, stringValue) } - public func MessageTimer_Months(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[7 * 6 + Int(form.rawValue)]!, stringValue) + public func PUSH_MESSAGE_VIDEOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[7 * 6 + Int(form.rawValue)]!, _1, _2) } - public func SharedMedia_Link(_ value: Int32) -> String { + public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[8 * 6 + Int(form.rawValue)]!, stringValue) } - public func Watch_LastSeen_HoursAgo(_ value: Int32) -> String { + public func PrivacyLastSeenSettings_AddUsers(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[9 * 6 + Int(form.rawValue)]!, stringValue) } - public func InviteText_ContactsCountText(_ value: Int32) -> String { + public func Notifications_ExceptionMuteExpires_Minutes(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[10 * 6 + Int(form.rawValue)]!, stringValue) } - public func SharedMedia_Generic(_ value: Int32) -> String { + public func MessageTimer_ShortHours(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[11 * 6 + Int(form.rawValue)]!, stringValue) } - public func Call_Seconds(_ value: Int32) -> String { + public func MuteExpires_Hours(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[12 * 6 + Int(form.rawValue)]!, stringValue) } - public func Call_ShortSeconds(_ value: Int32) -> String { + public func StickerPack_AddStickerCount(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[13 * 6 + Int(form.rawValue)]!, stringValue) } - public func Call_ShortMinutes(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[14 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Watch_LastSeen_MinutesAgo(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[15 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ServiceMessage_GameScoreSimple(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[16 * 6 + Int(form.rawValue)]!, stringValue) - } - public func AttachmentMenu_SendPhoto(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[17 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_CHANNEL_MESSAGE_PHOTOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + public func PUSH_MESSAGES(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[18 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func PUSH_CHAT_MESSAGES(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[19 * 6 + Int(form.rawValue)]!, _2, _1, _3) - } - public func VoiceOver_Chat_PollOptionCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[20 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PrivacyLastSeenSettings_AddUsers(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[21 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MessageTimer_ShortWeeks(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[22 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_MESSAGE_PHOTOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[23 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func Notification_GameScoreSimple(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[24 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Notifications_ExceptionMuteExpires_Days(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[25 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Conversation_StatusSubscribers(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[26 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_CHAT_MESSAGE_ROUNDS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[27 * 6 + Int(form.rawValue)]!, _2, _1, _3) - } - public func ForwardedAudios(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[28 * 6 + Int(form.rawValue)]!, stringValue) - } - public func SharedMedia_DeleteItemsConfirmation(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[29 * 6 + Int(form.rawValue)]!, stringValue) - } - public func QuickSend_Photos(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[30 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[14 * 6 + Int(form.rawValue)]!, _1, _2) } public func MessageTimer_Weeks(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[31 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[15 * 6 + Int(form.rawValue)]!, stringValue) } - public func Notification_GameScoreSelfSimple(_ value: Int32) -> String { + public func ForwardedVideos(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[32 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MuteExpires_Hours(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[33 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ForwardedFiles(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[34 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MuteExpires_Days(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[35 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Invitation_Members(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[36 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MessageTimer_Hours(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[37 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_CHANNEL_MESSAGES(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[38 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func MessageTimer_Seconds(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[39 * 6 + Int(form.rawValue)]!, stringValue) - } - public func SharedMedia_Video(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[40 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MessageTimer_Years(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[41 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Media_SharePhoto(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[42 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Watch_UserInfo_Mute(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[43 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ForwardedPhotos(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[44 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Media_ShareVideo(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[45 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Wallet_Updated_HoursAgo(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[46 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Notifications_ExceptionMuteExpires_Minutes(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[47 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[16 * 6 + Int(form.rawValue)]!, stringValue) } public func Notification_GameScoreExtended(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[48 * 6 + Int(form.rawValue)]!, stringValue) - } - public func SharedMedia_Photo(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[49 * 6 + Int(form.rawValue)]!, stringValue) - } - public func AttachmentMenu_SendItem(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[50 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ServiceMessage_GameScoreExtended(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[51 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MessagePoll_VotedCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[52 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MessageTimer_ShortSeconds(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[53 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MuteFor_Hours(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[54 * 6 + Int(form.rawValue)]!, stringValue) - } - public func StickerPack_AddStickerCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[55 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_MESSAGE_FWDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[56 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func ServiceMessage_GameScoreSelfExtended(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[57 * 6 + Int(form.rawValue)]!, stringValue) - } - public func VoiceOver_Chat_ContactPhoneNumberCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[58 * 6 + Int(form.rawValue)]!, stringValue) - } - public func LiveLocation_MenuChatsCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[59 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ForwardedVideoMessages(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[60 * 6 + Int(form.rawValue)]!, stringValue) - } - public func CreatePoll_AddMoreOptions(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[61 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ChatList_DeleteConfirmation(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[62 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Conversation_SelectedMessages(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[63 * 6 + Int(form.rawValue)]!, stringValue) - } - public func StickerPack_RemoveMaskCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[64 * 6 + Int(form.rawValue)]!, stringValue) - } - public func StickerPack_AddMaskCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[65 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ForwardedMessages(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[66 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MuteFor_Days(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[67 * 6 + Int(form.rawValue)]!, stringValue) - } - public func VoiceOver_Chat_PollVotes(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[68 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Map_ETAHours(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[69 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_MESSAGE_VIDEOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[70 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func Notifications_ExceptionMuteExpires_Hours(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[71 * 6 + Int(form.rawValue)]!, stringValue) - } - public func AttachmentMenu_SendVideo(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[72 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[17 * 6 + Int(form.rawValue)]!, stringValue) } public func ServiceMessage_GameScoreSelfSimple(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[73 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[18 * 6 + Int(form.rawValue)]!, stringValue) + } + public func InviteText_ContactsCountText(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[19 * 6 + Int(form.rawValue)]!, stringValue) + } + public func SharedMedia_DeleteItemsConfirmation(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[20 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_CHAT_MESSAGES(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[21 * 6 + Int(form.rawValue)]!, _2, _1, _3) + } + public func OldChannels_InactiveMonth(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[22 * 6 + Int(form.rawValue)]!, stringValue) + } + public func DialogList_LiveLocationChatsCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[23 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ForwardedPolls(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[24 * 6 + Int(form.rawValue)]!, stringValue) + } + public func StickerPack_StickerCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[25 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ChatList_DeleteConfirmation(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[26 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Watch_LastSeen_MinutesAgo(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[27 * 6 + Int(form.rawValue)]!, stringValue) } public func Conversation_StatusMembers(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[28 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ServiceMessage_GameScoreSimple(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[29 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ForwardedGifs(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[30 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Call_ShortMinutes(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[31 * 6 + Int(form.rawValue)]!, stringValue) + } + public func StickerPack_RemoveMaskCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[32 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Chat_DeleteMessagesConfirmation(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[33 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Contacts_InviteContacts(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[34 * 6 + Int(form.rawValue)]!, stringValue) + } + public func SharedMedia_Generic(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[35 * 6 + Int(form.rawValue)]!, stringValue) + } + public func GroupInfo_ShowMoreMembers(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[36 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Notifications_Exceptions(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[37 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PasscodeSettings_FailedAttempts(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[38 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ServiceMessage_GameScoreExtended(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[39 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Notification_GameScoreSelfExtended(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[40 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_CHAT_MESSAGE_PHOTOS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[41 * 6 + Int(form.rawValue)]!, _2, _1, _3) + } + public func VoiceOver_Chat_ContactEmailCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[42 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Wallet_Updated_HoursAgo(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[43 * 6 + Int(form.rawValue)]!, stringValue) + } + public func SharedMedia_File(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[44 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MessageTimer_ShortWeeks(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[45 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_MESSAGE_ROUNDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[46 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func Passport_Scans(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[47 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Invitation_Members(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[48 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Wallpaper_DeleteConfirmation(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[49 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PeopleNearby_ShowMorePeople(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[50 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Map_ETAHours(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[51 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Map_ETAMinutes(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[52 * 6 + Int(form.rawValue)]!, stringValue) + } + public func VoiceOver_Chat_PollVotes(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[53 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_CHANNEL_MESSAGES(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[54 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func ForwardedMessages(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[55 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Conversation_StatusSubscribers(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[56 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MessageTimer_ShortSeconds(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[57 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_CHAT_MESSAGE_ROUNDS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[58 * 6 + Int(form.rawValue)]!, _2, _1, _3) + } + public func VoiceOver_Chat_PollOptionCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[59 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Theme_UsersCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[60 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Watch_LastSeen_HoursAgo(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[61 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ForwardedAuthorsOthers(_ selector: Int32, _ _0: String, _ _1: String) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[62 * 6 + Int(form.rawValue)]!, _0, _1) + } + public func UserCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[63 * 6 + Int(form.rawValue)]!, stringValue) + } + public func LiveLocationUpdated_MinutesAgo(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[64 * 6 + Int(form.rawValue)]!, stringValue) + } + public func LiveLocation_MenuChatsCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[65 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MessageTimer_Hours(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[66 * 6 + Int(form.rawValue)]!, stringValue) + } + public func CreatePoll_AddMoreOptions(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[67 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Media_SharePhoto(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[68 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Contacts_ImportersCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[69 * 6 + Int(form.rawValue)]!, stringValue) + } + public func AttachmentMenu_SendVideo(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[70 * 6 + Int(form.rawValue)]!, stringValue) + } + public func GroupInfo_ParticipantCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[71 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MessageTimer_Seconds(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[72 * 6 + Int(form.rawValue)]!, stringValue) + } + public func AttachmentMenu_SendPhoto(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[73 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Call_Minutes(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[74 * 6 + Int(form.rawValue)]!, stringValue) } - public func Forward_ConfirmMultipleFiles(_ value: Int32) -> String { + public func MuteFor_Days(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[75 * 6 + Int(form.rawValue)]!, stringValue) } - public func PUSH_CHANNEL_MESSAGE_ROUNDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[76 * 6 + Int(form.rawValue)]!, _1, _2) + public func MuteExpires_Minutes(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[76 * 6 + Int(form.rawValue)]!, stringValue) } - public func LastSeen_HoursAgo(_ value: Int32) -> String { + public func ForwardedContacts(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[77 * 6 + Int(form.rawValue)]!, stringValue) @@ -4947,194 +5126,239 @@ public final class PresentationStrings: Equatable { let form = getPluralizationForm(self.lc, selector) return String(format: self._ps[78 * 6 + Int(form.rawValue)]!, _2, _1, _3) } - public func SharedMedia_File(_ value: Int32) -> String { + public func SharedMedia_Video(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[79 * 6 + Int(form.rawValue)]!, stringValue) } - public func ForwardedPolls(_ value: Int32) -> String { + public func OldChannels_Leave(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[80 * 6 + Int(form.rawValue)]!, stringValue) } - public func ChatList_DeletedChats(_ value: Int32) -> String { + public func ForwardedPhotos(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[81 * 6 + Int(form.rawValue)]!, stringValue) } - public func Passport_Scans(_ value: Int32) -> String { + public func Notifications_ExceptionMuteExpires_Hours(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[82 * 6 + Int(form.rawValue)]!, stringValue) } - public func ForwardedLocations(_ value: Int32) -> String { + public func ForwardedVideoMessages(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[83 * 6 + Int(form.rawValue)]!, stringValue) } - public func GroupInfo_ParticipantCount(_ value: Int32) -> String { + public func StickerPack_AddMaskCount(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[84 * 6 + Int(form.rawValue)]!, stringValue) } - public func Theme_UsersCount(_ value: Int32) -> String { + public func MessageTimer_Minutes(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[85 * 6 + Int(form.rawValue)]!, stringValue) } - public func ForwardedAuthorsOthers(_ selector: Int32, _ _0: String, _ _1: String) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[86 * 6 + Int(form.rawValue)]!, _0, _1) + public func ServiceMessage_GameScoreSelfExtended(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[86 * 6 + Int(form.rawValue)]!, stringValue) } - public func PasscodeSettings_FailedAttempts(_ value: Int32) -> String { + public func ChatList_DeletedChats(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[87 * 6 + Int(form.rawValue)]!, stringValue) } - public func Conversation_StatusOnline(_ value: Int32) -> String { + public func StickerPack_RemoveStickerCount(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[88 * 6 + Int(form.rawValue)]!, stringValue) } - public func Call_Minutes(_ value: Int32) -> String { + public func PollResults_ShowMore(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[89 * 6 + Int(form.rawValue)]!, stringValue) } - public func LiveLocationUpdated_MinutesAgo(_ value: Int32) -> String { + public func LastSeen_MinutesAgo(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[90 * 6 + Int(form.rawValue)]!, stringValue) } - public func Chat_DeleteMessagesConfirmation(_ value: Int32) -> String { + public func ForwardedLocations(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[91 * 6 + Int(form.rawValue)]!, stringValue) } - public func Notification_GameScoreSelfExtended(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[92 * 6 + Int(form.rawValue)]!, stringValue) + public func PUSH_CHANNEL_MESSAGE_ROUNDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[92 * 6 + Int(form.rawValue)]!, _1, _2) } - public func ChatList_SelectedChats(_ value: Int32) -> String { + public func ForwardedFiles(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[93 * 6 + Int(form.rawValue)]!, stringValue) } - public func Media_ShareItem(_ value: Int32) -> String { + public func Conversation_LiveLocationMembersCount(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[94 * 6 + Int(form.rawValue)]!, stringValue) } - public func PUSH_CHAT_MESSAGE_PHOTOS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[95 * 6 + Int(form.rawValue)]!, _2, _1, _3) - } - public func MessageTimer_ShortHours(_ value: Int32) -> String { + public func VoiceOver_Chat_ContactPhoneNumberCount(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[96 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[95 * 6 + Int(form.rawValue)]!, stringValue) } - public func AttachmentMenu_SendGif(_ value: Int32) -> String { + public func PUSH_CHAT_MESSAGE_FWDS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[96 * 6 + Int(form.rawValue)]!, _2, _1, _3) + } + public func OldChannels_InactiveYear(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[97 * 6 + Int(form.rawValue)]!, stringValue) } - public func ForwardedVideos(_ value: Int32) -> String { + public func MessageTimer_ShortDays(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[98 * 6 + Int(form.rawValue)]!, stringValue) } - public func PUSH_MESSAGES(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + public func PUSH_CHANNEL_MESSAGE_PHOTOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { let form = getPluralizationForm(self.lc, selector) return String(format: self._ps[99 * 6 + Int(form.rawValue)]!, _1, _2) } - public func LastSeen_MinutesAgo(_ value: Int32) -> String { + public func OldChannels_InactiveWeek(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[100 * 6 + Int(form.rawValue)]!, stringValue) } - public func MessageTimer_Days(_ value: Int32) -> String { + public func MessageTimer_ShortMinutes(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[101 * 6 + Int(form.rawValue)]!, stringValue) } - public func PUSH_CHANNEL_MESSAGE_VIDEOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[102 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func MessageTimer_ShortDays(_ value: Int32) -> String { + public func MessageTimer_Months(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[103 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[102 * 6 + Int(form.rawValue)]!, stringValue) } - public func DialogList_LiveLocationChatsCount(_ value: Int32) -> String { + public func PUSH_CHANNEL_MESSAGE_VIDEOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[103 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func SharedMedia_Photo(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[104 * 6 + Int(form.rawValue)]!, stringValue) } - public func PUSH_CHAT_MESSAGE_FWDS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[105 * 6 + Int(form.rawValue)]!, _2, _1, _3) + public func SharedMedia_Link(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[105 * 6 + Int(form.rawValue)]!, stringValue) } - public func MuteExpires_Minutes(_ value: Int32) -> String { + public func AttachmentMenu_SendItem(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[106 * 6 + Int(form.rawValue)]!, stringValue) } - public func PUSH_CHANNEL_MESSAGE_FWDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[107 * 6 + Int(form.rawValue)]!, _1, _2) + public func Media_ShareItem(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[107 * 6 + Int(form.rawValue)]!, stringValue) } - public func Wallpaper_DeleteConfirmation(_ value: Int32) -> String { + public func Notifications_ExceptionMuteExpires_Days(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[108 * 6 + Int(form.rawValue)]!, stringValue) } - public func StickerPack_StickerCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[109 * 6 + Int(form.rawValue)]!, stringValue) + public func PUSH_CHANNEL_MESSAGE_FWDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[109 * 6 + Int(form.rawValue)]!, _1, _2) } - public func MessageTimer_ShortMinutes(_ value: Int32) -> String { + public func Call_Seconds(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[110 * 6 + Int(form.rawValue)]!, stringValue) } - public func UserCount(_ value: Int32) -> String { + public func MessagePoll_QuizCount(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[111 * 6 + Int(form.rawValue)]!, stringValue) } - public func Map_ETAMinutes(_ value: Int32) -> String { + public func Watch_UserInfo_Mute(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[112 * 6 + Int(form.rawValue)]!, stringValue) } - public func PUSH_MESSAGE_ROUNDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[113 * 6 + Int(form.rawValue)]!, _1, _2) + public func Forward_ConfirmMultipleFiles(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[113 * 6 + Int(form.rawValue)]!, stringValue) } - public func ForwardedGifs(_ value: Int32) -> String { + public func MessageTimer_Years(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[114 * 6 + Int(form.rawValue)]!, stringValue) } - public func ForwardedStickers(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[115 * 6 + Int(form.rawValue)]!, stringValue) + public func PUSH_MESSAGE_FWDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[115 * 6 + Int(form.rawValue)]!, _1, _2) } - public func MessageTimer_Minutes(_ value: Int32) -> String { + public func Notification_GameScoreSelfSimple(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[116 * 6 + Int(form.rawValue)]!, stringValue) } - public func Notifications_Exceptions(_ value: Int32) -> String { + public func MessageTimer_Days(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[117 * 6 + Int(form.rawValue)]!, stringValue) } + public func PUSH_MESSAGE_PHOTOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[118 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func ForwardedAudios(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[119 * 6 + Int(form.rawValue)]!, stringValue) + } + public func AttachmentMenu_SendGif(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[120 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MuteFor_Hours(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[121 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ChatList_SelectedChats(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[122 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Conversation_StatusOnline(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[123 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ForwardedStickers(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[124 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Notification_GameScoreSimple(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[125 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Media_ShareVideo(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[126 * 6 + Int(form.rawValue)]!, stringValue) + } public init(primaryComponent: PresentationStringsComponent, secondaryComponent: PresentationStringsComponent?, groupingSeparator: String) { self.primaryComponent = primaryComponent diff --git a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift index 2b04cbf843..b5696e11e6 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift @@ -17,6 +17,10 @@ public final class PresentationThemeGradientColors { public var colors: (UIColor, UIColor) { return (self.topColor, self.bottomColor) } + + public func withUpdated(topColor: UIColor? = nil, bottomColor: UIColor? = nil) -> PresentationThemeGradientColors { + return PresentationThemeGradientColors(topColor: topColor ?? self.topColor, bottomColor: bottomColor ?? self.bottomColor) + } } public final class PresentationThemeIntro { @@ -35,6 +39,10 @@ public final class PresentationThemeIntro { self.startButtonColor = startButtonColor self.dotColor = dotColor } + + public func withUpdated(statusBarStyle: PresentationThemeStatusBarStyle? = nil, primaryTextColor: UIColor? = nil, accentTextColor: UIColor? = nil, disabledTextColor: UIColor? = nil, startButtonColor: UIColor? = nil, dotColor: UIColor? = nil) -> PresentationThemeIntro { + return PresentationThemeIntro(statusBarStyle: statusBarStyle ?? self.statusBarStyle, primaryTextColor: primaryTextColor ?? self.primaryTextColor, accentTextColor: accentTextColor ?? self.accentTextColor, disabledTextColor: disabledTextColor ?? self.disabledTextColor, startButtonColor: startButtonColor ?? self.startButtonColor, dotColor: dotColor ?? self.dotColor) + } } public final class PresentationThemePasscode { @@ -45,6 +53,10 @@ public final class PresentationThemePasscode { self.backgroundColors = backgroundColors self.buttonColor = buttonColor } + + public func withUpdated(backgroundColors: PresentationThemeGradientColors? = nil, buttonColor: UIColor? = nil) -> PresentationThemePasscode { + return PresentationThemePasscode(backgroundColors: backgroundColors ?? self.backgroundColors, buttonColor: buttonColor ?? self.buttonColor) + } } public final class PresentationThemeRootTabBar { @@ -69,6 +81,10 @@ public final class PresentationThemeRootTabBar { self.badgeStrokeColor = badgeStrokeColor self.badgeTextColor = badgeTextColor } + + public func withUpdated(backgroundColor: UIColor? = nil, separatorColor: UIColor? = nil, iconColor: UIColor? = nil, selectedIconColor: UIColor? = nil, textColor: UIColor? = nil, selectedTextColor: UIColor? = nil, badgeBackgroundColor: UIColor? = nil, badgeStrokeColor: UIColor? = nil, badgeTextColor: UIColor? = nil) -> PresentationThemeRootTabBar { + return PresentationThemeRootTabBar(backgroundColor: backgroundColor ?? self.backgroundColor, separatorColor: separatorColor ?? self.separatorColor, iconColor: iconColor ?? self.iconColor, selectedIconColor: selectedIconColor ?? self.selectedIconColor, textColor: textColor ?? self.textColor, selectedTextColor: selectedTextColor ?? self.selectedTextColor, badgeBackgroundColor: badgeBackgroundColor ?? self.badgeBackgroundColor, badgeStrokeColor: badgeStrokeColor ?? self.badgeStrokeColor, badgeTextColor: badgeTextColor ?? self.badgeTextColor) + } } public enum PresentationThemeStatusBarStyle: Int32 { @@ -128,6 +144,10 @@ public final class PresentationThemeRootNavigationBar { self.segmentedTextColor = segmentedTextColor self.segmentedDividerColor = segmentedDividerColor } + + public func withUpdated(buttonColor: UIColor? = nil, disabledButtonColor: UIColor? = nil, primaryTextColor: UIColor? = nil, secondaryTextColor: UIColor? = nil, controlColor: UIColor? = nil, accentTextColor: UIColor? = nil, backgroundColor: UIColor? = nil, separatorColor: UIColor? = nil, badgeBackgroundColor: UIColor? = nil, badgeStrokeColor: UIColor? = nil, badgeTextColor: UIColor? = nil, segmentedBackgroundColor: UIColor? = nil, segmentedForegroundColor: UIColor? = nil, segmentedTextColor: UIColor? = nil, segmentedDividerColor: UIColor? = nil) -> PresentationThemeRootNavigationBar { + return PresentationThemeRootNavigationBar(buttonColor: buttonColor ?? self.buttonColor, disabledButtonColor: disabledButtonColor ?? self.disabledButtonColor, primaryTextColor: primaryTextColor ?? self.primaryTextColor, secondaryTextColor: secondaryTextColor ?? self.secondaryTextColor, controlColor: controlColor ?? self.controlColor, accentTextColor: accentTextColor ?? self.accentTextColor, backgroundColor: backgroundColor ?? self.backgroundColor, separatorColor: separatorColor ?? self.separatorColor, badgeBackgroundColor: badgeBackgroundColor ?? self.badgeBackgroundColor, badgeStrokeColor: badgeStrokeColor ?? self.badgeStrokeColor, badgeTextColor: badgeTextColor ?? self.badgeTextColor, segmentedBackgroundColor: segmentedBackgroundColor ?? self.segmentedBackgroundColor, segmentedForegroundColor: segmentedForegroundColor ?? self.segmentedForegroundColor, segmentedTextColor: segmentedTextColor ?? self.segmentedTextColor, segmentedDividerColor: segmentedDividerColor ?? self.segmentedDividerColor) + } } public final class PresentationThemeNavigationSearchBar { @@ -150,6 +170,10 @@ public final class PresentationThemeNavigationSearchBar { self.inputClearButtonColor = inputClearButtonColor self.separatorColor = separatorColor } + + public func withUpdated(backgroundColor: UIColor? = nil, accentColor: UIColor? = nil, inputFillColor: UIColor? = nil, inputTextColor: UIColor? = nil, inputPlaceholderTextColor: UIColor? = nil, inputIconColor: UIColor? = nil, inputClearButtonColor: UIColor? = nil, separatorColor: UIColor? = nil) -> PresentationThemeNavigationSearchBar { + return PresentationThemeNavigationSearchBar(backgroundColor: backgroundColor ?? self.backgroundColor, accentColor: accentColor ?? self.accentColor, inputFillColor: inputFillColor ?? self.inputFillColor, inputTextColor: inputTextColor ?? self.inputTextColor, inputPlaceholderTextColor: inputPlaceholderTextColor ?? self.inputPlaceholderTextColor, inputIconColor: inputIconColor ?? self.inputIconColor, inputClearButtonColor: inputClearButtonColor ?? self.inputClearButtonColor, separatorColor: separatorColor ?? self.separatorColor) + } } public final class PresentationThemeRootController { @@ -166,6 +190,10 @@ public final class PresentationThemeRootController { self.navigationSearchBar = navigationSearchBar self.keyboardColor = keyboardColor } + + public func withUpdated(statusBarStyle: PresentationThemeStatusBarStyle? = nil, tabBar: PresentationThemeRootTabBar? = nil, navigationBar: PresentationThemeRootNavigationBar? = nil, navigationSearchBar: PresentationThemeNavigationSearchBar? = nil, keyboardColor: PresentationThemeKeyboardColor? = nil) -> PresentationThemeRootController { + return PresentationThemeRootController(statusBarStyle: statusBarStyle ?? self.statusBarStyle, tabBar: tabBar ?? self.tabBar, navigationBar: navigationBar ?? self.navigationBar, navigationSearchBar: navigationSearchBar ?? self.navigationSearchBar, keyboardColor: keyboardColor ?? self.keyboardColor) + } } public enum PresentationThemeActionSheetBackgroundType: Int32 { @@ -217,6 +245,10 @@ public final class PresentationThemeActionSheet { self.inputClearButtonColor = inputClearButtonColor self.checkContentColor = checkContentColor } + + public func withUpdated(dimColor: UIColor? = nil, backgroundType: PresentationThemeActionSheetBackgroundType? = nil, opaqueItemBackgroundColor: UIColor? = nil, itemBackgroundColor: UIColor? = nil, opaqueItemHighlightedBackgroundColor: UIColor? = nil, itemHighlightedBackgroundColor: UIColor? = nil, opaqueItemSeparatorColor: UIColor? = nil, standardActionTextColor: UIColor? = nil, destructiveActionTextColor: UIColor? = nil, disabledActionTextColor: UIColor? = nil, primaryTextColor: UIColor? = nil, secondaryTextColor: UIColor? = nil, controlAccentColor: UIColor? = nil, inputBackgroundColor: UIColor? = nil, inputHollowBackgroundColor: UIColor? = nil, inputBorderColor: UIColor? = nil, inputPlaceholderColor: UIColor? = nil, inputTextColor: UIColor? = nil, inputClearButtonColor: UIColor? = nil, checkContentColor: UIColor? = nil) -> PresentationThemeActionSheet { + return PresentationThemeActionSheet(dimColor: dimColor ?? self.dimColor, backgroundType: backgroundType ?? self.backgroundType, opaqueItemBackgroundColor: opaqueItemBackgroundColor ?? self.opaqueItemBackgroundColor, itemBackgroundColor: itemBackgroundColor ?? self.itemBackgroundColor, opaqueItemHighlightedBackgroundColor: opaqueItemHighlightedBackgroundColor ?? self.opaqueItemHighlightedBackgroundColor, itemHighlightedBackgroundColor: itemHighlightedBackgroundColor ?? self.itemHighlightedBackgroundColor, opaqueItemSeparatorColor: opaqueItemSeparatorColor ?? self.opaqueItemSeparatorColor, standardActionTextColor: standardActionTextColor ?? self.standardActionTextColor, destructiveActionTextColor: destructiveActionTextColor ?? self.destructiveActionTextColor, disabledActionTextColor: disabledActionTextColor ?? self.disabledActionTextColor, primaryTextColor: primaryTextColor ?? self.primaryTextColor, secondaryTextColor: secondaryTextColor ?? self.secondaryTextColor, controlAccentColor: controlAccentColor ?? self.controlAccentColor, inputBackgroundColor: inputBackgroundColor ?? self.inputBackgroundColor, inputHollowBackgroundColor: inputHollowBackgroundColor ?? self.inputHollowBackgroundColor, inputBorderColor: inputBorderColor ?? self.inputBorderColor, inputPlaceholderColor: inputPlaceholderColor ?? self.inputPlaceholderColor, inputTextColor: inputTextColor ?? self.inputTextColor, inputClearButtonColor: inputClearButtonColor ?? self.inputClearButtonColor, checkContentColor: checkContentColor ?? self.checkContentColor) + } } public final class PresentationThemeContextMenu { @@ -241,6 +273,10 @@ public final class PresentationThemeContextMenu { self.secondaryColor = secondaryColor self.destructiveColor = destructiveColor } + + public func withUpdated(dimColor: UIColor? = nil, backgroundColor: UIColor? = nil, itemSeparatorColor: UIColor? = nil, sectionSeparatorColor: UIColor? = nil, itemBackgroundColor: UIColor? = nil, itemHighlightedBackgroundColor: UIColor? = nil, primaryColor: UIColor? = nil, secondaryColor: UIColor? = nil, destructiveColor: UIColor? = nil) -> PresentationThemeContextMenu { + return PresentationThemeContextMenu(dimColor: dimColor ?? self.dimColor, backgroundColor: backgroundColor ?? self.backgroundColor, itemSeparatorColor: itemSeparatorColor ?? self.itemSeparatorColor, sectionSeparatorColor: sectionSeparatorColor ?? self.sectionSeparatorColor, itemBackgroundColor: itemBackgroundColor ?? self.itemBackgroundColor, itemHighlightedBackgroundColor: itemHighlightedBackgroundColor ?? self.itemHighlightedBackgroundColor, primaryColor: primaryColor ?? self.primaryColor, secondaryColor: secondaryColor ?? self.secondaryColor, destructiveColor: destructiveColor ?? self.destructiveColor) + } } public final class PresentationThemeSwitch { @@ -257,6 +293,10 @@ public final class PresentationThemeSwitch { self.positiveColor = positiveColor self.negativeColor = negativeColor } + + public func withUpdated(frameColor: UIColor? = nil, handleColor: UIColor? = nil, contentColor: UIColor? = nil, positiveColor: UIColor? = nil, negativeColor: UIColor? = nil) -> PresentationThemeSwitch { + return PresentationThemeSwitch(frameColor: frameColor ?? self.frameColor, handleColor: handleColor ?? self.handleColor, contentColor: contentColor ?? self.contentColor, positiveColor: positiveColor ?? self.positiveColor, negativeColor: negativeColor ?? self.negativeColor) + } } public final class PresentationThemeFillForeground { @@ -267,6 +307,10 @@ public final class PresentationThemeFillForeground { self.fillColor = fillColor self.foregroundColor = foregroundColor } + + public func withUpdated(fillColor: UIColor? = nil, foregroundColor: UIColor? = nil) -> PresentationThemeFillForeground { + return PresentationThemeFillForeground(fillColor: fillColor ?? self.fillColor, foregroundColor: foregroundColor ?? self.foregroundColor) + } } public final class PresentationThemeItemDisclosureActions { @@ -287,6 +331,10 @@ public final class PresentationThemeItemDisclosureActions { self.warning = warning self.inactive = inactive } + + public func withUpdated(neutral1: PresentationThemeFillForeground? = nil, neutral2: PresentationThemeFillForeground? = nil, destructive: PresentationThemeFillForeground? = nil, constructive: PresentationThemeFillForeground? = nil, accent: PresentationThemeFillForeground? = nil, warning: PresentationThemeFillForeground? = nil, inactive: PresentationThemeFillForeground? = nil) -> PresentationThemeItemDisclosureActions { + return PresentationThemeItemDisclosureActions(neutral1: neutral1 ?? self.neutral1, neutral2: neutral2 ?? self.neutral2, destructive: destructive ?? self.destructive, constructive: constructive ?? self.constructive, accent: accent ?? self.accent, warning: warning ?? self.warning, inactive: inactive ?? self.inactive) + } } public final class PresentationThemeItemBarChart { @@ -299,6 +347,10 @@ public final class PresentationThemeItemBarChart { self.color2 = color2 self.color3 = color3 } + + public func withUpdated(color1: UIColor? = nil, color2: UIColor? = nil, color3: UIColor? = nil) -> PresentationThemeItemBarChart { + return PresentationThemeItemBarChart(color1: color1 ?? self.color1, color2: color2 ?? self.color2, color3: color3 ?? self.color3) + } } public final class PresentationThemeFillStrokeForeground { @@ -311,6 +363,10 @@ public final class PresentationThemeFillStrokeForeground { self.strokeColor = strokeColor self.foregroundColor = foregroundColor } + + public func withUpdated(fillColor: UIColor? = nil, strokeColor: UIColor? = nil, foregroundColor: UIColor? = nil) -> PresentationThemeFillStrokeForeground { + return PresentationThemeFillStrokeForeground(fillColor: fillColor ?? self.fillColor, strokeColor: strokeColor ?? self.strokeColor, foregroundColor: foregroundColor ?? self.foregroundColor) + } } public final class PresentationInputFieldTheme { @@ -327,6 +383,10 @@ public final class PresentationInputFieldTheme { self.primaryColor = primaryColor self.controlColor = controlColor } + + public func withUpdated(backgroundColor: UIColor? = nil, strokeColor: UIColor? = nil, placeholderColor: UIColor? = nil, primaryColor: UIColor? = nil, controlColor: UIColor? = nil) -> PresentationInputFieldTheme { + return PresentationInputFieldTheme(backgroundColor: backgroundColor ?? self.backgroundColor, strokeColor: strokeColor ?? self.strokeColor, placeholderColor: placeholderColor ?? self.placeholderColor, primaryColor: primaryColor ?? self.primaryColor, controlColor: controlColor ?? self.controlColor) + } } public final class PresentationThemeList { @@ -354,13 +414,14 @@ public final class PresentationThemeList { public let itemCheckColors: PresentationThemeFillStrokeForeground public let controlSecondaryColor: UIColor public let freeInputField: PresentationInputFieldTheme + public let freePlainInputField: PresentationInputFieldTheme public let mediaPlaceholderColor: UIColor public let scrollIndicatorColor: UIColor public let pageIndicatorInactiveColor: UIColor public let inputClearButtonColor: UIColor public let itemBarChart: PresentationThemeItemBarChart - public init(blocksBackgroundColor: UIColor, plainBackgroundColor: UIColor, itemPrimaryTextColor: UIColor, itemSecondaryTextColor: UIColor, itemDisabledTextColor: UIColor, itemAccentColor: UIColor, itemHighlightedColor: UIColor, itemDestructiveColor: UIColor, itemPlaceholderTextColor: UIColor, itemBlocksBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, itemBlocksSeparatorColor: UIColor, itemPlainSeparatorColor: UIColor, disclosureArrowColor: UIColor, sectionHeaderTextColor: UIColor, freeTextColor: UIColor, freeTextErrorColor: UIColor, freeTextSuccessColor: UIColor, freeMonoIconColor: UIColor, itemSwitchColors: PresentationThemeSwitch, itemDisclosureActions: PresentationThemeItemDisclosureActions, itemCheckColors: PresentationThemeFillStrokeForeground, controlSecondaryColor: UIColor, freeInputField: PresentationInputFieldTheme, mediaPlaceholderColor: UIColor, scrollIndicatorColor: UIColor, pageIndicatorInactiveColor: UIColor, inputClearButtonColor: UIColor, itemBarChart: PresentationThemeItemBarChart) { + public init(blocksBackgroundColor: UIColor, plainBackgroundColor: UIColor, itemPrimaryTextColor: UIColor, itemSecondaryTextColor: UIColor, itemDisabledTextColor: UIColor, itemAccentColor: UIColor, itemHighlightedColor: UIColor, itemDestructiveColor: UIColor, itemPlaceholderTextColor: UIColor, itemBlocksBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, itemBlocksSeparatorColor: UIColor, itemPlainSeparatorColor: UIColor, disclosureArrowColor: UIColor, sectionHeaderTextColor: UIColor, freeTextColor: UIColor, freeTextErrorColor: UIColor, freeTextSuccessColor: UIColor, freeMonoIconColor: UIColor, itemSwitchColors: PresentationThemeSwitch, itemDisclosureActions: PresentationThemeItemDisclosureActions, itemCheckColors: PresentationThemeFillStrokeForeground, controlSecondaryColor: UIColor, freeInputField: PresentationInputFieldTheme, freePlainInputField: PresentationInputFieldTheme, mediaPlaceholderColor: UIColor, scrollIndicatorColor: UIColor, pageIndicatorInactiveColor: UIColor, inputClearButtonColor: UIColor, itemBarChart: PresentationThemeItemBarChart) { self.blocksBackgroundColor = blocksBackgroundColor self.plainBackgroundColor = plainBackgroundColor self.itemPrimaryTextColor = itemPrimaryTextColor @@ -385,12 +446,17 @@ public final class PresentationThemeList { self.itemCheckColors = itemCheckColors self.controlSecondaryColor = controlSecondaryColor self.freeInputField = freeInputField + self.freePlainInputField = freePlainInputField self.mediaPlaceholderColor = mediaPlaceholderColor self.scrollIndicatorColor = scrollIndicatorColor self.pageIndicatorInactiveColor = pageIndicatorInactiveColor self.inputClearButtonColor = inputClearButtonColor self.itemBarChart = itemBarChart } + + public func withUpdated(blocksBackgroundColor: UIColor? = nil, plainBackgroundColor: UIColor? = nil, itemPrimaryTextColor: UIColor? = nil, itemSecondaryTextColor: UIColor? = nil, itemDisabledTextColor: UIColor? = nil, itemAccentColor: UIColor? = nil, itemHighlightedColor: UIColor? = nil, itemDestructiveColor: UIColor? = nil, itemPlaceholderTextColor: UIColor? = nil, itemBlocksBackgroundColor: UIColor? = nil, itemHighlightedBackgroundColor: UIColor? = nil, itemBlocksSeparatorColor: UIColor? = nil, itemPlainSeparatorColor: UIColor? = nil, disclosureArrowColor: UIColor? = nil, sectionHeaderTextColor: UIColor? = nil, freeTextColor: UIColor? = nil, freeTextErrorColor: UIColor? = nil, freeTextSuccessColor: UIColor? = nil, freeMonoIconColor: UIColor? = nil, itemSwitchColors: PresentationThemeSwitch? = nil, itemDisclosureActions: PresentationThemeItemDisclosureActions? = nil, itemCheckColors: PresentationThemeFillStrokeForeground? = nil, controlSecondaryColor: UIColor? = nil, freeInputField: PresentationInputFieldTheme? = nil, freePlainInputField: PresentationInputFieldTheme? = nil, mediaPlaceholderColor: UIColor? = nil, scrollIndicatorColor: UIColor? = nil, pageIndicatorInactiveColor: UIColor? = nil, inputClearButtonColor: UIColor? = nil, itemBarChart: PresentationThemeItemBarChart? = nil) -> PresentationThemeList { + return PresentationThemeList(blocksBackgroundColor: blocksBackgroundColor ?? self.blocksBackgroundColor, plainBackgroundColor: plainBackgroundColor ?? self.plainBackgroundColor, itemPrimaryTextColor: itemPrimaryTextColor ?? self.itemPrimaryTextColor, itemSecondaryTextColor: itemSecondaryTextColor ?? self.itemSecondaryTextColor, itemDisabledTextColor: itemDisabledTextColor ?? self.itemDisabledTextColor, itemAccentColor: itemAccentColor ?? self.itemAccentColor, itemHighlightedColor: itemHighlightedColor ?? self.itemHighlightedColor, itemDestructiveColor: itemDestructiveColor ?? self.itemDestructiveColor, itemPlaceholderTextColor: itemPlaceholderTextColor ?? self.itemPlaceholderTextColor, itemBlocksBackgroundColor: itemBlocksBackgroundColor ?? self.itemBlocksBackgroundColor, itemHighlightedBackgroundColor: itemHighlightedBackgroundColor ?? self.itemHighlightedBackgroundColor, itemBlocksSeparatorColor: itemBlocksSeparatorColor ?? self.itemBlocksSeparatorColor, itemPlainSeparatorColor: itemPlainSeparatorColor ?? self.itemPlainSeparatorColor, disclosureArrowColor: disclosureArrowColor ?? self.disclosureArrowColor, sectionHeaderTextColor: sectionHeaderTextColor ?? self.sectionHeaderTextColor, freeTextColor: freeTextColor ?? self.freeTextColor, freeTextErrorColor: freeTextErrorColor ?? self.freeTextErrorColor, freeTextSuccessColor: freeTextSuccessColor ?? self.freeTextSuccessColor, freeMonoIconColor: freeMonoIconColor ?? self.freeMonoIconColor, itemSwitchColors: itemSwitchColors ?? self.itemSwitchColors, itemDisclosureActions: itemDisclosureActions ?? self.itemDisclosureActions, itemCheckColors: itemCheckColors ?? self.itemCheckColors, controlSecondaryColor: controlSecondaryColor ?? self.controlSecondaryColor, freeInputField: freeInputField ?? self.freeInputField, freePlainInputField: freePlainInputField ?? self.freePlainInputField, mediaPlaceholderColor: mediaPlaceholderColor ?? self.mediaPlaceholderColor, scrollIndicatorColor: scrollIndicatorColor ?? self.scrollIndicatorColor, pageIndicatorInactiveColor: pageIndicatorInactiveColor ?? self.pageIndicatorInactiveColor, inputClearButtonColor: inputClearButtonColor ?? self.inputClearButtonColor, itemBarChart: itemBarChart ?? self.itemBarChart) + } } public final class PresentationThemeArchiveAvatarColors { @@ -401,6 +467,10 @@ public final class PresentationThemeArchiveAvatarColors { self.backgroundColors = backgroundColors self.foregroundColor = foregroundColor } + + public func withUpdated(backgroundColors: PresentationThemeGradientColors? = nil, foregroundColor: UIColor? = nil) -> PresentationThemeArchiveAvatarColors { + return PresentationThemeArchiveAvatarColors(backgroundColors: backgroundColors ?? self.backgroundColors, foregroundColor: foregroundColor ?? self.foregroundColor) + } } public final class PresentationThemeChatList { @@ -473,17 +543,41 @@ public final class PresentationThemeChatList { self.unpinnedArchiveAvatarColor = unpinnedArchiveAvatarColor self.onlineDotColor = onlineDotColor } + + public func withUpdated(backgroundColor: UIColor? = nil, itemSeparatorColor: UIColor? = nil, itemBackgroundColor: UIColor? = nil, pinnedItemBackgroundColor: UIColor? = nil, itemHighlightedBackgroundColor: UIColor? = nil, itemSelectedBackgroundColor: UIColor? = nil, titleColor: UIColor? = nil, secretTitleColor: UIColor? = nil, dateTextColor: UIColor? = nil, authorNameColor: UIColor? = nil, messageTextColor: UIColor? = nil, messageHighlightedTextColor: UIColor? = nil, messageDraftTextColor: UIColor? = nil, checkmarkColor: UIColor? = nil, pendingIndicatorColor: UIColor? = nil, failedFillColor: UIColor? = nil, failedForegroundColor: UIColor? = nil, muteIconColor: UIColor? = nil, unreadBadgeActiveBackgroundColor: UIColor? = nil, unreadBadgeActiveTextColor: UIColor? = nil, unreadBadgeInactiveBackgroundColor: UIColor? = nil, unreadBadgeInactiveTextColor: UIColor? = nil, pinnedBadgeColor: UIColor? = nil, pinnedSearchBarColor: UIColor? = nil, regularSearchBarColor: UIColor? = nil, sectionHeaderFillColor: UIColor? = nil, sectionHeaderTextColor: UIColor? = nil, verifiedIconFillColor: UIColor? = nil, verifiedIconForegroundColor: UIColor? = nil, secretIconColor: UIColor? = nil, pinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors? = nil, unpinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors? = nil, onlineDotColor: UIColor? = nil) -> PresentationThemeChatList { + return PresentationThemeChatList(backgroundColor: backgroundColor ?? self.backgroundColor, itemSeparatorColor: itemSeparatorColor ?? self.itemSeparatorColor, itemBackgroundColor: itemBackgroundColor ?? self.itemBackgroundColor, pinnedItemBackgroundColor: pinnedItemBackgroundColor ?? self.pinnedItemBackgroundColor, itemHighlightedBackgroundColor: itemHighlightedBackgroundColor ?? self.itemHighlightedBackgroundColor, itemSelectedBackgroundColor: itemSelectedBackgroundColor ?? self.itemSelectedBackgroundColor, titleColor: titleColor ?? self.titleColor, secretTitleColor: secretTitleColor ?? self.secretTitleColor, dateTextColor: dateTextColor ?? self.dateTextColor, authorNameColor: authorNameColor ?? self.authorNameColor, messageTextColor: messageTextColor ?? self.messageTextColor, messageHighlightedTextColor: messageHighlightedTextColor ?? self.messageHighlightedTextColor, messageDraftTextColor: messageDraftTextColor ?? self.messageDraftTextColor, checkmarkColor: checkmarkColor ?? self.checkmarkColor, pendingIndicatorColor: pendingIndicatorColor ?? self.pendingIndicatorColor, failedFillColor: failedFillColor ?? self.failedFillColor, failedForegroundColor: failedForegroundColor ?? self.failedForegroundColor, muteIconColor: muteIconColor ?? self.muteIconColor, unreadBadgeActiveBackgroundColor: unreadBadgeActiveBackgroundColor ?? self.unreadBadgeActiveBackgroundColor, unreadBadgeActiveTextColor: unreadBadgeActiveTextColor ?? self.unreadBadgeActiveTextColor, unreadBadgeInactiveBackgroundColor: unreadBadgeInactiveBackgroundColor ?? self.unreadBadgeInactiveBackgroundColor, unreadBadgeInactiveTextColor: unreadBadgeInactiveTextColor ?? self.unreadBadgeInactiveTextColor, pinnedBadgeColor: pinnedBadgeColor ?? self.pinnedBadgeColor, pinnedSearchBarColor: pinnedSearchBarColor ?? self.pinnedSearchBarColor, regularSearchBarColor: regularSearchBarColor ?? self.regularSearchBarColor, sectionHeaderFillColor: sectionHeaderFillColor ?? self.sectionHeaderFillColor, sectionHeaderTextColor: sectionHeaderTextColor ?? self.sectionHeaderTextColor, verifiedIconFillColor: verifiedIconFillColor ?? self.verifiedIconFillColor, verifiedIconForegroundColor: verifiedIconForegroundColor ?? self.verifiedIconForegroundColor, secretIconColor: secretIconColor ?? self.secretIconColor, pinnedArchiveAvatarColor: pinnedArchiveAvatarColor ?? self.pinnedArchiveAvatarColor, unpinnedArchiveAvatarColor: unpinnedArchiveAvatarColor ?? self.unpinnedArchiveAvatarColor, onlineDotColor: onlineDotColor ?? self.onlineDotColor) + } +} + +public struct PresentationThemeBubbleShadow { + public var color: UIColor + public var radius: CGFloat + public var verticalOffset: CGFloat + + public init(color: UIColor, radius: CGFloat, verticalOffset: CGFloat) { + self.color = color + self.radius = radius + self.verticalOffset = verticalOffset + } } public final class PresentationThemeBubbleColorComponents { public let fill: UIColor + public let gradientFill: UIColor public let highlightedFill: UIColor public let stroke: UIColor + public let shadow: PresentationThemeBubbleShadow? - public init(fill: UIColor, highlightedFill: UIColor, stroke: UIColor) { + public init(fill: UIColor, gradientFill: UIColor? = nil, highlightedFill: UIColor, stroke: UIColor, shadow: PresentationThemeBubbleShadow?) { self.fill = fill + self.gradientFill = gradientFill ?? fill self.highlightedFill = highlightedFill self.stroke = stroke + self.shadow = shadow + } + + public func withUpdated(fill: UIColor? = nil, gradientFill: UIColor? = nil, highlightedFill: UIColor? = nil, stroke: UIColor? = nil) -> PresentationThemeBubbleColorComponents { + return PresentationThemeBubbleColorComponents(fill: fill ?? self.fill, gradientFill: gradientFill ?? self.gradientFill, highlightedFill: highlightedFill ?? self.highlightedFill, stroke: stroke ?? self.stroke, shadow: self.shadow) } } @@ -495,6 +589,10 @@ public final class PresentationThemeBubbleColor { self.withWallpaper = withWallpaper self.withoutWallpaper = withoutWallpaper } + + public func withUpdated(withWallpaper: PresentationThemeBubbleColorComponents? = nil, withoutWallpaper: PresentationThemeBubbleColorComponents? = nil) -> PresentationThemeBubbleColor { + return PresentationThemeBubbleColor(withWallpaper: withWallpaper ?? self.withWallpaper, withoutWallpaper: withoutWallpaper ?? self.withoutWallpaper) + } } public final class PresentationThemeVariableColor { @@ -510,6 +608,10 @@ public final class PresentationThemeVariableColor { self.withWallpaper = color self.withoutWallpaper = color } + + public func withUpdated(withWallpaper: UIColor? = nil, withoutWallpaper: UIColor? = nil) -> PresentationThemeVariableColor { + return PresentationThemeVariableColor(withWallpaper: withWallpaper ?? self.withWallpaper, withoutWallpaper: withoutWallpaper ?? self.withoutWallpaper) + } } public func bubbleColorComponents(theme: PresentationTheme, incoming: Bool, wallpaper: Bool) -> PresentationThemeBubbleColorComponents { @@ -543,13 +645,23 @@ public final class PresentationThemeChatBubblePolls { public let highlight: UIColor public let separator: UIColor public let bar: UIColor + public let barIconForeground: UIColor + public let barPositive: UIColor + public let barNegative: UIColor - public init(radioButton: UIColor, radioProgress: UIColor, highlight: UIColor, separator: UIColor, bar: UIColor) { + public init(radioButton: UIColor, radioProgress: UIColor, highlight: UIColor, separator: UIColor, bar: UIColor, barIconForeground: UIColor, barPositive: UIColor, barNegative: UIColor) { self.radioButton = radioButton self.radioProgress = radioProgress self.highlight = highlight self.separator = separator self.bar = bar + self.barIconForeground = barIconForeground + self.barPositive = barPositive + self.barNegative = barNegative + } + + public func withUpdated(radioButton: UIColor? = nil, radioProgress: UIColor? = nil, highlight: UIColor? = nil, separator: UIColor? = nil, bar: UIColor? = nil, barIconForeground: UIColor? = nil, barPositive: UIColor? = nil, barNegative: UIColor? = nil) -> PresentationThemeChatBubblePolls { + return PresentationThemeChatBubblePolls(radioButton: radioButton ?? self.radioButton, radioProgress: radioProgress ?? self.radioProgress, highlight: highlight ?? self.highlight, separator: separator ?? self.separator, bar: bar ?? self.bar, barIconForeground: barIconForeground ?? self.barIconForeground, barPositive: barPositive ?? self.barPositive, barNegative: barNegative ?? self.barNegative) } } @@ -563,6 +675,7 @@ public final class PresentationThemePartedColors { public let textHighlightColor: UIColor public let accentTextColor: UIColor public let accentControlColor: UIColor + public let accentControlDisabledColor: UIColor public let mediaActiveControlColor: UIColor public let mediaInactiveControlColor: UIColor public let mediaControlInnerBackgroundColor: UIColor @@ -578,7 +691,7 @@ public final class PresentationThemePartedColors { public let textSelectionColor: UIColor public let textSelectionKnobColor: UIColor - public init(bubble: PresentationThemeBubbleColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, linkTextColor: UIColor, linkHighlightColor: UIColor, scamColor: UIColor, textHighlightColor: UIColor, accentTextColor: UIColor, accentControlColor: UIColor, mediaActiveControlColor: UIColor, mediaInactiveControlColor: UIColor, mediaControlInnerBackgroundColor: UIColor, pendingActivityColor: UIColor, fileTitleColor: UIColor, fileDescriptionColor: UIColor, fileDurationColor: UIColor, mediaPlaceholderColor: UIColor, polls: PresentationThemeChatBubblePolls, actionButtonsFillColor: PresentationThemeVariableColor, actionButtonsStrokeColor: PresentationThemeVariableColor, actionButtonsTextColor: PresentationThemeVariableColor, textSelectionColor: UIColor, textSelectionKnobColor: UIColor) { + public init(bubble: PresentationThemeBubbleColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, linkTextColor: UIColor, linkHighlightColor: UIColor, scamColor: UIColor, textHighlightColor: UIColor, accentTextColor: UIColor, accentControlColor: UIColor, accentControlDisabledColor: UIColor, mediaActiveControlColor: UIColor, mediaInactiveControlColor: UIColor, mediaControlInnerBackgroundColor: UIColor, pendingActivityColor: UIColor, fileTitleColor: UIColor, fileDescriptionColor: UIColor, fileDurationColor: UIColor, mediaPlaceholderColor: UIColor, polls: PresentationThemeChatBubblePolls, actionButtonsFillColor: PresentationThemeVariableColor, actionButtonsStrokeColor: PresentationThemeVariableColor, actionButtonsTextColor: PresentationThemeVariableColor, textSelectionColor: UIColor, textSelectionKnobColor: UIColor) { self.bubble = bubble self.primaryTextColor = primaryTextColor self.secondaryTextColor = secondaryTextColor @@ -588,6 +701,7 @@ public final class PresentationThemePartedColors { self.textHighlightColor = textHighlightColor self.accentTextColor = accentTextColor self.accentControlColor = accentControlColor + self.accentControlDisabledColor = accentControlDisabledColor self.mediaActiveControlColor = mediaActiveControlColor self.mediaInactiveControlColor = mediaInactiveControlColor self.mediaControlInnerBackgroundColor = mediaControlInnerBackgroundColor @@ -603,52 +717,50 @@ public final class PresentationThemePartedColors { self.textSelectionColor = textSelectionColor self.textSelectionKnobColor = textSelectionKnobColor } + + public func withUpdated(bubble: PresentationThemeBubbleColor? = nil, primaryTextColor: UIColor? = nil, secondaryTextColor: UIColor? = nil, linkTextColor: UIColor? = nil, linkHighlightColor: UIColor? = nil, scamColor: UIColor? = nil, textHighlightColor: UIColor? = nil, accentTextColor: UIColor? = nil, accentControlColor: UIColor? = nil, accentControlDisabledColor: UIColor? = nil, mediaActiveControlColor: UIColor? = nil, mediaInactiveControlColor: UIColor? = nil, mediaControlInnerBackgroundColor: UIColor? = nil, pendingActivityColor: UIColor? = nil, fileTitleColor: UIColor? = nil, fileDescriptionColor: UIColor? = nil, fileDurationColor: UIColor? = nil, mediaPlaceholderColor: UIColor? = nil, polls: PresentationThemeChatBubblePolls? = nil, actionButtonsFillColor: PresentationThemeVariableColor? = nil, actionButtonsStrokeColor: PresentationThemeVariableColor? = nil, actionButtonsTextColor: PresentationThemeVariableColor? = nil, textSelectionColor: UIColor? = nil, textSelectionKnobColor: UIColor? = nil) -> PresentationThemePartedColors { + return PresentationThemePartedColors(bubble: bubble ?? self.bubble, primaryTextColor: primaryTextColor ?? self.primaryTextColor, secondaryTextColor: secondaryTextColor ?? self.secondaryTextColor, linkTextColor: linkTextColor ?? self.linkTextColor, linkHighlightColor: linkHighlightColor ?? self.linkHighlightColor, scamColor: scamColor ?? self.scamColor, textHighlightColor: textHighlightColor ?? self.textHighlightColor, accentTextColor: accentTextColor ?? self.accentTextColor, accentControlColor: accentControlColor ?? self.accentControlColor, accentControlDisabledColor: accentControlDisabledColor ?? self.accentControlDisabledColor, mediaActiveControlColor: mediaActiveControlColor ?? self.mediaActiveControlColor, mediaInactiveControlColor: mediaInactiveControlColor ?? self.mediaInactiveControlColor, mediaControlInnerBackgroundColor: mediaControlInnerBackgroundColor ?? self.mediaControlInnerBackgroundColor, pendingActivityColor: pendingActivityColor ?? self.pendingActivityColor, fileTitleColor: fileTitleColor ?? self.fileTitleColor, fileDescriptionColor: fileDescriptionColor ?? self.fileDescriptionColor, fileDurationColor: fileDurationColor ?? self.fileDurationColor, mediaPlaceholderColor: mediaPlaceholderColor ?? self.mediaPlaceholderColor, polls: polls ?? self.polls, actionButtonsFillColor: actionButtonsFillColor ?? self.actionButtonsFillColor, actionButtonsStrokeColor: actionButtonsStrokeColor ?? self.actionButtonsStrokeColor, actionButtonsTextColor: actionButtonsTextColor ?? self.actionButtonsTextColor, textSelectionColor: textSelectionColor ?? self.textSelectionColor, textSelectionKnobColor: textSelectionKnobColor ?? self.textSelectionKnobColor) + } } public final class PresentationThemeChatMessage { public let incoming: PresentationThemePartedColors public let outgoing: PresentationThemePartedColors public let freeform: PresentationThemeBubbleColor - public let infoPrimaryTextColor: UIColor public let infoLinkTextColor: UIColor - public let outgoingCheckColor: UIColor public let mediaDateAndStatusFillColor: UIColor public let mediaDateAndStatusTextColor: UIColor - public let shareButtonFillColor: PresentationThemeVariableColor public let shareButtonStrokeColor: PresentationThemeVariableColor public let shareButtonForegroundColor: PresentationThemeVariableColor - public let mediaOverlayControlColors: PresentationThemeFillForeground public let selectionControlColors: PresentationThemeFillStrokeForeground public let deliveryFailedColors: PresentationThemeFillForeground - public let mediaHighlightOverlayColor: UIColor public init(incoming: PresentationThemePartedColors, outgoing: PresentationThemePartedColors, freeform: PresentationThemeBubbleColor, infoPrimaryTextColor: UIColor, infoLinkTextColor: UIColor, outgoingCheckColor: UIColor, mediaDateAndStatusFillColor: UIColor, mediaDateAndStatusTextColor: UIColor, shareButtonFillColor: PresentationThemeVariableColor, shareButtonStrokeColor: PresentationThemeVariableColor, shareButtonForegroundColor: PresentationThemeVariableColor, mediaOverlayControlColors: PresentationThemeFillForeground, selectionControlColors: PresentationThemeFillStrokeForeground, deliveryFailedColors: PresentationThemeFillForeground, mediaHighlightOverlayColor: UIColor) { self.incoming = incoming self.outgoing = outgoing self.freeform = freeform - self.infoPrimaryTextColor = infoPrimaryTextColor self.infoLinkTextColor = infoLinkTextColor - self.outgoingCheckColor = outgoingCheckColor self.mediaDateAndStatusFillColor = mediaDateAndStatusFillColor self.mediaDateAndStatusTextColor = mediaDateAndStatusTextColor - self.shareButtonFillColor = shareButtonFillColor self.shareButtonStrokeColor = shareButtonStrokeColor self.shareButtonForegroundColor = shareButtonForegroundColor - self.mediaOverlayControlColors = mediaOverlayControlColors self.selectionControlColors = selectionControlColors self.deliveryFailedColors = deliveryFailedColors - self.mediaHighlightOverlayColor = mediaHighlightOverlayColor } + + public func withUpdated(incoming: PresentationThemePartedColors? = nil, outgoing: PresentationThemePartedColors? = nil, freeform: PresentationThemeBubbleColor? = nil, infoPrimaryTextColor: UIColor? = nil, infoLinkTextColor: UIColor? = nil, outgoingCheckColor: UIColor? = nil, mediaDateAndStatusFillColor: UIColor? = nil, mediaDateAndStatusTextColor: UIColor? = nil, shareButtonFillColor: PresentationThemeVariableColor? = nil, shareButtonStrokeColor: PresentationThemeVariableColor? = nil, shareButtonForegroundColor: PresentationThemeVariableColor? = nil, mediaOverlayControlColors: PresentationThemeFillForeground? = nil, selectionControlColors: PresentationThemeFillStrokeForeground? = nil, deliveryFailedColors: PresentationThemeFillForeground? = nil, mediaHighlightOverlayColor: UIColor? = nil) -> PresentationThemeChatMessage { + return PresentationThemeChatMessage(incoming: incoming ?? self.incoming, outgoing: outgoing ?? self.outgoing, freeform: freeform ?? self.freeform, infoPrimaryTextColor: infoPrimaryTextColor ?? self.infoPrimaryTextColor, infoLinkTextColor: infoLinkTextColor ?? self.infoLinkTextColor, outgoingCheckColor: outgoingCheckColor ?? self.outgoingCheckColor, mediaDateAndStatusFillColor: mediaDateAndStatusFillColor ?? self.mediaDateAndStatusFillColor, mediaDateAndStatusTextColor: mediaDateAndStatusTextColor ?? self.mediaDateAndStatusTextColor, shareButtonFillColor: shareButtonFillColor ?? self.shareButtonFillColor, shareButtonStrokeColor: shareButtonStrokeColor ?? self.shareButtonStrokeColor, shareButtonForegroundColor: shareButtonForegroundColor ?? self.shareButtonForegroundColor, mediaOverlayControlColors: mediaOverlayControlColors ?? self.mediaOverlayControlColors, selectionControlColors: selectionControlColors ?? self.selectionControlColors, deliveryFailedColors: deliveryFailedColors ?? self.deliveryFailedColors, mediaHighlightOverlayColor: mediaHighlightOverlayColor ?? self.mediaHighlightOverlayColor) + } } public final class PresentationThemeServiceMessageColorComponents { @@ -668,21 +780,29 @@ public final class PresentationThemeServiceMessageColorComponents { self.dateFillStatic = dateFillStatic self.dateFillFloating = dateFillFloating } + + public func withUpdated(fill: UIColor? = nil, primaryText: UIColor? = nil, linkHighlight: UIColor? = nil, scam: UIColor? = nil, dateFillStatic: UIColor? = nil, dateFillFloating: UIColor? = nil) -> PresentationThemeServiceMessageColorComponents { + return PresentationThemeServiceMessageColorComponents(fill: fill ?? self.fill, primaryText: primaryText ?? self.primaryText, linkHighlight: linkHighlight ?? self.linkHighlight, scam: scam ?? self.scam, dateFillStatic: dateFillStatic ?? self.dateFillStatic, dateFillFloating: dateFillFloating ?? self.dateFillFloating) + } } public func serviceMessageColorComponents(theme: PresentationTheme, wallpaper: TelegramWallpaper) -> PresentationThemeServiceMessageColorComponents { return serviceMessageColorComponents(chatTheme: theme.chat, wallpaper: wallpaper) } -public func serviceMessageColorComponents(chatTheme: PresentationThemeChat, wallpaper: TelegramWallpaper) -> PresentationThemeServiceMessageColorComponents { +public func serviceMessageColorHasDefaultWallpaper(_ wallpaper: TelegramWallpaper) -> Bool { switch wallpaper { case .color(0xffffff): - return chatTheme.serviceMessage.components.withDefaultWallpaper + return true default: - return chatTheme.serviceMessage.components.withCustomWallpaper + return false } } +public func serviceMessageColorComponents(chatTheme: PresentationThemeChat, wallpaper: TelegramWallpaper) -> PresentationThemeServiceMessageColorComponents { + return serviceMessageColorHasDefaultWallpaper(wallpaper) ? chatTheme.serviceMessage.components.withDefaultWallpaper : chatTheme.serviceMessage.components.withCustomWallpaper +} + public final class PresentationThemeServiceMessageColor { public let withDefaultWallpaper: PresentationThemeServiceMessageColorComponents public let withCustomWallpaper: PresentationThemeServiceMessageColorComponents @@ -691,15 +811,17 @@ public final class PresentationThemeServiceMessageColor { self.withDefaultWallpaper = withDefaultWallpaper self.withCustomWallpaper = withCustomWallpaper } + + public func withUpdated(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents? = nil, withCustomWallpaper: PresentationThemeServiceMessageColorComponents? = nil) -> PresentationThemeServiceMessageColor { + return PresentationThemeServiceMessageColor(withDefaultWallpaper: withDefaultWallpaper ?? self.withDefaultWallpaper, withCustomWallpaper: withCustomWallpaper ?? self.withCustomWallpaper) + } } public final class PresentationThemeServiceMessage { public let components: PresentationThemeServiceMessageColor - public let unreadBarFillColor: UIColor public let unreadBarStrokeColor: UIColor public let unreadBarTextColor: UIColor - public let dateTextColor: PresentationThemeVariableColor public init(components: PresentationThemeServiceMessageColor, unreadBarFillColor: UIColor, unreadBarStrokeColor: UIColor, unreadBarTextColor: UIColor, dateTextColor: PresentationThemeVariableColor) { @@ -709,6 +831,10 @@ public final class PresentationThemeServiceMessage { self.unreadBarTextColor = unreadBarTextColor self.dateTextColor = dateTextColor } + + public func withUpdated(components: PresentationThemeServiceMessageColor? = nil, unreadBarFillColor: UIColor? = nil, unreadBarStrokeColor: UIColor? = nil, unreadBarTextColor: UIColor? = nil, dateTextColor: PresentationThemeVariableColor? = nil) -> PresentationThemeServiceMessage { + return PresentationThemeServiceMessage(components: components ?? self.components, unreadBarFillColor: unreadBarFillColor ?? self.unreadBarFillColor, unreadBarStrokeColor: unreadBarStrokeColor ?? self.unreadBarStrokeColor, unreadBarTextColor: unreadBarTextColor ?? self.unreadBarTextColor, dateTextColor: dateTextColor ?? self.dateTextColor) + } } public enum PresentationThemeKeyboardColor: Int32 { @@ -735,10 +861,15 @@ public final class PresentationThemeChatInputPanelMediaRecordingControl { self.micLevelColor = micLevelColor self.activeIconColor = activeIconColor } + + public func withUpdated(buttonColor: UIColor? = nil, micLevelColor: UIColor? = nil, activeIconColor: UIColor? = nil) -> PresentationThemeChatInputPanelMediaRecordingControl { + return PresentationThemeChatInputPanelMediaRecordingControl(buttonColor: buttonColor ?? self.buttonColor, micLevelColor: micLevelColor ?? self.micLevelColor, activeIconColor: activeIconColor ?? self.activeIconColor) + } } public final class PresentationThemeChatInputPanel { public let panelBackgroundColor: UIColor + public let panelBackgroundColorNoWallpaper: UIColor public let panelSeparatorColor: UIColor public let panelControlAccentColor: UIColor public let panelControlColor: UIColor @@ -756,8 +887,9 @@ public final class PresentationThemeChatInputPanel { public let mediaRecordingDotColor: UIColor public let mediaRecordingControl: PresentationThemeChatInputPanelMediaRecordingControl - public init(panelBackgroundColor: UIColor, panelSeparatorColor: UIColor, panelControlAccentColor: UIColor, panelControlColor: UIColor, panelControlDisabledColor: UIColor, panelControlDestructiveColor: UIColor, inputBackgroundColor: UIColor, inputStrokeColor: UIColor, inputPlaceholderColor: UIColor, inputTextColor: UIColor, inputControlColor: UIColor, actionControlFillColor: UIColor, actionControlForegroundColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, mediaRecordingDotColor: UIColor, mediaRecordingControl: PresentationThemeChatInputPanelMediaRecordingControl) { + public init(panelBackgroundColor: UIColor, panelBackgroundColorNoWallpaper: UIColor, panelSeparatorColor: UIColor, panelControlAccentColor: UIColor, panelControlColor: UIColor, panelControlDisabledColor: UIColor, panelControlDestructiveColor: UIColor, inputBackgroundColor: UIColor, inputStrokeColor: UIColor, inputPlaceholderColor: UIColor, inputTextColor: UIColor, inputControlColor: UIColor, actionControlFillColor: UIColor, actionControlForegroundColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, mediaRecordingDotColor: UIColor, mediaRecordingControl: PresentationThemeChatInputPanelMediaRecordingControl) { self.panelBackgroundColor = panelBackgroundColor + self.panelBackgroundColorNoWallpaper = panelBackgroundColorNoWallpaper self.panelSeparatorColor = panelSeparatorColor self.panelControlAccentColor = panelControlAccentColor self.panelControlColor = panelControlColor @@ -775,6 +907,10 @@ public final class PresentationThemeChatInputPanel { self.mediaRecordingDotColor = mediaRecordingDotColor self.mediaRecordingControl = mediaRecordingControl } + + public func withUpdated(panelBackgroundColor: UIColor? = nil, panelBackgroundColorNoWallpaper: UIColor? = nil, panelSeparatorColor: UIColor? = nil, panelControlAccentColor: UIColor? = nil, panelControlColor: UIColor? = nil, panelControlDisabledColor: UIColor? = nil, panelControlDestructiveColor: UIColor? = nil, inputBackgroundColor: UIColor? = nil, inputStrokeColor: UIColor? = nil, inputPlaceholderColor: UIColor? = nil, inputTextColor: UIColor? = nil, inputControlColor: UIColor? = nil, actionControlFillColor: UIColor? = nil, actionControlForegroundColor: UIColor? = nil, primaryTextColor: UIColor? = nil, secondaryTextColor: UIColor? = nil, mediaRecordingDotColor: UIColor? = nil, mediaRecordingControl: PresentationThemeChatInputPanelMediaRecordingControl? = nil) -> PresentationThemeChatInputPanel { + return PresentationThemeChatInputPanel(panelBackgroundColor: panelBackgroundColor ?? self.panelBackgroundColor, panelBackgroundColorNoWallpaper: panelBackgroundColorNoWallpaper ?? self.panelBackgroundColorNoWallpaper, panelSeparatorColor: panelSeparatorColor ?? self.panelSeparatorColor, panelControlAccentColor: panelControlAccentColor ?? self.panelControlAccentColor, panelControlColor: panelControlColor ?? self.panelControlColor, panelControlDisabledColor: panelControlDisabledColor ?? self.panelControlDisabledColor, panelControlDestructiveColor: panelControlDestructiveColor ?? self.panelControlDestructiveColor, inputBackgroundColor: inputBackgroundColor ?? self.inputBackgroundColor, inputStrokeColor: inputStrokeColor ?? self.inputStrokeColor, inputPlaceholderColor: inputPlaceholderColor ?? self.inputPlaceholderColor, inputTextColor: inputTextColor ?? self.inputTextColor, inputControlColor: inputControlColor ?? self.inputControlColor, actionControlFillColor: actionControlFillColor ?? self.actionControlFillColor, actionControlForegroundColor: actionControlForegroundColor ?? self.actionControlForegroundColor, primaryTextColor: primaryTextColor ?? self.primaryTextColor, secondaryTextColor: secondaryTextColor ?? self.secondaryTextColor, mediaRecordingDotColor: mediaRecordingDotColor ?? self.mediaRecordingDotColor, mediaRecordingControl: mediaRecordingControl ?? self.mediaRecordingControl) + } } public final class PresentationThemeInputMediaPanel { @@ -801,6 +937,10 @@ public final class PresentationThemeInputMediaPanel { self.stickersSearchControlColor = stickersSearchControlColor self.gifsBackgroundColor = gifsBackgroundColor } + + public func withUpdated(panelSeparatorColor: UIColor? = nil, panelIconColor: UIColor? = nil, panelHighlightedIconBackgroundColor: UIColor? = nil, stickersBackgroundColor: UIColor? = nil, stickersSectionTextColor: UIColor? = nil, stickersSearchBackgroundColor: UIColor? = nil, stickersSearchPlaceholderColor: UIColor? = nil, stickersSearchPrimaryColor: UIColor? = nil, stickersSearchControlColor: UIColor? = nil, gifsBackgroundColor: UIColor? = nil) -> PresentationThemeInputMediaPanel { + return PresentationThemeInputMediaPanel(panelSeparatorColor: panelSeparatorColor ?? self.panelSeparatorColor, panelIconColor: panelIconColor ?? self.panelIconColor, panelHighlightedIconBackgroundColor: panelHighlightedIconBackgroundColor ?? self.panelHighlightedIconBackgroundColor, stickersBackgroundColor: stickersBackgroundColor ?? self.stickersBackgroundColor, stickersSectionTextColor: stickersSectionTextColor ?? self.stickersSectionTextColor, stickersSearchBackgroundColor: stickersSearchBackgroundColor ?? self.stickersSearchBackgroundColor, stickersSearchPlaceholderColor: stickersSearchPlaceholderColor ?? self.stickersSearchPlaceholderColor, stickersSearchPrimaryColor: stickersSearchPrimaryColor ?? self.stickersSearchPrimaryColor, stickersSearchControlColor: stickersSearchControlColor ?? self.stickersSearchControlColor, gifsBackgroundColor: gifsBackgroundColor ?? self.gifsBackgroundColor) + } } public final class PresentationThemeInputButtonPanel { @@ -821,6 +961,10 @@ public final class PresentationThemeInputButtonPanel { self.buttonHighlightedStrokeColor = buttonHighlightedStrokeColor self.buttonTextColor = buttonTextColor } + + public func withUpdated(panelSeparatorColor: UIColor? = nil, panelBackgroundColor: UIColor? = nil, buttonFillColor: UIColor? = nil, buttonStrokeColor: UIColor? = nil, buttonHighlightedFillColor: UIColor? = nil, buttonHighlightedStrokeColor: UIColor? = nil, buttonTextColor: UIColor? = nil) -> PresentationThemeInputButtonPanel { + return PresentationThemeInputButtonPanel(panelSeparatorColor: panelSeparatorColor ?? self.panelSeparatorColor, panelBackgroundColor: panelBackgroundColor ?? self.panelBackgroundColor, buttonFillColor: buttonFillColor ?? self.buttonFillColor, buttonStrokeColor: buttonStrokeColor ?? self.buttonStrokeColor, buttonHighlightedFillColor: buttonHighlightedFillColor ?? self.buttonHighlightedFillColor, buttonHighlightedStrokeColor: buttonHighlightedStrokeColor ?? self.buttonHighlightedStrokeColor, buttonTextColor: buttonTextColor ?? self.buttonTextColor) + } } public final class PresentationThemeChatHistoryNavigation { @@ -839,6 +983,10 @@ public final class PresentationThemeChatHistoryNavigation { self.badgeStrokeColor = badgeStrokeColor self.badgeTextColor = badgeTextColor } + + public func withUpdated(fillColor: UIColor? = nil, strokeColor: UIColor? = nil, foregroundColor: UIColor? = nil, badgeBackgroundColor: UIColor? = nil, badgeStrokeColor: UIColor? = nil, badgeTextColor: UIColor? = nil) -> PresentationThemeChatHistoryNavigation { + return PresentationThemeChatHistoryNavigation(fillColor: fillColor ?? self.fillColor, strokeColor: strokeColor ?? self.strokeColor, foregroundColor: foregroundColor ?? self.foregroundColor, badgeBackgroundColor: badgeBackgroundColor ?? self.badgeBackgroundColor, badgeStrokeColor: badgeStrokeColor ?? self.badgeStrokeColor, badgeTextColor: badgeTextColor ?? self.badgeTextColor) + } } public final class PresentationThemeChat { @@ -860,8 +1008,8 @@ public final class PresentationThemeChat { self.historyNavigation = historyNavigation } - public func withUpdatedDefaultWallpaper(_ defaultWallpaper: TelegramWallpaper?) -> PresentationThemeChat { - return PresentationThemeChat(defaultWallpaper: defaultWallpaper ?? self.defaultWallpaper, message: self.message, serviceMessage: self.serviceMessage, inputPanel: self.inputPanel, inputMediaPanel: self.inputMediaPanel, inputButtonPanel: self.inputButtonPanel, historyNavigation: self.historyNavigation) + public func withUpdated(defaultWallpaper: TelegramWallpaper? = nil, message: PresentationThemeChatMessage? = nil, serviceMessage: PresentationThemeServiceMessage? = nil, inputPanel: PresentationThemeChatInputPanel? = nil, inputMediaPanel: PresentationThemeInputMediaPanel? = nil, inputButtonPanel: PresentationThemeInputButtonPanel? = nil, historyNavigation: PresentationThemeChatHistoryNavigation? = nil) -> PresentationThemeChat { + return PresentationThemeChat(defaultWallpaper: defaultWallpaper ?? self.defaultWallpaper, message: message ?? self.message, serviceMessage: serviceMessage ?? self.serviceMessage, inputPanel: inputPanel ?? self.inputPanel, inputMediaPanel: inputMediaPanel ?? self.inputMediaPanel, inputButtonPanel: inputButtonPanel ?? self.inputButtonPanel, historyNavigation: historyNavigation ?? self.historyNavigation) } } @@ -882,6 +1030,10 @@ public final class PresentationThemeExpandedNotificationNavigationBar { self.controlColor = controlColor self.separatorColor = separatorColor } + + public func withUpdated(backgroundColor: UIColor? = nil, primaryTextColor: UIColor? = nil, controlColor: UIColor? = nil, separatorColor: UIColor? = nil) -> PresentationThemeExpandedNotificationNavigationBar { + return PresentationThemeExpandedNotificationNavigationBar(backgroundColor: backgroundColor ?? self.backgroundColor, primaryTextColor: primaryTextColor ?? self.primaryTextColor, controlColor: controlColor ?? self.controlColor, separatorColor: separatorColor ?? self.separatorColor) + } } public final class PresentationThemeExpandedNotification { @@ -892,6 +1044,10 @@ public final class PresentationThemeExpandedNotification { self.backgroundType = backgroundType self.navigationBar = navigationBar } + + public func withUpdated(backgroundType: PresentationThemeExpandedNotificationBackgroundType? = nil, navigationBar: PresentationThemeExpandedNotificationNavigationBar? = nil) -> PresentationThemeExpandedNotification { + return PresentationThemeExpandedNotification(backgroundType: backgroundType ?? self.backgroundType, navigationBar: navigationBar ?? self.navigationBar) + } } public final class PresentationThemeInAppNotification { @@ -905,6 +1061,10 @@ public final class PresentationThemeInAppNotification { self.primaryTextColor = primaryTextColor self.expandedNotification = expandedNotification } + + public func withUpdated(fillColor: UIColor? = nil, primaryTextColor: UIColor? = nil, expandedNotification: PresentationThemeExpandedNotification? = nil) -> PresentationThemeInAppNotification { + return PresentationThemeInAppNotification(fillColor: fillColor ?? self.fillColor, primaryTextColor: primaryTextColor ?? self.primaryTextColor, expandedNotification: expandedNotification ?? self.expandedNotification) + } } public enum PresentationThemeBuiltinName { @@ -967,11 +1127,33 @@ public enum PresentationThemeName: Equatable { } } +public extension PresentationThemeReference { + public var name: PresentationThemeName { + switch self { + case let .builtin(theme): + switch theme { + case .day: + return .builtin(.day) + case .dayClassic: + return .builtin(.dayClassic) + case .night: + return .builtin(.night) + case .nightAccent: + return .builtin(.nightAccent) + } + case let .cloud(info): + return .custom(info.theme.title) + default: + return .custom("") + } + } +} + public final class PresentationTheme: Equatable { public let name: PresentationThemeName + public let index: Int64 public let referenceTheme: PresentationBuiltinThemeReference public let overallDarkAppearance: Bool - public let baseColor: PresentationThemeBaseColor? public let intro: PresentationThemeIntro public let passcode: PresentationThemePasscode public let rootController: PresentationThemeRootController @@ -985,11 +1167,11 @@ public final class PresentationTheme: Equatable { public let resourceCache: PresentationsResourceCache = PresentationsResourceCache() - public init(name: PresentationThemeName, referenceTheme: PresentationBuiltinThemeReference, overallDarkAppearance: Bool, baseColor: PresentationThemeBaseColor?, intro: PresentationThemeIntro, passcode: PresentationThemePasscode, rootController: PresentationThemeRootController, list: PresentationThemeList, chatList: PresentationThemeChatList, chat: PresentationThemeChat, actionSheet: PresentationThemeActionSheet, contextMenu: PresentationThemeContextMenu, inAppNotification: PresentationThemeInAppNotification, preview: Bool = false) { + public init(name: PresentationThemeName, index: Int64, referenceTheme: PresentationBuiltinThemeReference, overallDarkAppearance: Bool, intro: PresentationThemeIntro, passcode: PresentationThemePasscode, rootController: PresentationThemeRootController, list: PresentationThemeList, chatList: PresentationThemeChatList, chat: PresentationThemeChat, actionSheet: PresentationThemeActionSheet, contextMenu: PresentationThemeContextMenu, inAppNotification: PresentationThemeInAppNotification, preview: Bool = false) { self.name = name + self.index = index self.referenceTheme = referenceTheme self.overallDarkAppearance = overallDarkAppearance - self.baseColor = baseColor self.intro = intro self.passcode = passcode self.rootController = rootController @@ -1006,10 +1188,18 @@ public final class PresentationTheme: Equatable { return self.resourceCache.image(key, self, generate) } + public func image(_ key: PresentationResourceParameterKey, _ generate: (PresentationTheme) -> UIImage?) -> UIImage? { + return self.resourceCache.parameterImage(key, self, generate) + } + public func object(_ key: Int32, _ generate: (PresentationTheme) -> AnyObject?) -> AnyObject? { return self.resourceCache.object(key, self, generate) } + public func object(_ key: PresentationResourceParameterKey, _ generate: (PresentationTheme) -> AnyObject?) -> AnyObject? { + return self.resourceCache.parameterObject(key, self, generate) + } + public static func ==(lhs: PresentationTheme, rhs: PresentationTheme) -> Bool { return lhs === rhs } @@ -1024,6 +1214,10 @@ public final class PresentationTheme: Equatable { break } } - return PresentationTheme(name: name.flatMap(PresentationThemeName.custom) ?? .custom(self.name.string), referenceTheme: self.referenceTheme, overallDarkAppearance: self.overallDarkAppearance, baseColor: nil, intro: self.intro, passcode: self.passcode, rootController: self.rootController, list: self.list, chatList: self.chatList, chat: self.chat.withUpdatedDefaultWallpaper(defaultWallpaper), actionSheet: self.actionSheet, contextMenu: self.contextMenu, inAppNotification: self.inAppNotification) + return PresentationTheme(name: name.flatMap(PresentationThemeName.custom) ?? .custom(self.name.string), index: self.index, referenceTheme: self.referenceTheme, overallDarkAppearance: self.overallDarkAppearance, intro: self.intro, passcode: self.passcode, rootController: self.rootController, list: self.list, chatList: self.chatList, chat: self.chat.withUpdated(defaultWallpaper: defaultWallpaper), actionSheet: self.actionSheet, contextMenu: self.contextMenu, inAppNotification: self.inAppNotification) + } + + public func withUpdated(preview: Bool) -> PresentationTheme { + return PresentationTheme(name: self.name, index: self.index, referenceTheme: self.referenceTheme, overallDarkAppearance: self.overallDarkAppearance, intro: self.intro, passcode: self.passcode, rootController: self.rootController, list: self.list, chatList: self.chatList, chat: self.chat, actionSheet: self.actionSheet, contextMenu: self.contextMenu, inAppNotification: self.inAppNotification, preview: preview) } } diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift index 2c14713e3b..18b7119106 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift @@ -39,38 +39,72 @@ extension TelegramWallpaper: Codable { case "builtin": self = .builtin(WallpaperSettings()) default: - if [6,7].contains(value.count), let color = UIColor(hexString: value) { - self = .color(Int32(bitPattern: color.rgb)) + let optionKeys = ["motion", "blur"] + + if [6, 8].contains(value.count), let color = UIColor(hexString: value) { + self = .color(color.argb) } else { let components = value.components(separatedBy: " ") - var slug: String? - var color: Int32? - var intensity: Int32? var blur = false var motion = false - if !components.isEmpty { - slug = components[0] - } - if components.count > 1, !["motion", "blur"].contains(components[1]), components[1].count == 6, let value = UIColor(hexString: components[1]) { - color = Int32(bitPattern: value.rgb) - } - if components.count > 2, !["motion", "blur"].contains(components[2]), let value = Int32(components[2]) { - if value >= 0 && value <= 100 { - intensity = value - } else { - intensity = 50 - } - } if components.contains("motion") { motion = true } if components.contains("blur") { blur = true } - if let slug = slug { - self = .file(id: 0, accessHash: 0, isCreator: false, isDefault: false, isPattern: color != nil, isDark: false, slug: slug, file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], immediateThumbnailData: nil, mimeType: "", size: nil, attributes: []), settings: WallpaperSettings(blur: blur, motion: motion, color: color, intensity: intensity)) + + if components.count >= 2 && components.count <= 5 && [6, 8].contains(components[0].count) && !optionKeys.contains(components[0]) && [6, 8].contains(components[1].count) && !optionKeys.contains(components[1]), let topColor = UIColor(hexString: components[0]), let bottomColor = UIColor(hexString: components[1]) { + var rotation: Int32? + if components.count > 2, components[2].count <= 3, let value = Int32(components[2]) { + if value >= 0 && value < 360 { + rotation = value + } + } + + self = .gradient(topColor.argb, bottomColor.argb, WallpaperSettings(blur: blur, motion: motion, rotation: rotation)) } else { - throw PresentationThemeDecodingError.generic + var slug: String? + var color: UInt32? + var bottomColor: UInt32? + var intensity: Int32? + var rotation: Int32? + + if !components.isEmpty { + slug = components[0] + } + if components.count > 1 { + for i in 1 ..< components.count { + let component = components[i] + if optionKeys.contains(component) { + continue + } + if [6, 8].contains(component.count), let value = UIColor(hexString: component) { + if color == nil { + color = value.argb + } else if bottomColor == nil { + bottomColor = value.argb + } + } else if component.count <= 3, let value = Int32(component) { + if intensity == nil { + if value >= 0 && value <= 100 { + intensity = value + } else { + intensity = 50 + } + } else if rotation == nil { + if value >= 0 && value < 360 { + rotation = value + } + } + } + } + } + if let slug = slug { + self = .file(id: 0, accessHash: 0, isCreator: false, isDefault: false, isPattern: color != nil, isDark: false, slug: slug, file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], immediateThumbnailData: nil, mimeType: "", size: nil, attributes: []), settings: WallpaperSettings(blur: blur, motion: motion, color: color, bottomColor: bottomColor, intensity: intensity, rotation: rotation)) + } else { + throw PresentationThemeDecodingError.generic + } } } } @@ -84,18 +118,38 @@ extension TelegramWallpaper: Codable { switch self { case .builtin: try container.encode("builtin") - case let .color(value): - try container.encode(String(format: "%06x", value)) + case let .color(color): + try container.encode(String(format: "%06x", color)) + case let .gradient(topColor, bottomColor, settings): + var components: [String] = [] + components.append(String(format: "%06x", topColor)) + components.append(String(format: "%06x", bottomColor)) + if let rotation = settings.rotation { + components.append("\(rotation)") + } + if settings.motion { + components.append("motion") + } + if settings.blur { + components.append("blur") + } + try container.encode(components.joined(separator: " ")) case let .file(file): var components: [String] = [] components.append(file.slug) - if file.isPattern { + if self.isPattern { if let color = file.settings.color { components.append(String(format: "%06x", color)) } if let intensity = file.settings.intensity { components.append("\(intensity)") } + if let bottomColor = file.settings.bottomColor { + components.append(String(format: "%06x", bottomColor)) + } + if let rotation = file.settings.rotation, rotation != 0 { + components.append("\(rotation)") + } } if file.settings.motion { components.append("motion") @@ -708,6 +762,7 @@ extension PresentationThemeList: Codable { case check case controlSecondary case freeInputField + case freePlainInputField case mediaPlaceholder case scrollIndicator case pageIndicatorInactive @@ -717,35 +772,46 @@ extension PresentationThemeList: Codable { public convenience init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) - self.init(blocksBackgroundColor: try decodeColor(values, .blocksBg), - plainBackgroundColor: try decodeColor(values, .plainBg), - itemPrimaryTextColor: try decodeColor(values, .primaryText), - itemSecondaryTextColor: try decodeColor(values, .secondaryText), - itemDisabledTextColor: try decodeColor(values, .disabledText), - itemAccentColor: try decodeColor(values, .accent), - itemHighlightedColor: try decodeColor(values, .highlighted), - itemDestructiveColor: try decodeColor(values, .destructive), - itemPlaceholderTextColor: try decodeColor(values, .placeholderText), - itemBlocksBackgroundColor: try decodeColor(values, .itemBlocksBg), - itemHighlightedBackgroundColor: try decodeColor(values, .itemHighlightedBg), - itemBlocksSeparatorColor: try decodeColor(values, .blocksSeparator), - itemPlainSeparatorColor: try decodeColor(values, .plainSeparator), - disclosureArrowColor: try decodeColor(values, .disclosureArrow), - sectionHeaderTextColor: try decodeColor(values, .sectionHeaderText), - freeTextColor: try decodeColor(values, .freeText), - freeTextErrorColor: try decodeColor(values, .freeTextError), - freeTextSuccessColor: try decodeColor(values, .freeTextSuccess), - freeMonoIconColor: try decodeColor(values, .freeMonoIcon), - itemSwitchColors: try values.decode(PresentationThemeSwitch.self, forKey: .switch), - itemDisclosureActions: try values.decode(PresentationThemeItemDisclosureActions.self, forKey: .disclosureActions), - itemCheckColors: try values.decode(PresentationThemeFillStrokeForeground.self, forKey: .check), - controlSecondaryColor: try decodeColor(values, .controlSecondary), - freeInputField: try values.decode(PresentationInputFieldTheme.self, forKey: .freeInputField), - mediaPlaceholderColor: try decodeColor(values, .mediaPlaceholder), - scrollIndicatorColor: try decodeColor(values, .scrollIndicator), - pageIndicatorInactiveColor: try decodeColor(values, .pageIndicatorInactive), - inputClearButtonColor: try decodeColor(values, .inputClearButton), - itemBarChart: try values.decode(PresentationThemeItemBarChart.self, forKey: .itemBarChart)) + + let freePlainInputField: PresentationInputFieldTheme + if let value = try? values.decode(PresentationInputFieldTheme.self, forKey: .freePlainInputField) { + freePlainInputField = value + } else { + freePlainInputField = try values.decode(PresentationInputFieldTheme.self, forKey: .freeInputField) + } + + self.init( + blocksBackgroundColor: try decodeColor(values, .blocksBg), + plainBackgroundColor: try decodeColor(values, .plainBg), + itemPrimaryTextColor: try decodeColor(values, .primaryText), + itemSecondaryTextColor: try decodeColor(values, .secondaryText), + itemDisabledTextColor: try decodeColor(values, .disabledText), + itemAccentColor: try decodeColor(values, .accent), + itemHighlightedColor: try decodeColor(values, .highlighted), + itemDestructiveColor: try decodeColor(values, .destructive), + itemPlaceholderTextColor: try decodeColor(values, .placeholderText), + itemBlocksBackgroundColor: try decodeColor(values, .itemBlocksBg), + itemHighlightedBackgroundColor: try decodeColor(values, .itemHighlightedBg), + itemBlocksSeparatorColor: try decodeColor(values, .blocksSeparator), + itemPlainSeparatorColor: try decodeColor(values, .plainSeparator), + disclosureArrowColor: try decodeColor(values, .disclosureArrow), + sectionHeaderTextColor: try decodeColor(values, .sectionHeaderText), + freeTextColor: try decodeColor(values, .freeText), + freeTextErrorColor: try decodeColor(values, .freeTextError), + freeTextSuccessColor: try decodeColor(values, .freeTextSuccess), + freeMonoIconColor: try decodeColor(values, .freeMonoIcon), + itemSwitchColors: try values.decode(PresentationThemeSwitch.self, forKey: .switch), + itemDisclosureActions: try values.decode(PresentationThemeItemDisclosureActions.self, forKey: .disclosureActions), + itemCheckColors: try values.decode(PresentationThemeFillStrokeForeground.self, forKey: .check), + controlSecondaryColor: try decodeColor(values, .controlSecondary), + freeInputField: try values.decode(PresentationInputFieldTheme.self, forKey: .freeInputField), + freePlainInputField: freePlainInputField, + mediaPlaceholderColor: try decodeColor(values, .mediaPlaceholder), + scrollIndicatorColor: try decodeColor(values, .scrollIndicator), + pageIndicatorInactiveColor: try decodeColor(values, .pageIndicatorInactive), + inputClearButtonColor: try decodeColor(values, .inputClearButton), + itemBarChart: try values.decode(PresentationThemeItemBarChart.self, forKey: .itemBarChart) + ) } public func encode(to encoder: Encoder) throws { @@ -913,23 +979,55 @@ extension PresentationThemeChatList: Codable { } } +extension PresentationThemeBubbleShadow: Codable { + enum CodingKeys: String, CodingKey { + case color + case radius + case verticalOffset + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + self.init( + color: try decodeColor(values, .color), + radius: try CGFloat(Double(truncating: values.decode(Decimal.self, forKey: .radius) as NSNumber)), + verticalOffset: try CGFloat(Double(truncating: values.decode(Decimal.self, forKey: .verticalOffset) as NSNumber)) + ) + } + + public func encode(to encoder: Encoder) throws { + var values = encoder.container(keyedBy: CodingKeys.self) + try encodeColor(&values, self.color, .color) + try values.encode(Decimal(Double(self.radius)), forKey: .radius) + try values.encode(Decimal(Double(self.verticalOffset)), forKey: .verticalOffset) + } +} + extension PresentationThemeBubbleColorComponents: Codable { enum CodingKeys: String, CodingKey { case bg + case gradientBg case highlightedBg case stroke + case shadow } public convenience init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) - self.init(fill: try decodeColor(values, .bg), - highlightedFill: try decodeColor(values, .highlightedBg), - stroke: try decodeColor(values, .stroke)) + let codingPath = decoder.codingPath.map { $0.stringValue }.joined(separator: ".") + self.init( + fill: try decodeColor(values, .bg), + gradientFill: try decodeColor(values, .gradientBg, decoder: decoder, fallbackKey: codingPath + ".bg"), + highlightedFill: try decodeColor(values, .highlightedBg), + stroke: try decodeColor(values, .stroke), + shadow: try? values.decode(PresentationThemeBubbleShadow.self, forKey: .shadow) + ) } public func encode(to encoder: Encoder) throws { var values = encoder.container(keyedBy: CodingKeys.self) try encodeColor(&values, self.fill, .bg) + try encodeColor(&values, self.gradientFill, .gradientBg) try encodeColor(&values, self.highlightedFill, .highlightedBg) try encodeColor(&values, self.stroke, .stroke) } @@ -980,15 +1078,24 @@ extension PresentationThemeChatBubblePolls: Codable { case highlight case separator case bar + case barIconForeground + case barPositive + case barNegative } public convenience init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) - self.init(radioButton: try decodeColor(values, .radioButton), - radioProgress: try decodeColor(values, .radioProgress), - highlight: try decodeColor(values, .highlight), - separator: try decodeColor(values, .separator), - bar: try decodeColor(values, .bar)) + let bar = try decodeColor(values, .bar) + self.init( + radioButton: try decodeColor(values, .radioButton), + radioProgress: try decodeColor(values, .radioProgress), + highlight: try decodeColor(values, .highlight), + separator: try decodeColor(values, .separator), + bar: bar, + barIconForeground: (try? decodeColor(values, .barIconForeground)) ?? .clear, + barPositive: (try? decodeColor(values, .barPositive)) ?? bar, + barNegative: (try? decodeColor(values, .barNegative)) ?? bar + ) } public func encode(to encoder: Encoder) throws { @@ -998,6 +1105,9 @@ extension PresentationThemeChatBubblePolls: Codable { try encodeColor(&values, self.highlight, .highlight) try encodeColor(&values, self.separator, .separator) try encodeColor(&values, self.bar, .bar) + try encodeColor(&values, self.barIconForeground, .barIconForeground) + try encodeColor(&values, self.barPositive, .barPositive) + try encodeColor(&values, self.barNegative, .barNegative) } } @@ -1026,11 +1136,13 @@ extension PresentationThemePartedColors: Codable { case actionButtonsText case textSelection case textSelectionKnob + case accentControlDisabled } public convenience init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) let codingPath = decoder.codingPath.map { $0.stringValue }.joined(separator: ".") + let accentControlColor = try decodeColor(values, .accentControl) self.init( bubble: try values.decode(PresentationThemeBubbleColor.self, forKey: .bubble), primaryTextColor: try decodeColor(values, .primaryText), @@ -1040,7 +1152,8 @@ extension PresentationThemePartedColors: Codable { scamColor: try decodeColor(values, .scam), textHighlightColor: try decodeColor(values, .textHighlight), accentTextColor: try decodeColor(values, .accentText), - accentControlColor: try decodeColor(values, .accentControl), + accentControlColor: accentControlColor, + accentControlDisabledColor: (try? decodeColor(values, .accentControlDisabled)) ?? accentControlColor.withAlphaComponent(0.5), mediaActiveControlColor: try decodeColor(values, .mediaActiveControl), mediaInactiveControlColor: try decodeColor(values, .mediaInactiveControl), mediaControlInnerBackgroundColor: try decodeColor(values, .mediaControlInnerBg, decoder: decoder, fallbackKey: codingPath + ".bubble.withWp.bg"), @@ -1247,6 +1360,7 @@ extension PresentationThemeChatInputPanelMediaRecordingControl: Codable { extension PresentationThemeChatInputPanel: Codable { enum CodingKeys: String, CodingKey { case panelBg + case panelBgNoWallpaper case panelSeparator case panelControlAccent case panelControl @@ -1267,7 +1381,9 @@ extension PresentationThemeChatInputPanel: Codable { public convenience init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) + let codingPath = decoder.codingPath.map { $0.stringValue }.joined(separator: ".") self.init(panelBackgroundColor: try decodeColor(values, .panelBg), + panelBackgroundColorNoWallpaper: try decodeColor(values, .panelBg, decoder: decoder, fallbackKey: codingPath + ".panelBgNoWallpaper"), panelSeparatorColor: try decodeColor(values, .panelSeparator), panelControlAccentColor: try decodeColor(values, .panelControlAccent), panelControlColor: try decodeColor(values, .panelControl), @@ -1600,7 +1716,7 @@ extension PresentationBuiltinThemeReference: Codable { self = .day case "classic": self = .dayClassic - case "nightTinted": + case "nighttinted": self = .nightAccent case "night": self = .night @@ -1620,7 +1736,7 @@ extension PresentationBuiltinThemeReference: Codable { case .dayClassic: try container.encode("classic") case .nightAccent: - try container.encode("nightTinted") + try container.encode("nighttinted") case .night: try container.encode("night") } @@ -1652,15 +1768,19 @@ extension PresentationTheme: Codable { referenceTheme = .dayClassic } + let index: Int64 if let decoder = decoder as? PresentationThemeDecoding { - let serviceBackgroundColor = decoder.serviceBackgroundColor ?? .black - decoder.referenceTheme = makeDefaultPresentationTheme(reference: referenceTheme, accentColor: nil, serviceBackgroundColor: serviceBackgroundColor, baseColor: nil) + let serviceBackgroundColor = decoder.serviceBackgroundColor ?? defaultServiceBackgroundColor + decoder.referenceTheme = makeDefaultPresentationTheme(reference: referenceTheme, serviceBackgroundColor: serviceBackgroundColor) + index = decoder.reference?.index ?? arc4random64() + } else { + index = arc4random64() } self.init(name: (try? values.decode(PresentationThemeName.self, forKey: .name)) ?? .custom("Untitled"), + index: index, referenceTheme: referenceTheme, overallDarkAppearance: (try? values.decode(Bool.self, forKey: .dark)) ?? false, - baseColor: nil, intro: try values.decode(PresentationThemeIntro.self, forKey: .intro), passcode: try values.decode(PresentationThemePasscode.self, forKey: .passcode), rootController: try values.decode(PresentationThemeRootController.self, forKey: .root), diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeCoder.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeCoder.swift index cce47b6c95..6811a56333 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeCoder.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeCoder.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import TelegramCore import SyncCore +import TelegramUIPreferences public func encodePresentationTheme(_ theme: PresentationTheme) -> String? { let encoding = PresentationThemeEncoding() @@ -341,7 +342,7 @@ private class PresentationThemeDecodingLevel { } } -public func makePresentationTheme(data: Data, resolvedWallpaper: TelegramWallpaper? = nil) -> PresentationTheme? { +public func makePresentationTheme(data: Data, themeReference: PresentationThemeReference? = nil, resolvedWallpaper: TelegramWallpaper? = nil) -> PresentationTheme? { guard let string = String(data: data, encoding: .utf8) else { return nil } @@ -402,6 +403,7 @@ public func makePresentationTheme(data: Data, resolvedWallpaper: TelegramWallpap } let decoder = PresentationThemeDecoding(referencing: topLevel.data) + decoder.reference = themeReference decoder.resolvedWallpaper = resolvedWallpaper if let value = try? decoder.unbox(topLevel.data, as: PresentationTheme.self) { return value @@ -418,6 +420,7 @@ class PresentationThemeDecoding: Decoder { return [:] } + var reference: PresentationThemeReference? var referenceTheme: PresentationTheme? var serviceBackgroundColor: UIColor? var resolvedWallpaper: TelegramWallpaper? @@ -925,7 +928,11 @@ extension PresentationThemeDecoding { fileprivate func unbox_(_ value: Any, as type: Decodable.Type) throws -> Any? { if type == Decimal.self || type == NSDecimalNumber.self { - return try self.unbox(value, as: Decimal.self) + if let value = value as? String { + return Decimal(string: value) + } else { + return try self.unbox(value, as: Decimal.self) + } } else if let stringKeyedDictType = type as? _YAMLStringDictionaryDecodableMarker.Type { return try self.unbox(value, as: stringKeyedDictType) } else { diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift index 7f9e7971d0..2aa29887cd 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift @@ -7,9 +7,12 @@ import SyncCore import TelegramUIPreferences import AppBundle -private func generateCheckImage(partial: Bool, color: UIColor) -> UIImage? { - return generateImage(CGSize(width: 11.0, height: 9.0), rotatedContext: { size, context in +func generateCheckImage(partial: Bool, color: UIColor, width: CGFloat) -> UIImage? { + return generateImage(CGSize(width: width, height: floor(width * 9.0 / 11.0)), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) + + context.scaleBy(x: width / 11.0, y: width / 11.0) + context.translateBy(x: 1.0, y: 1.0) context.setStrokeColor(color.cgColor) context.setLineWidth(0.99) @@ -75,40 +78,64 @@ private func chatBubbleActionButtonImage(fillColor: UIColor, strokeColor: UIColo public final class PrincipalThemeEssentialGraphics { public let chatMessageBackgroundIncomingMaskImage: UIImage public let chatMessageBackgroundIncomingImage: UIImage + public let chatMessageBackgroundIncomingOutlineImage: UIImage + public let chatMessageBackgroundIncomingShadowImage: UIImage public let chatMessageBackgroundIncomingHighlightedImage: UIImage public let chatMessageBackgroundIncomingMergedTopMaskImage: UIImage public let chatMessageBackgroundIncomingMergedTopImage: UIImage + public let chatMessageBackgroundIncomingMergedTopOutlineImage: UIImage + public let chatMessageBackgroundIncomingMergedTopShadowImage: UIImage public let chatMessageBackgroundIncomingMergedTopHighlightedImage: UIImage public let chatMessageBackgroundIncomingMergedTopSideMaskImage: UIImage public let chatMessageBackgroundIncomingMergedTopSideImage: UIImage + public let chatMessageBackgroundIncomingMergedTopSideOutlineImage: UIImage + public let chatMessageBackgroundIncomingMergedTopSideShadowImage: UIImage public let chatMessageBackgroundIncomingMergedTopSideHighlightedImage: UIImage public let chatMessageBackgroundIncomingMergedBottomMaskImage: UIImage public let chatMessageBackgroundIncomingMergedBottomImage: UIImage + public let chatMessageBackgroundIncomingMergedBottomOutlineImage: UIImage + public let chatMessageBackgroundIncomingMergedBottomShadowImage: UIImage public let chatMessageBackgroundIncomingMergedBottomHighlightedImage: UIImage public let chatMessageBackgroundIncomingMergedBothMaskImage: UIImage public let chatMessageBackgroundIncomingMergedBothImage: UIImage + public let chatMessageBackgroundIncomingMergedBothOutlineImage: UIImage + public let chatMessageBackgroundIncomingMergedBothShadowImage: UIImage public let chatMessageBackgroundIncomingMergedBothHighlightedImage: UIImage public let chatMessageBackgroundIncomingMergedSideMaskImage: UIImage public let chatMessageBackgroundIncomingMergedSideImage: UIImage + public let chatMessageBackgroundIncomingMergedSideOutlineImage: UIImage + public let chatMessageBackgroundIncomingMergedSideShadowImage: UIImage public let chatMessageBackgroundIncomingMergedSideHighlightedImage: UIImage public let chatMessageBackgroundOutgoingMaskImage: UIImage public let chatMessageBackgroundOutgoingImage: UIImage + public let chatMessageBackgroundOutgoingOutlineImage: UIImage + public let chatMessageBackgroundOutgoingShadowImage: UIImage public let chatMessageBackgroundOutgoingHighlightedImage: UIImage public let chatMessageBackgroundOutgoingMergedTopMaskImage: UIImage public let chatMessageBackgroundOutgoingMergedTopImage: UIImage + public let chatMessageBackgroundOutgoingMergedTopOutlineImage: UIImage + public let chatMessageBackgroundOutgoingMergedTopShadowImage: UIImage public let chatMessageBackgroundOutgoingMergedTopHighlightedImage: UIImage public let chatMessageBackgroundOutgoingMergedTopSideMaskImage: UIImage public let chatMessageBackgroundOutgoingMergedTopSideImage: UIImage + public let chatMessageBackgroundOutgoingMergedTopSideOutlineImage: UIImage + public let chatMessageBackgroundOutgoingMergedTopSideShadowImage: UIImage public let chatMessageBackgroundOutgoingMergedTopSideHighlightedImage: UIImage public let chatMessageBackgroundOutgoingMergedBottomMaskImage: UIImage public let chatMessageBackgroundOutgoingMergedBottomImage: UIImage + public let chatMessageBackgroundOutgoingMergedBottomOutlineImage: UIImage + public let chatMessageBackgroundOutgoingMergedBottomShadowImage: UIImage public let chatMessageBackgroundOutgoingMergedBottomHighlightedImage: UIImage public let chatMessageBackgroundOutgoingMergedBothMaskImage: UIImage public let chatMessageBackgroundOutgoingMergedBothImage: UIImage + public let chatMessageBackgroundOutgoingMergedBothOutlineImage: UIImage + public let chatMessageBackgroundOutgoingMergedBothShadowImage: UIImage public let chatMessageBackgroundOutgoingMergedBothHighlightedImage: UIImage public let chatMessageBackgroundOutgoingMergedSideMaskImage: UIImage public let chatMessageBackgroundOutgoingMergedSideImage: UIImage + public let chatMessageBackgroundOutgoingMergedSideOutlineImage: UIImage + public let chatMessageBackgroundOutgoingMergedSideShadowImage: UIImage public let chatMessageBackgroundOutgoingMergedSideHighlightedImage: UIImage public let checkBubbleFullImage: UIImage @@ -145,117 +172,127 @@ public final class PrincipalThemeEssentialGraphics { public let incomingBubbleGradientImage: UIImage? public let outgoingBubbleGradientImage: UIImage? - init(mediaBox: MediaBox, presentationTheme: PresentationTheme, wallpaper initialWallpaper: TelegramWallpaper, preview: Bool = false, knockoutMode: Bool, gradientBubbles: Bool) { + init(mediaBox: MediaBox, presentationTheme: PresentationTheme, wallpaper initialWallpaper: TelegramWallpaper, preview: Bool = false, knockoutMode: Bool, bubbleCorners: PresentationChatBubbleCorners) { let theme = presentationTheme.chat var wallpaper = initialWallpaper + let incoming: PresentationThemeBubbleColorComponents = wallpaper.isEmpty ? theme.message.incoming.bubble.withoutWallpaper : theme.message.incoming.bubble.withWallpaper + let outgoing: PresentationThemeBubbleColorComponents = wallpaper.isEmpty ? theme.message.outgoing.bubble.withoutWallpaper : theme.message.outgoing.bubble.withWallpaper + if knockoutMode { let wallpaperImage = chatControllerBackgroundImage(theme: presentationTheme, wallpaper: wallpaper, mediaBox: mediaBox, knockoutMode: false) self.incomingBubbleGradientImage = wallpaperImage self.outgoingBubbleGradientImage = wallpaperImage wallpaper = presentationTheme.chat.defaultWallpaper - } else if case .color = wallpaper { - switch presentationTheme.name { - case let .builtin(name): - switch name { - case .day, .night, .nightAccent: - var incomingGradientColors: (UIColor, UIColor)? - if let incomingGradientColors = incomingGradientColors { - self.incomingBubbleGradientImage = generateImage(CGSize(width: 1.0, height: 512.0), opaque: true, scale: 1.0, rotatedContext: { size, context in - var locations: [CGFloat] = [0.0, 1.0] - let colors = [incomingGradientColors.0.cgColor, incomingGradientColors.1.cgColor] as NSArray - - let colorSpace = deviceColorSpace - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: &locations)! - - context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) - }) - } else { - self.incomingBubbleGradientImage = nil - } + } else { + var incomingGradientColors: (UIColor, UIColor)? + if incoming.fill.rgb != incoming.gradientFill.rgb { + incomingGradientColors = (incoming.fill, incoming.gradientFill) + } + if let incomingGradientColors = incomingGradientColors { + self.incomingBubbleGradientImage = generateImage(CGSize(width: 1.0, height: 512.0), opaque: true, scale: 1.0, rotatedContext: { size, context in + var locations: [CGFloat] = [0.0, 1.0] + let colors = [incomingGradientColors.0.cgColor, incomingGradientColors.1.cgColor] as NSArray - var outgoingGradientColors: (UIColor, UIColor)? - if gradientBubbles, let baseColor = presentationTheme.baseColor { - if presentationTheme.baseColor == .custom { - - } else { - let colors = baseColor.outgoingGradientColors - if !colors.0.isEqual(colors.1) { - outgoingGradientColors = colors - } - } - } - if let outgoingGradientColors = outgoingGradientColors { - self.outgoingBubbleGradientImage = generateImage(CGSize(width: 1.0, height: 512.0), opaque: true, scale: 1.0, rotatedContext: { size, context in - var locations: [CGFloat] = [0.0, 1.0] - let colors = [outgoingGradientColors.0.cgColor, outgoingGradientColors.1.cgColor] as NSArray - - let colorSpace = deviceColorSpace - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: &locations)! - - context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) - }) - } else { - self.outgoingBubbleGradientImage = nil - } - case .dayClassic: - self.incomingBubbleGradientImage = nil - self.outgoingBubbleGradientImage = nil - } - case .custom: + let colorSpace = deviceColorSpace + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + } else { self.incomingBubbleGradientImage = nil + } + + var outgoingGradientColors: (UIColor, UIColor)? + if outgoing.fill.rgb != outgoing.gradientFill.rgb { + outgoingGradientColors = (outgoing.fill, outgoing.gradientFill) + } + if let outgoingGradientColors = outgoingGradientColors { + self.outgoingBubbleGradientImage = generateImage(CGSize(width: 1.0, height: 512.0), opaque: true, scale: 1.0, rotatedContext: { size, context in + var locations: [CGFloat] = [0.0, 1.0] + let colors = [outgoingGradientColors.0.cgColor, outgoingGradientColors.1.cgColor] as NSArray + + let colorSpace = deviceColorSpace + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + } else { self.outgoingBubbleGradientImage = nil } - } else { - self.incomingBubbleGradientImage = nil - self.outgoingBubbleGradientImage = nil } - let incoming: PresentationThemeBubbleColorComponents = wallpaper.isEmpty ? theme.message.incoming.bubble.withoutWallpaper : theme.message.incoming.bubble.withWallpaper - let outgoing: PresentationThemeBubbleColorComponents = wallpaper.isEmpty ? theme.message.outgoing.bubble.withoutWallpaper : theme.message.outgoing.bubble.withWallpaper - let incomingKnockout = self.incomingBubbleGradientImage != nil let outgoingKnockout = self.outgoingBubbleGradientImage != nil + let serviceColor = serviceMessageColorComponents(chatTheme: theme, wallpaper: wallpaper) + + let maxCornerRadius = bubbleCorners.mainRadius + let minCornerRadius = (bubbleCorners.mergeBubbleCorners && maxCornerRadius >= 10.0) ? bubbleCorners.auxiliaryRadius : bubbleCorners.mainRadius + let emptyImage = UIImage() if preview { - self.chatMessageBackgroundIncomingMaskImage = messageBubbleImage(incoming: true, fillColor: UIColor.black, strokeColor: UIColor.clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) - self.chatMessageBackgroundIncomingImage = messageBubbleImage(incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) - self.chatMessageBackgroundOutgoingMaskImage = messageBubbleImage(incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) - self.chatMessageBackgroundOutgoingImage = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) - self.checkBubbleFullImage = generateCheckImage(partial: false, color: theme.message.outgoingCheckColor)! - self.checkBubblePartialImage = generateCheckImage(partial: true, color: theme.message.outgoingCheckColor)! + self.chatMessageBackgroundIncomingMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: UIColor.black, strokeColor: UIColor.clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) + self.chatMessageBackgroundIncomingImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) + self.chatMessageBackgroundIncomingOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundIncomingShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyShadow: true) + self.chatMessageBackgroundOutgoingMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) + self.chatMessageBackgroundOutgoingImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) + self.chatMessageBackgroundOutgoingOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundOutgoingShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyShadow: true) + self.checkBubbleFullImage = generateCheckImage(partial: false, color: theme.message.outgoingCheckColor, width: 11.0)! + self.checkBubblePartialImage = generateCheckImage(partial: true, color: theme.message.outgoingCheckColor, width: 11.0)! self.chatMessageBackgroundIncomingHighlightedImage = emptyImage self.chatMessageBackgroundIncomingMergedTopMaskImage = emptyImage - self.chatMessageBackgroundIncomingMergedTopImage = messageBubbleImage(incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) + self.chatMessageBackgroundIncomingMergedTopImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) + self.chatMessageBackgroundIncomingMergedTopOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundIncomingMergedTopShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyShadow: true) self.chatMessageBackgroundIncomingMergedTopHighlightedImage = emptyImage self.chatMessageBackgroundIncomingMergedTopSideMaskImage = emptyImage self.chatMessageBackgroundIncomingMergedTopSideImage = emptyImage + self.chatMessageBackgroundIncomingMergedTopSideOutlineImage = emptyImage + self.chatMessageBackgroundIncomingMergedTopSideShadowImage = emptyImage self.chatMessageBackgroundIncomingMergedTopSideHighlightedImage = emptyImage self.chatMessageBackgroundIncomingMergedBottomMaskImage = emptyImage - self.chatMessageBackgroundIncomingMergedBottomImage = messageBubbleImage(incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) + self.chatMessageBackgroundIncomingMergedBottomImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) + self.chatMessageBackgroundIncomingMergedBottomOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundIncomingMergedBottomShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyShadow: true) self.chatMessageBackgroundIncomingMergedBottomHighlightedImage = emptyImage self.chatMessageBackgroundIncomingMergedBothMaskImage = emptyImage self.chatMessageBackgroundIncomingMergedBothImage = emptyImage + self.chatMessageBackgroundIncomingMergedBothOutlineImage = emptyImage + self.chatMessageBackgroundIncomingMergedBothShadowImage = emptyImage self.chatMessageBackgroundIncomingMergedBothHighlightedImage = emptyImage self.chatMessageBackgroundIncomingMergedSideMaskImage = emptyImage self.chatMessageBackgroundIncomingMergedSideImage = emptyImage + self.chatMessageBackgroundIncomingMergedSideOutlineImage = emptyImage + self.chatMessageBackgroundIncomingMergedSideShadowImage = emptyImage self.chatMessageBackgroundIncomingMergedSideHighlightedImage = emptyImage self.chatMessageBackgroundOutgoingHighlightedImage = emptyImage - self.chatMessageBackgroundOutgoingMergedTopMaskImage = messageBubbleImage(incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .top(side: false), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) - self.chatMessageBackgroundOutgoingMergedTopImage = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) + self.chatMessageBackgroundOutgoingMergedTopMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .top(side: false), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedTopImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedTopOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundOutgoingMergedTopShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyShadow: true) self.chatMessageBackgroundOutgoingMergedTopHighlightedImage = emptyImage self.chatMessageBackgroundOutgoingMergedTopSideMaskImage = emptyImage self.chatMessageBackgroundOutgoingMergedTopSideImage = emptyImage + self.chatMessageBackgroundOutgoingMergedTopSideOutlineImage = emptyImage + self.chatMessageBackgroundOutgoingMergedTopSideShadowImage = emptyImage self.chatMessageBackgroundOutgoingMergedTopSideHighlightedImage = emptyImage - self.chatMessageBackgroundOutgoingMergedBottomMaskImage = messageBubbleImage(incoming: false, fillColor: .black, strokeColor: .white, neighbors: .bottom, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) - self.chatMessageBackgroundOutgoingMergedBottomImage = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) + self.chatMessageBackgroundOutgoingMergedBottomMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .bottom, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedBottomImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedBottomOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundOutgoingMergedBottomShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyShadow: true) self.chatMessageBackgroundOutgoingMergedBottomHighlightedImage = emptyImage - self.chatMessageBackgroundOutgoingMergedBothMaskImage = messageBubbleImage(incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .both, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) - self.chatMessageBackgroundOutgoingMergedBothImage = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) + self.chatMessageBackgroundOutgoingMergedBothMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .both, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedBothImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedBothOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundOutgoingMergedBothShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyShadow: true) self.chatMessageBackgroundOutgoingMergedBothHighlightedImage = emptyImage self.chatMessageBackgroundOutgoingMergedSideMaskImage = emptyImage self.chatMessageBackgroundOutgoingMergedSideImage = emptyImage + self.chatMessageBackgroundOutgoingMergedSideOutlineImage = emptyImage + self.chatMessageBackgroundOutgoingMergedSideShadowImage = emptyImage self.chatMessageBackgroundOutgoingMergedSideHighlightedImage = emptyImage self.checkMediaFullImage = emptyImage self.checkMediaPartialImage = emptyImage @@ -275,59 +312,80 @@ public final class PrincipalThemeEssentialGraphics { self.outgoingDateAndStatusImpressionIcon = emptyImage self.mediaImpressionIcon = emptyImage self.freeImpressionIcon = emptyImage - self.dateStaticBackground = emptyImage - self.dateFloatingBackground = emptyImage self.radialIndicatorFileIconIncoming = emptyImage self.radialIndicatorFileIconOutgoing = emptyImage } else { - self.chatMessageBackgroundIncomingMaskImage = messageBubbleImage(incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) - self.chatMessageBackgroundIncomingImage = messageBubbleImage(incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) - self.chatMessageBackgroundIncomingHighlightedImage = messageBubbleImage(incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) - self.chatMessageBackgroundIncomingMergedTopMaskImage = messageBubbleImage(incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .top(side: false), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) - self.chatMessageBackgroundIncomingMergedTopImage = messageBubbleImage(incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) - self.chatMessageBackgroundIncomingMergedTopHighlightedImage = messageBubbleImage(incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) - self.chatMessageBackgroundIncomingMergedTopSideMaskImage = messageBubbleImage(incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .top(side: true), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) - self.chatMessageBackgroundIncomingMergedTopSideImage = messageBubbleImage(incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) - self.chatMessageBackgroundIncomingMergedTopSideHighlightedImage = messageBubbleImage(incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) - self.chatMessageBackgroundIncomingMergedBottomMaskImage = messageBubbleImage(incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .bottom, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) - self.chatMessageBackgroundIncomingMergedBottomImage = messageBubbleImage(incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) - self.chatMessageBackgroundIncomingMergedBottomHighlightedImage = messageBubbleImage(incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) - self.chatMessageBackgroundIncomingMergedBothMaskImage = messageBubbleImage(incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .both, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) - self.chatMessageBackgroundIncomingMergedBothImage = messageBubbleImage(incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) - self.chatMessageBackgroundIncomingMergedBothHighlightedImage = messageBubbleImage(incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout) + self.chatMessageBackgroundIncomingMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) + self.chatMessageBackgroundIncomingImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) + self.chatMessageBackgroundIncomingOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundIncomingShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyShadow: true) + self.chatMessageBackgroundIncomingHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) + self.chatMessageBackgroundIncomingMergedTopMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .top(side: false), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) + self.chatMessageBackgroundIncomingMergedTopImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) + self.chatMessageBackgroundIncomingMergedTopOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundIncomingMergedTopShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyShadow: true) + self.chatMessageBackgroundIncomingMergedTopHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) + self.chatMessageBackgroundIncomingMergedTopSideMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .top(side: true), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) + self.chatMessageBackgroundIncomingMergedTopSideImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) + self.chatMessageBackgroundIncomingMergedTopSideOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundIncomingMergedTopSideShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyShadow: true) + self.chatMessageBackgroundIncomingMergedTopSideHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) + self.chatMessageBackgroundIncomingMergedBottomMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .bottom, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) + self.chatMessageBackgroundIncomingMergedBottomImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) + self.chatMessageBackgroundIncomingMergedBottomOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundIncomingMergedBottomShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyShadow: true) + self.chatMessageBackgroundIncomingMergedBottomHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) + self.chatMessageBackgroundIncomingMergedBothMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .both, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) + self.chatMessageBackgroundIncomingMergedBothImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) + self.chatMessageBackgroundIncomingMergedBothOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundIncomingMergedBothShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true, onlyShadow: true) + self.chatMessageBackgroundIncomingMergedBothHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) - self.chatMessageBackgroundOutgoingMaskImage = messageBubbleImage(incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) - self.chatMessageBackgroundOutgoingImage = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) - self.chatMessageBackgroundOutgoingHighlightedImage = messageBubbleImage(incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) - self.chatMessageBackgroundOutgoingMergedTopMaskImage = messageBubbleImage(incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .top(side: false), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) - self.chatMessageBackgroundOutgoingMergedTopImage = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) - self.chatMessageBackgroundOutgoingMergedTopHighlightedImage = messageBubbleImage(incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) - self.chatMessageBackgroundOutgoingMergedTopSideMaskImage = messageBubbleImage(incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .top(side: true), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) - self.chatMessageBackgroundOutgoingMergedTopSideImage = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) - self.chatMessageBackgroundOutgoingMergedTopSideHighlightedImage = messageBubbleImage(incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) - self.chatMessageBackgroundOutgoingMergedBottomMaskImage = messageBubbleImage(incoming: false, fillColor: .black, strokeColor: .white, neighbors: .bottom, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) - self.chatMessageBackgroundOutgoingMergedBottomImage = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) - self.chatMessageBackgroundOutgoingMergedBottomHighlightedImage = messageBubbleImage(incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) - self.chatMessageBackgroundOutgoingMergedBothMaskImage = messageBubbleImage(incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .both, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) - self.chatMessageBackgroundOutgoingMergedBothImage = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) - self.chatMessageBackgroundOutgoingMergedBothHighlightedImage = messageBubbleImage(incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) + self.chatMessageBackgroundOutgoingMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) + self.chatMessageBackgroundOutgoingImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) + self.chatMessageBackgroundOutgoingOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundOutgoingShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyShadow: true) + self.chatMessageBackgroundOutgoingHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .none, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedTopMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .top(side: false), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedTopImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedTopOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundOutgoingMergedTopShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyShadow: true) + self.chatMessageBackgroundOutgoingMergedTopHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .top(side: false), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedTopSideMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .top(side: true), theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedTopSideImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedTopSideOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundOutgoingMergedTopSideShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyShadow: true) + self.chatMessageBackgroundOutgoingMergedTopSideHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .top(side: true), theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedBottomMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .bottom, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedBottomImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedBottomOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundOutgoingMergedBottomShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyShadow: true) + self.chatMessageBackgroundOutgoingMergedBottomHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .bottom, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedBothMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .both, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedBothImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedBothOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundOutgoingMergedBothShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyShadow: true) + self.chatMessageBackgroundOutgoingMergedBothHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .both, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) - self.chatMessageBackgroundIncomingMergedSideMaskImage = messageBubbleImage(incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .side, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) - self.chatMessageBackgroundIncomingMergedSideImage = messageBubbleImage(incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) - self.chatMessageBackgroundOutgoingMergedSideMaskImage = messageBubbleImage(incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .side, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true) - self.chatMessageBackgroundOutgoingMergedSideImage = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) - self.chatMessageBackgroundIncomingMergedSideHighlightedImage = messageBubbleImage(incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) - self.chatMessageBackgroundOutgoingMergedSideHighlightedImage = messageBubbleImage(incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout) + self.chatMessageBackgroundIncomingMergedSideMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .side, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) + self.chatMessageBackgroundIncomingMergedSideImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) + self.chatMessageBackgroundIncomingMergedSideOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundIncomingMergedSideShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyShadow: true) + self.chatMessageBackgroundOutgoingMergedSideMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .side, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedSideImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedSideOutlineImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyOutline: true) + self.chatMessageBackgroundOutgoingMergedSideShadowImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true, onlyShadow: true) + self.chatMessageBackgroundIncomingMergedSideHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) + self.chatMessageBackgroundOutgoingMergedSideHighlightedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: false, fillColor: outgoing.highlightedFill, strokeColor: outgoing.stroke, neighbors: .side, theme: theme, wallpaper: wallpaper, knockout: outgoingKnockout, extendedEdges: true) - self.checkBubbleFullImage = generateCheckImage(partial: false, color: theme.message.outgoingCheckColor)! - self.checkBubblePartialImage = generateCheckImage(partial: true, color: theme.message.outgoingCheckColor)! + self.checkBubbleFullImage = generateCheckImage(partial: false, color: theme.message.outgoingCheckColor, width: 11.0)! + self.checkBubblePartialImage = generateCheckImage(partial: true, color: theme.message.outgoingCheckColor, width: 11.0)! - self.checkMediaFullImage = generateCheckImage(partial: false, color: .white)! - self.checkMediaPartialImage = generateCheckImage(partial: true, color: .white)! + self.checkMediaFullImage = generateCheckImage(partial: false, color: .white, width: 11.0)! + self.checkMediaPartialImage = generateCheckImage(partial: true, color: .white, width: 11.0)! - let serviceColor = serviceMessageColorComponents(chatTheme: theme, wallpaper: wallpaper) - self.checkFreeFullImage = generateCheckImage(partial: false, color: serviceColor.primaryText)! - self.checkFreePartialImage = generateCheckImage(partial: true, color: serviceColor.primaryText)! + self.checkFreeFullImage = generateCheckImage(partial: false, color: serviceColor.primaryText, width: 11.0)! + self.checkFreePartialImage = generateCheckImage(partial: true, color: serviceColor.primaryText, width: 11.0)! self.clockBubbleIncomingFrameImage = generateClockFrameImage(color: theme.message.incoming.pendingActivityColor)! self.clockBubbleIncomingMinImage = generateClockMinImage(color: theme.message.incoming.pendingActivityColor)! @@ -349,22 +407,22 @@ public final class PrincipalThemeEssentialGraphics { self.mediaImpressionIcon = generateTintedImage(image: impressionCountImage, color: .white)! self.freeImpressionIcon = generateTintedImage(image: impressionCountImage, color: serviceColor.primaryText)! - let chatDateSize: CGFloat = 20.0 - self.dateStaticBackground = generateImage(CGSize(width: chatDateSize, height: chatDateSize), contextGenerator: { size, context -> Void in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(serviceColor.dateFillStatic.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - })!.stretchableImage(withLeftCapWidth: Int(chatDateSize) / 2, topCapHeight: Int(chatDateSize) / 2) - - self.dateFloatingBackground = generateImage(CGSize(width: chatDateSize, height: chatDateSize), contextGenerator: { size, context -> Void in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(serviceColor.dateFillFloating.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - })!.stretchableImage(withLeftCapWidth: Int(chatDateSize) / 2, topCapHeight: Int(chatDateSize) / 2) - - self.radialIndicatorFileIconIncoming = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: incoming.fill)! - self.radialIndicatorFileIconOutgoing = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: outgoing.fill)! + self.radialIndicatorFileIconIncoming = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)! + self.radialIndicatorFileIconOutgoing = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)! } + + let chatDateSize: CGFloat = 20.0 + self.dateStaticBackground = generateImage(CGSize(width: chatDateSize, height: chatDateSize), contextGenerator: { size, context -> Void in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(serviceColor.dateFillStatic.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + })!.stretchableImage(withLeftCapWidth: Int(chatDateSize) / 2, topCapHeight: Int(chatDateSize) / 2) + + self.dateFloatingBackground = generateImage(CGSize(width: chatDateSize, height: chatDateSize), contextGenerator: { size, context -> Void in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(serviceColor.dateFillFloating.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + })!.stretchableImage(withLeftCapWidth: Int(chatDateSize) / 2, topCapHeight: Int(chatDateSize) / 2) } } @@ -401,7 +459,7 @@ public final class PrincipalThemeAdditionalGraphics { public let chatEmptyItemLockIcon: UIImage public let emptyChatListCheckIcon: UIImage - init(_ theme: PresentationThemeChat, wallpaper: TelegramWallpaper) { + init(_ theme: PresentationThemeChat, wallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners) { let serviceColor = serviceMessageColorComponents(chatTheme: theme, wallpaper: wallpaper) self.chatServiceBubbleFillImage = generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context -> Void in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -422,14 +480,14 @@ public final class PrincipalThemeAdditionalGraphics { self.chatBubbleShareButtonImage = chatBubbleActionButtonImage(fillColor: bubbleVariableColor(variableColor: theme.message.shareButtonFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.shareButtonStrokeColor, wallpaper: wallpaper), foregroundColor: bubbleVariableColor(variableColor: theme.message.shareButtonForegroundColor, wallpaper: wallpaper), image: UIImage(bundleImageName: "Chat/Message/ShareIcon"))! self.chatBubbleNavigateButtonImage = chatBubbleActionButtonImage(fillColor: bubbleVariableColor(variableColor: theme.message.shareButtonFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.shareButtonStrokeColor, wallpaper: wallpaper), foregroundColor: bubbleVariableColor(variableColor: theme.message.shareButtonForegroundColor, wallpaper: wallpaper), image: UIImage(bundleImageName: "Chat/Message/NavigateToMessageIcon"), iconOffset: CGPoint(x: 0.0, y: 1.0))! - self.chatBubbleActionButtonIncomingMiddleImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsStrokeColor, wallpaper: wallpaper), position: .middle) - self.chatBubbleActionButtonIncomingBottomLeftImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsStrokeColor, wallpaper: wallpaper), position: .bottomLeft) - self.chatBubbleActionButtonIncomingBottomRightImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsStrokeColor, wallpaper: wallpaper), position: .bottomRight) - self.chatBubbleActionButtonIncomingBottomSingleImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsStrokeColor, wallpaper: wallpaper), position: .bottomSingle) - self.chatBubbleActionButtonOutgoingMiddleImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsStrokeColor, wallpaper: wallpaper), position: .middle) - self.chatBubbleActionButtonOutgoingBottomLeftImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsStrokeColor, wallpaper: wallpaper), position: .bottomLeft) - self.chatBubbleActionButtonOutgoingBottomRightImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsStrokeColor, wallpaper: wallpaper), position: .bottomRight) - self.chatBubbleActionButtonOutgoingBottomSingleImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsStrokeColor, wallpaper: wallpaper), position: .bottomSingle) + self.chatBubbleActionButtonIncomingMiddleImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsStrokeColor, wallpaper: wallpaper), position: .middle, bubbleCorners: bubbleCorners) + self.chatBubbleActionButtonIncomingBottomLeftImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsStrokeColor, wallpaper: wallpaper), position: .bottomLeft, bubbleCorners: bubbleCorners) + self.chatBubbleActionButtonIncomingBottomRightImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsStrokeColor, wallpaper: wallpaper), position: .bottomRight, bubbleCorners: bubbleCorners) + self.chatBubbleActionButtonIncomingBottomSingleImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsStrokeColor, wallpaper: wallpaper), position: .bottomSingle, bubbleCorners: bubbleCorners) + self.chatBubbleActionButtonOutgoingMiddleImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsStrokeColor, wallpaper: wallpaper), position: .middle, bubbleCorners: bubbleCorners) + self.chatBubbleActionButtonOutgoingBottomLeftImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsStrokeColor, wallpaper: wallpaper), position: .bottomLeft, bubbleCorners: bubbleCorners) + self.chatBubbleActionButtonOutgoingBottomRightImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsStrokeColor, wallpaper: wallpaper), position: .bottomRight, bubbleCorners: bubbleCorners) + self.chatBubbleActionButtonOutgoingBottomSingleImage = messageBubbleActionButtonImage(color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsFillColor, wallpaper: wallpaper), strokeColor: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsStrokeColor, wallpaper: wallpaper), position: .bottomSingle, bubbleCorners: bubbleCorners) self.chatBubbleActionButtonIncomingMessageIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotMessage"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonIncomingLinkIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLink"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonIncomingShareIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotShare"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! diff --git a/submodules/TelegramPresentationData/Sources/PresentationsResourceCache.swift b/submodules/TelegramPresentationData/Sources/PresentationsResourceCache.swift index 601f095294..2d59cc9eec 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationsResourceCache.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationsResourceCache.swift @@ -4,10 +4,12 @@ import SwiftSignalKit private final class PresentationsResourceCacheHolder { var images: [Int32: UIImage] = [:] + var parameterImages: [PresentationResourceParameterKey: UIImage] = [:] } private final class PresentationsResourceAnyCacheHolder { var objects: [Int32: AnyObject] = [:] + var parameterObjects: [PresentationResourceParameterKey: AnyObject] = [:] } public final class PresentationsResourceCache { @@ -32,6 +34,24 @@ public final class PresentationsResourceCache { } } + public func parameterImage(_ key: PresentationResourceParameterKey, _ theme: PresentationTheme, _ generate: (PresentationTheme) -> UIImage?) -> UIImage? { + let result = self.imageCache.with { holder -> UIImage? in + return holder.parameterImages[key] + } + if let result = result { + return result + } else { + if let image = generate(theme) { + self.imageCache.with { holder -> Void in + holder.parameterImages[key] = image + } + return image + } else { + return nil + } + } + } + public func object(_ key: Int32, _ theme: PresentationTheme, _ generate: (PresentationTheme) -> AnyObject?) -> AnyObject? { let result = self.objectCache.with { holder -> AnyObject? in return holder.objects[key] @@ -49,4 +69,22 @@ public final class PresentationsResourceCache { } } } + + public func parameterObject(_ key: PresentationResourceParameterKey, _ theme: PresentationTheme, _ generate: (PresentationTheme) -> AnyObject?) -> AnyObject? { + let result = self.objectCache.with { holder -> AnyObject? in + return holder.parameterObjects[key] + } + if let result = result { + return result + } else { + if let object = generate(theme) { + self.objectCache.with { holder -> Void in + holder.parameterObjects[key] = object + } + return object + } else { + return nil + } + } + } } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index b41ba4527b..5a33489aa8 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -1,4 +1,5 @@ import Foundation +import UIKit public struct PresentationResources { } @@ -19,6 +20,7 @@ public enum PresentationResourceKey: Int32 { case navigationShareIcon case navigationSearchIcon case navigationCompactSearchIcon + case navigationMoreIcon case navigationAddIcon case navigationPlayerCloseButton @@ -47,6 +49,8 @@ public enum PresentationResourceKey: Int32 { case itemListVerifiedPeerIcon case itemListCloudFetchIcon case itemListCloseIconImage + case itemListMakeVisibleIcon + case itemListMakeInvisibleIcon case itemListCornersTop case itemListCornersBottom case itemListCornersBoth @@ -77,12 +81,6 @@ public enum PresentationResourceKey: Int32 { case chatTitleLockIcon case chatTitleMuteIcon - case chatPrincipalThemeEssentialGraphicsWithWallpaper - case chatPrincipalThemeEssentialGraphicsWithoutWallpaper - - case chatPrincipalThemeAdditionalGraphicsWithCustomWallpaper - case chatPrincipalThemeAdditionalGraphicsWithDefaultWallpaper - case chatBubbleVerticalLineIncomingImage case chatBubbleVerticalLineOutgoingImage @@ -203,9 +201,7 @@ public enum PresentationResourceKey: Int32 { case chatBubbleIncomingCallButtonImage case chatBubbleOutgoingCallButtonImage - - case chatBubbleMapPinImage - + case callListOutgoingIcon case callListInfoButton @@ -221,3 +217,23 @@ public enum PresentationResourceKey: Int32 { case emptyChatListCheckIcon } + +public enum PresentationResourceParameterKey: Hashable { + case chatOutgoingFullCheck(CGFloat) + case chatOutgoingPartialCheck(CGFloat) + case chatMediaFullCheck(CGFloat) + case chatMediaPartialCheck(CGFloat) + case chatFreeFullCheck(CGFloat, Bool) + case chatFreePartialCheck(CGFloat, Bool) + + case chatListBadgeBackgroundActive(CGFloat) + case chatListBadgeBackgroundInactive(CGFloat) + case chatListBadgeBackgroundMention(CGFloat) + case chatListBadgeBackgroundInactiveMention(CGFloat) + case chatListBadgeBackgroundPinned(CGFloat) + + case chatBubbleMediaCorner(incoming: Bool, mainRadius: CGFloat, inset: CGFloat) + + case chatPrincipalThemeEssentialGraphics(hasWallpaper: Bool, bubbleCorners: PresentationChatBubbleCorners) + case chatPrincipalThemeAdditionalGraphics(isCustomWallpaper: Bool, bubbleCorners: PresentationChatBubbleCorners) +} diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index f21b719d48..c639d5bfa1 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -69,18 +69,17 @@ public struct PresentationResourcesChat { }) } - public static func principalGraphics(mediaBox: MediaBox, knockoutWallpaper: Bool, theme: PresentationTheme, wallpaper: TelegramWallpaper, gradientBubbles: Bool) -> PrincipalThemeEssentialGraphics { + public static func principalGraphics(mediaBox: MediaBox, knockoutWallpaper: Bool, theme: PresentationTheme, wallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners) -> PrincipalThemeEssentialGraphics { let hasWallpaper = !wallpaper.isEmpty - let key: PresentationResourceKey = !hasWallpaper ? PresentationResourceKey.chatPrincipalThemeEssentialGraphicsWithoutWallpaper : PresentationResourceKey.chatPrincipalThemeEssentialGraphicsWithWallpaper - return theme.object(key.rawValue, { theme in - return PrincipalThemeEssentialGraphics(mediaBox: mediaBox, presentationTheme: theme, wallpaper: wallpaper, preview: theme.preview, knockoutMode: knockoutWallpaper, gradientBubbles: gradientBubbles) + return theme.object(PresentationResourceParameterKey.chatPrincipalThemeEssentialGraphics(hasWallpaper: hasWallpaper, bubbleCorners: bubbleCorners), { theme in + return PrincipalThemeEssentialGraphics(mediaBox: mediaBox, presentationTheme: theme, wallpaper: wallpaper, preview: theme.preview, knockoutMode: knockoutWallpaper, bubbleCorners: bubbleCorners) }) as! PrincipalThemeEssentialGraphics } - public static func additionalGraphics(_ theme: PresentationTheme, wallpaper: TelegramWallpaper) -> PrincipalThemeAdditionalGraphics { - let key: PresentationResourceKey = wallpaper.isBuiltin ? PresentationResourceKey.chatPrincipalThemeAdditionalGraphicsWithDefaultWallpaper : PresentationResourceKey.chatPrincipalThemeAdditionalGraphicsWithCustomWallpaper - return theme.object(key.rawValue, { theme in - return PrincipalThemeAdditionalGraphics(theme.chat, wallpaper: wallpaper) + public static func additionalGraphics(_ theme: PresentationTheme, wallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners) -> PrincipalThemeAdditionalGraphics { + let key: PresentationResourceParameterKey = .chatPrincipalThemeAdditionalGraphics(isCustomWallpaper: !wallpaper.isBuiltin, bubbleCorners: bubbleCorners) + return theme.object(key, { theme in + return PrincipalThemeAdditionalGraphics(theme.chat, wallpaper: wallpaper, bubbleCorners: bubbleCorners) }) as! PrincipalThemeAdditionalGraphics } @@ -251,7 +250,7 @@ public struct PresentationResourcesChat { public static func chatInputMediaPanelAddPackButtonImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputMediaPanelAddPackButtonImage.rawValue, { theme in - return generateStretchableFilledCircleImage(diameter: 8.0, color: nil, strokeColor: theme.chat.inputPanel.panelControlAccentColor, strokeWidth: 1.0, backgroundColor: nil) + return generateStretchableFilledCircleImage(diameter: 28.0, color: theme.chat.inputPanel.panelControlAccentColor, strokeColor: nil, strokeWidth: 1.0, backgroundColor: nil) }) } @@ -644,23 +643,6 @@ public struct PresentationResourcesChat { }) } - public static func chatBubbleMapPinImage(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.chatBubbleMapPinImage.rawValue, { theme in - return generateImage(CGSize(width: 62.0, height: 74.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - if let shadowImage = UIImage(bundleImageName: "Chat/Message/LocationPinShadow"), let cgImage = shadowImage.cgImage { - context.draw(cgImage, in: CGRect(origin: CGPoint(), size: shadowImage.size)) - } - if let backgroundImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/LocationPinBackground"), color: theme.list.itemAccentColor), let cgImage = backgroundImage.cgImage { - context.draw(cgImage, in: CGRect(origin: CGPoint(), size: backgroundImage.size)) - } - if let foregroundImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/LocationPinForeground"), color: theme.list.plainBackgroundColor), let cgImage = foregroundImage.cgImage { - context.draw(cgImage, in: CGRect(origin: CGPoint(x: 15.0, y: 26.0), size: foregroundImage.size)) - } - }) - }) - } - public static func chatInputSearchPanelUpImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputSearchPanelUpImage.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/UpButton"), color: theme.chat.inputPanel.panelControlAccentColor) @@ -929,4 +911,48 @@ public struct PresentationResourcesChat { return UIImage(bundleImageName: "Chat/Info/GroupMembersIcon")?.precomposed() }) } + + public static func chatOutgoingFullCheck(_ theme: PresentationTheme, size: CGFloat) -> UIImage? { + return theme.image(PresentationResourceParameterKey.chatOutgoingFullCheck(size), { _ in + return generateCheckImage(partial: false, color: theme.chat.message.outgoingCheckColor, width: size) + }) + } + + public static func chatOutgoingPartialCheck(_ theme: PresentationTheme, size: CGFloat) -> UIImage? { + return theme.image(PresentationResourceParameterKey.chatOutgoingPartialCheck(size), { _ in + return generateCheckImage(partial: true, color: theme.chat.message.outgoingCheckColor, width: size) + }) + } + + public static func chatMediaFullCheck(_ theme: PresentationTheme, size: CGFloat) -> UIImage? { + return theme.image(PresentationResourceParameterKey.chatMediaFullCheck(size), { _ in + return generateCheckImage(partial: false, color: .white, width: size) + }) + } + + public static func chatMediaPartialCheck(_ theme: PresentationTheme, size: CGFloat) -> UIImage? { + return theme.image(PresentationResourceParameterKey.chatMediaPartialCheck(size), { _ in + return generateCheckImage(partial: true, color: .white, width: size) + }) + } + + public static func chatFreeFullCheck(_ theme: PresentationTheme, size: CGFloat, isDefaultWallpaper: Bool) -> UIImage? { + return theme.image(PresentationResourceParameterKey.chatFreeFullCheck(size, isDefaultWallpaper), { _ in + let color = isDefaultWallpaper ? theme.chat.serviceMessage.components.withDefaultWallpaper.primaryText : theme.chat.serviceMessage.components.withCustomWallpaper.primaryText + return generateCheckImage(partial: false, color: color, width: size) + }) + } + + public static func chatFreePartialCheck(_ theme: PresentationTheme, size: CGFloat, isDefaultWallpaper: Bool) -> UIImage? { + return theme.image(PresentationResourceParameterKey.chatFreePartialCheck(size, isDefaultWallpaper), { _ in + let color = isDefaultWallpaper ? theme.chat.serviceMessage.components.withDefaultWallpaper.primaryText : theme.chat.serviceMessage.components.withCustomWallpaper.primaryText + return generateCheckImage(partial: true, color: color, width: size) + }) + } + + public static func chatBubbleMediaCorner(_ theme: PresentationTheme, incoming: Bool, mainRadius: CGFloat, inset: CGFloat) -> UIImage? { + return theme.image(PresentationResourceParameterKey.chatBubbleMediaCorner(incoming: incoming, mainRadius: mainRadius, inset: inset), { _ in + return mediaBubbleCornerImage(incoming: incoming, radius: mainRadius, inset: inset) + }) + } } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift index 77dd16f219..12aca8462b 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift @@ -19,8 +19,8 @@ private func generateStatusCheckImage(theme: PresentationTheme, single: Bool) -> }) } -private func generateBadgeBackgroundImage(theme: PresentationTheme, active: Bool, icon: UIImage? = nil) -> UIImage? { - return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context in +private func generateBadgeBackgroundImage(theme: PresentationTheme, diameter: CGFloat, active: Bool, icon: UIImage? = nil) -> UIImage? { + return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) if active { context.setFillColor(theme.chatList.unreadBadgeActiveBackgroundColor.cgColor) @@ -31,7 +31,7 @@ private func generateBadgeBackgroundImage(theme: PresentationTheme, active: Bool if let icon = icon, let cgImage = icon.cgImage { context.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((size.width - icon.size.width) / 2.0), y: floor((size.height - icon.size.height) / 2.0)), size: icon.size)) } - })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10) + })?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0)) } private func generateClockFrameImage(color: UIColor) -> UIImage? { @@ -169,32 +169,32 @@ public struct PresentationResourcesChatList { }) } - public static func badgeBackgroundActive(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.chatListBadgeBackgroundActive.rawValue, { theme in - return generateBadgeBackgroundImage(theme: theme, active: true) + public static func badgeBackgroundActive(_ theme: PresentationTheme, diameter: CGFloat) -> UIImage? { + return theme.image(PresentationResourceParameterKey.chatListBadgeBackgroundActive(diameter), { theme in + return generateBadgeBackgroundImage(theme: theme, diameter: diameter, active: true) }) } - public static func badgeBackgroundInactive(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.chatListBadgeBackgroundInactive.rawValue, { theme in - return generateBadgeBackgroundImage(theme: theme, active: false) + public static func badgeBackgroundInactive(_ theme: PresentationTheme, diameter: CGFloat) -> UIImage? { + return theme.image(PresentationResourceParameterKey.chatListBadgeBackgroundInactive(diameter), { theme in + return generateBadgeBackgroundImage(theme: theme, diameter: diameter, active: false) }) } - public static func badgeBackgroundMention(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.chatListBadgeBackgroundMention.rawValue, { theme in - return generateBadgeBackgroundImage(theme: theme, active: true, icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: theme.chatList.unreadBadgeActiveTextColor)) + public static func badgeBackgroundMention(_ theme: PresentationTheme, diameter: CGFloat) -> UIImage? { + return theme.image(PresentationResourceParameterKey.chatListBadgeBackgroundMention(diameter), { theme in + return generateBadgeBackgroundImage(theme: theme, diameter: diameter, active: true, icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: theme.chatList.unreadBadgeActiveTextColor)) }) } - public static func badgeBackgroundInactiveMention(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.chatListBadgeBackgroundInactiveMention.rawValue, { theme in - return generateBadgeBackgroundImage(theme: theme, active: false, icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: theme.chatList.unreadBadgeInactiveTextColor)) + public static func badgeBackgroundInactiveMention(_ theme: PresentationTheme, diameter: CGFloat) -> UIImage? { + return theme.image(PresentationResourceParameterKey.chatListBadgeBackgroundInactiveMention(diameter), { theme in + return generateBadgeBackgroundImage(theme: theme, diameter: diameter, active: false, icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: theme.chatList.unreadBadgeInactiveTextColor)) }) } - public static func badgeBackgroundPinned(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.chatListBadgeBackgroundPinned.rawValue, { theme in + public static func badgeBackgroundPinned(_ theme: PresentationTheme, diameter: CGFloat) -> UIImage? { + return theme.image(PresentationResourceParameterKey.chatListBadgeBackgroundPinned(diameter), { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPinnedIcon"), color: theme.chatList.pinnedBadgeColor) }) } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift index 53b0efb48f..8a3e6fc808 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift @@ -152,6 +152,18 @@ public struct PresentationResourcesItemList { }) } + public static func makeVisibleIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListMakeVisibleIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Contact List/MakeVisibleIcon"), color: theme.list.itemAccentColor) + }) + } + + public static func makeInvisibleIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListMakeInvisibleIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Contact List/MakeInvisibleIcon"), color: theme.list.itemDestructiveColor) + }) + } + public static func cornersImage(_ theme: PresentationTheme, top: Bool, bottom: Bool) -> UIImage? { if !top && !bottom { return nil diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift index b5982305bb..afc4cc9f0a 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift @@ -71,6 +71,19 @@ public struct PresentationResourcesRootController { }) } + public static func navigationMoreIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationMoreIcon.rawValue, { theme in + return generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.rootController.navigationBar.accentTextColor.cgColor) + let dotSize: CGFloat = 4.0 + context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: floor((size.height - dotSize) / 2.0)), size: CGSize(width: dotSize, height: dotSize))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 13.0, y: floor((size.height - dotSize) / 2.0)), size: CGSize(width: dotSize, height: dotSize))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 20.0, y: floor((size.height - dotSize) / 2.0)), size: CGSize(width: dotSize, height: dotSize))) + }) + }) + } + public static func navigationAddIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationAddIcon.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat List/AddIcon"), color: theme.rootController.navigationBar.accentTextColor) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index dd5d95ec10..c1d606760b 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -59,4 +59,6 @@ public struct PresentationResourcesSettings { public static let setPasscode = renderIcon(name: "Settings/MenuIcons/SetPasscode") public static let clearCache = renderIcon(name: "Settings/MenuIcons/ClearCache") public static let changePhoneNumber = renderIcon(name: "Settings/MenuIcons/ChangePhoneNumber") + + public static let websites = renderIcon(name: "Settings/MenuIcons/Websites") } diff --git a/submodules/TelegramPresentationData/Sources/WallpaperUtils.swift b/submodules/TelegramPresentationData/Sources/WallpaperUtils.swift index 6e2a569957..23bc0158dc 100644 --- a/submodules/TelegramPresentationData/Sources/WallpaperUtils.swift +++ b/submodules/TelegramPresentationData/Sources/WallpaperUtils.swift @@ -1,4 +1,5 @@ import Foundation +import UIKit import TelegramCore import SyncCore @@ -8,13 +9,31 @@ public extension TelegramWallpaper { case .image: return false case let .file(file): - if file.isPattern, file.settings.color == 0xffffff { + if self.isPattern, file.settings.color == 0xffffff || file.settings.color == 0xffffffff { return true } else { return false } case let .color(color): - return color == 0xffffff + return color == 0xffffff || color == 0xffffffff + default: + return false + } + } + + var isColorOrGradient: Bool { + switch self { + case .color, .gradient: + return true + default: + return false + } + } + + var isPattern: Bool { + switch self { + case let .file(file): + return file.isPattern || file.file.mimeType == "application/x-tgwallpattern" default: return false } @@ -28,4 +47,12 @@ public extension TelegramWallpaper { return false } } + + var dimensions: CGSize? { + if case let .file(file) = self { + return file.file.dimensions?.cgSize + } else { + return nil + } + } } diff --git a/submodules/TelegramStringFormatting/Sources/DateFormat.swift b/submodules/TelegramStringFormatting/Sources/DateFormat.swift index 40813bbdd9..a15cf8ecb8 100644 --- a/submodules/TelegramStringFormatting/Sources/DateFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/DateFormat.swift @@ -93,10 +93,19 @@ public func stringForDate(timestamp: Int32, strings: PresentationStrings) -> Str return formatter.string(from: Date(timeIntervalSince1970: Double(timestamp))) } -public func stringForDateWithoutYear(date: Date, strings: PresentationStrings) -> String { +public func stringForDate(date: Date, timeZone: TimeZone? = TimeZone(secondsFromGMT: 0), strings: PresentationStrings) -> String { let formatter = DateFormatter() formatter.timeStyle = .none - formatter.timeZone = TimeZone(secondsFromGMT: 0) + formatter.dateStyle = .medium + formatter.timeZone = timeZone + formatter.locale = localeWithStrings(strings) + return formatter.string(from: date) +} + +public func stringForDateWithoutYear(date: Date, timeZone: TimeZone? = TimeZone(secondsFromGMT: 0), strings: PresentationStrings) -> String { + let formatter = DateFormatter() + formatter.timeStyle = .none + formatter.timeZone = timeZone formatter.locale = localeWithStrings(strings) formatter.setLocalizedDateFormatFromTemplate("MMMMd") return formatter.string(from: date) diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index b0b628d0fe..b2e3f927b4 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -81,10 +81,10 @@ public enum MessageContentKind: Equatable { } } -public func messageContentKind(_ message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId) -> MessageContentKind { +public func messageContentKind(contentSettings: ContentSettings, message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId) -> MessageContentKind { for attribute in message.attributes { if let attribute = attribute as? RestrictedContentMessageAttribute { - if let text = attribute.platformText(platform: "ios") { + if let text = attribute.platformText(platform: "ios", contentSettings: contentSettings) { return .restricted(text) } break @@ -215,9 +215,9 @@ public func stringForMediaKind(_ kind: MessageContentKind, strings: Presentation } } -public func descriptionStringForMessage(_ message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId) -> (String, Bool) { +public func descriptionStringForMessage(contentSettings: ContentSettings, message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId) -> (String, Bool) { if !message.text.isEmpty { return (message.text, false) } - return stringForMediaKind(messageContentKind(message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId), strings: strings) + return stringForMediaKind(messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId), strings: strings) } diff --git a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift index b7bdd08a48..31893c0e27 100644 --- a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift @@ -109,12 +109,13 @@ public func stringForMonth(strings: PresentationStrings, month: Int32, ofYear ye public enum RelativeTimestampFormatDay { case today case yesterday + case tomorrow } public func stringForUserPresence(strings: PresentationStrings, day: RelativeTimestampFormatDay, dateTimeFormat: PresentationDateTimeFormat, hours: Int32, minutes: Int32) -> String { let dayString: String switch day { - case .today: + case .today, .tomorrow: dayString = strings.LastSeen_TodayAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).0 case .yesterday: dayString = strings.LastSeen_YesterdayAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).0 @@ -125,10 +126,13 @@ public func stringForUserPresence(strings: PresentationStrings, day: RelativeTim private func humanReadableStringForTimestamp(strings: PresentationStrings, day: RelativeTimestampFormatDay, dateTimeFormat: PresentationDateTimeFormat, hours: Int32, minutes: Int32) -> String { let dayString: String switch day { - case .today: - dayString = strings.Time_TodayAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).0 - case .yesterday: - dayString = strings.Time_YesterdayAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).0 + case .today: + dayString = strings.Time_TodayAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).0 + case .yesterday: + dayString = strings.Time_YesterdayAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).0 + case .tomorrow: + dayString = strings.Time_TomorrowAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).0 + } return dayString } @@ -148,12 +152,14 @@ public func humanReadableStringForTimestamp(strings: PresentationStrings, dateTi } let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday - if dayDifference == 0 || dayDifference == -1 { + if dayDifference == 0 || dayDifference == -1 || dayDifference == 1 { let day: RelativeTimestampFormatDay if dayDifference == 0 { day = .today - } else { + } else if dayDifference == -1 { day = .yesterday + } else { + day = .tomorrow } return humanReadableStringForTimestamp(strings: strings, day: day, dateTimeFormat: dateTimeFormat, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min) } else { diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 9e7e862c6f..d634e990e1 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -67,10 +67,15 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } } else { var attributePeerIds: [(Int, PeerId?)] = [(0, message.author?.id)] + let resultTitleString: (String, [(Int, NSRange)]) if peerIds.count == 1 { attributePeerIds.append((1, peerIds.first)) + resultTitleString = strings.Notification_Invited(authorName, peerDebugDisplayTitles(peerIds, message.peers)) + } else { + resultTitleString = strings.Notification_InvitedMultiple(authorName, peerDebugDisplayTitles(peerIds, message.peers)) } - attributedString = addAttributesToStringWithRanges(strings.Notification_Invited(authorName, peerDebugDisplayTitles(peerIds, message.peers)), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: attributePeerIds)) + + attributedString = addAttributesToStringWithRanges(resultTitleString, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: attributePeerIds)) } case let .removedMembers(peerIds): if peerIds.first == message.author?.id { @@ -127,7 +132,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, case sticker case location case contact - case poll + case poll(TelegramMediaPollKind) case deleted } @@ -183,8 +188,8 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, type = .location } else if let _ = media as? TelegramMediaContact { type = .contact - } else if let _ = media as? TelegramMediaPoll { - type = .poll + } else if let poll = media as? TelegramMediaPoll { + type = .poll(poll.kind) } } } else { @@ -224,8 +229,13 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedLocationMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) case .contact: attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedContactMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) - case .poll: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedPollMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + case let .poll(kind): + switch kind { + case .poll: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedPollMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + case .quiz: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedQuizMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + } case .deleted: attributedString = addAttributesToStringWithRanges(strings.PUSH_PINNED_NOTEXT(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) } diff --git a/submodules/TelegramUI/BUCK b/submodules/TelegramUI/BUCK index 35d07b5256..6eb5520ee9 100644 --- a/submodules/TelegramUI/BUCK +++ b/submodules/TelegramUI/BUCK @@ -47,7 +47,6 @@ framework( "//submodules/DeviceAccess:DeviceAccess", "//submodules/WatchCommon/Host:WatchCommon", "//submodules/LightweightAccountData:LightweightAccountData", - "//submodules/HockeySDK-iOS:HockeySDK", "//submodules/BuildConfig:BuildConfig", "//submodules/BuildConfigExtra:BuildConfigExtra", "//submodules/rlottie:RLottieBinding", @@ -187,8 +186,8 @@ framework( "//submodules/MessageReactionListUI:MessageReactionListUI", "//submodules/SegmentedControlNode:SegmentedControlNode", "//submodules/AppBundle:AppBundle", - "//submodules/WalletUI:WalletUI", - "//submodules/WalletCore:WalletCore", + #"//submodules/WalletUI:WalletUI", + #"//submodules/WalletCore:WalletCore", "//submodules/Markdown:Markdown", "//submodules/SearchPeerMembers:SearchPeerMembers", "//submodules/WidgetItems:WidgetItems", @@ -197,7 +196,12 @@ framework( "//submodules/AppLock:AppLock", "//submodules/NotificationsPresentationData:NotificationsPresentationData", "//submodules/UrlWhitelist:UrlWhitelist", - "//submodules/AppIntents:AppIntents", + "//submodules/TelegramIntents:TelegramIntents", + "//submodules/LocationResources:LocationResources", + "//submodules/ItemListVenueItem:ItemListVenueItem", + "//submodules/SemanticStatusNode:SemanticStatusNode", + "//submodules/AccountUtils:AccountUtils", + "//submodules/Svg:Svg", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/EditAvatarIconLarge.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Avatar/EditAvatarIconLarge.imageset/Contents.json new file mode 100644 index 0000000000..868e9f5048 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Avatar/EditAvatarIconLarge.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_camera.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/EditAvatarIconLarge.imageset/ic_camera.pdf b/submodules/TelegramUI/Images.xcassets/Avatar/EditAvatarIconLarge.imageset/ic_camera.pdf new file mode 100644 index 0000000000..6b39b47bbb Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Avatar/EditAvatarIconLarge.imageset/ic_camera.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chart/Contents.json b/submodules/TelegramUI/Images.xcassets/Chart/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chart/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "provides-namespace" : true + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chart/arrow_left.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chart/arrow_left.imageset/Contents.json new file mode 100644 index 0000000000..78b05b5e2a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chart/arrow_left.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "arrow_left.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/InfoIcon.imageset/Information Icon.pdf b/submodules/TelegramUI/Images.xcassets/Chart/arrow_left.imageset/arrow_left.pdf similarity index 79% rename from submodules/TelegramUI/Images.xcassets/Chat List/InfoIcon.imageset/Information Icon.pdf rename to submodules/TelegramUI/Images.xcassets/Chart/arrow_left.imageset/arrow_left.pdf index 601a593a3f..7b20434672 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Chat List/InfoIcon.imageset/Information Icon.pdf and b/submodules/TelegramUI/Images.xcassets/Chart/arrow_left.imageset/arrow_left.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chart/arrow_right.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chart/arrow_right.imageset/Contents.json new file mode 100644 index 0000000000..147027fa6d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chart/arrow_right.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "arrow_right.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chart/arrow_right.imageset/arrow_right.pdf b/submodules/TelegramUI/Images.xcassets/Chart/arrow_right.imageset/arrow_right.pdf new file mode 100644 index 0000000000..baf05c2435 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chart/arrow_right.imageset/arrow_right.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chart/selection_frame_dark.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chart/selection_frame_dark.imageset/Contents.json new file mode 100644 index 0000000000..65481db2a7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chart/selection_frame_dark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "selection_frame_dark.pdf", + "resizing" : { + "mode" : "3-part-horizontal", + "center" : { + "mode" : "stretch", + "width" : 1 + }, + "cap-insets" : { + "right" : 11, + "left" : 11 + } + } + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chart/selection_frame_dark.imageset/selection_frame_dark.pdf b/submodules/TelegramUI/Images.xcassets/Chart/selection_frame_dark.imageset/selection_frame_dark.pdf new file mode 100644 index 0000000000..ae27128091 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chart/selection_frame_dark.imageset/selection_frame_dark.pdf @@ -0,0 +1,61 @@ +%PDF-1.4 +%Óëéá +1 0 obj +<> +endobj +2 0 obj +<> stream +xœ’MN1 …÷9…×HìØùñ º.,8ÀÊbŠD{ 'ÓvBZ$&Ò(zvü9Î#@[ÙOÌG÷íª@$£ÉmàôîÞžàËyæ ž‹€—áeƒr:¸çÂáì³×ú…kÊpti­­Ú©‹ >)å Ô©SÝ JŒ¢) °zÆKº´¢Hf7Æ.<£Œ± U lü5…(eÌz;ní¹¿à[µfsc×éwøNÜú5Â&oW\~É·™tÄm„ß`vŸÖ{Õ *CK°þŠ=N' Õ|¡Tªžˆþ’[ŠRšü öbÐ35ÏL¡d‚Õvu™—ÖM³U ±²ÏD-=°1‰X—¬ÙײÉT¼a«ß'Û€²°™zq¯nßc¹¨M=ÄÿÀ"Xîà+& ð-y€ïÝKSª² +endstream +endobj +3 0 obj +<> +endobj +4 0 obj +<> +endobj +5 0 obj +<>>> +/MediaBox [0 0 114 42] +/Contents 2 0 R +/Parent 4 0 R>> +endobj +6 0 obj +<> +endobj +7 0 obj +<> +endobj +xref +0 8 +0000000000 65535 f +0000000015 00000 n +0000000089 00000 n +0000000482 00000 n +0000000529 00000 n +0000000584 00000 n +0000000765 00000 n +0000000802 00000 n +trailer +<> +startxref +880 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chart/selection_frame_light.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chart/selection_frame_light.imageset/Contents.json new file mode 100644 index 0000000000..8cc38ca79c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chart/selection_frame_light.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "selection_frame_light.pdf", + "resizing" : { + "mode" : "3-part-horizontal", + "center" : { + "mode" : "stretch", + "width" : 1 + }, + "cap-insets" : { + "right" : 11, + "left" : 11 + } + } + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chart/selection_frame_light.imageset/selection_frame_light.pdf b/submodules/TelegramUI/Images.xcassets/Chart/selection_frame_light.imageset/selection_frame_light.pdf new file mode 100644 index 0000000000..c426c5d695 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chart/selection_frame_light.imageset/selection_frame_light.pdf @@ -0,0 +1,62 @@ +%PDF-1.4 +%Óëéá +1 0 obj +<> +endobj +2 0 obj +<> stream +xœ’ÏNÃ0 Æï~ +Ÿ‘ìÄùã'Øypà*‡‰ñþNº­Y6$5оÏõÏuÌH¶&¶M<ÎGø†*0‹#¢@Ñävòxz‡·'ü—c@W¼½¶½ìðV8àyGxø¦ì´>þ’„3!­™U;uï’röÂ:Õ‘Ä(š’`P¨Ä’î–”8â £wæ±eôÎT-¸ñ׿”)ëõs+w0æþofnÅæ2z—Þrèð¸Õk„MÞ~q¹‘¯=éˆ[ ÞÁ ŸV{Õ ©]]õ­¾b—ÓÉÂ5^8U ùª'æ¿ä–„£”&?ȽôÃÆ‹ÛÄ1N¾dôBuèë²QZm„¸ŽPÐà2{Q ÷Áù˜D¬Ê ÙÕ†“¹8-,l Êl¤x…} E­ë>þn¡r_1i€oÁ|¿BªV +endstream +endobj +3 0 obj +<> +endobj +4 0 obj +<> +endobj +5 0 obj +<>>> +/MediaBox [0 0 114 42] +/Contents 2 0 R +/Parent 4 0 R>> +endobj +6 0 obj +<> +endobj +7 0 obj +<> +endobj +xref +0 8 +0000000000 65535 f +0000000015 00000 n +0000000089 00000 n +0000000482 00000 n +0000000529 00000 n +0000000584 00000 n +0000000765 00000 n +0000000802 00000 n +trailer +<> +startxref +880 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/InfoIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/InfoIcon.imageset/Contents.json index cde960dfed..e6e063985c 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat List/InfoIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat List/InfoIcon.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "Information Icon.pdf" + "filename" : "ic_info.pdf" } ], "info" : { diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/InfoIcon.imageset/ic_info.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/InfoIcon.imageset/ic_info.pdf new file mode 100644 index 0000000000..03335ea456 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/InfoIcon.imageset/ic_info.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "provides-namespace" : true + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconCalls.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconCalls.imageset/Contents.json new file mode 100644 index 0000000000..9afc9c4e5d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconCalls.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_calls.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconCalls.imageset/ic_calls.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconCalls.imageset/ic_calls.pdf new file mode 100644 index 0000000000..f42e5fe81b Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconCalls.imageset/ic_calls.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconChats.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconChats.imageset/Contents.json new file mode 100644 index 0000000000..3637003f27 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconChats.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_messages.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconChats.imageset/ic_messages.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconChats.imageset/ic_messages.pdf new file mode 100644 index 0000000000..64aba1907d Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconChats.imageset/ic_messages.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconContacts.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconContacts.imageset/Contents.json new file mode 100644 index 0000000000..006f82fb71 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconContacts.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_contacts.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconContacts.imageset/ic_contacts.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconContacts.imageset/ic_contacts.pdf new file mode 100644 index 0000000000..41f48e4e2d Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconContacts.imageset/ic_contacts.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconSettings.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconSettings.imageset/Contents.json new file mode 100644 index 0000000000..261d9ff3a9 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconSettings.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_settings.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconSettings.imageset/ic_settings.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconSettings.imageset/ic_settings.pdf new file mode 100644 index 0000000000..0f3b0dc083 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/Tabs/Holiday/IconSettings.imageset/ic_settings.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Tip.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Tip.imageset/Contents.json new file mode 100644 index 0000000000..8a66114530 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Tip.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_lamp.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Tip.imageset/ic_lamp.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Tip.imageset/ic_lamp.pdf new file mode 100644 index 0000000000..4b286ac00c Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Tip.imageset/ic_lamp.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/Contents.json index 52ee5360d8..3fd562e2a7 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/Contents.json @@ -2,17 +2,7 @@ "images" : [ { "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "StickerKeyboardTrendingIcon@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "StickerKeyboardTrendingIcon@3x.png", - "scale" : "3x" + "filename" : "ic_addstickers.pdf" } ], "info" : { diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/StickerKeyboardTrendingIcon@2x.png b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/StickerKeyboardTrendingIcon@2x.png deleted file mode 100644 index 67de340019..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/StickerKeyboardTrendingIcon@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/StickerKeyboardTrendingIcon@3x.png b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/StickerKeyboardTrendingIcon@3x.png deleted file mode 100644 index ed14979353..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/StickerKeyboardTrendingIcon@3x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/ic_addstickers.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/ic_addstickers.pdf new file mode 100644 index 0000000000..cbe24a9aa0 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/TrendingIcon.imageset/ic_addstickers.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/MakeInvisibleIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Contact List/MakeInvisibleIcon.imageset/Contents.json new file mode 100644 index 0000000000..7abcd976aa --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Contact List/MakeInvisibleIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_stopshowme.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/MakeInvisibleIcon.imageset/ic_stopshowme.pdf b/submodules/TelegramUI/Images.xcassets/Contact List/MakeInvisibleIcon.imageset/ic_stopshowme.pdf new file mode 100644 index 0000000000..9cc4c3d65a Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Contact List/MakeInvisibleIcon.imageset/ic_stopshowme.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/MakeVisibleIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Contact List/MakeVisibleIcon.imageset/Contents.json new file mode 100644 index 0000000000..97cc9be019 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Contact List/MakeVisibleIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_showme.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/MakeVisibleIcon.imageset/ic_showme.pdf b/submodules/TelegramUI/Images.xcassets/Contact List/MakeVisibleIcon.imageset/ic_showme.pdf new file mode 100644 index 0000000000..0c5a6e2133 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Contact List/MakeVisibleIcon.imageset/ic_showme.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/WallpaperColorIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/FoursquareAttribution.imageset/Contents.json similarity index 79% rename from submodules/TelegramUI/Images.xcassets/Settings/WallpaperColorIcon.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Location/FoursquareAttribution.imageset/Contents.json index 5426142806..9d99a2f4b9 100644 --- a/submodules/TelegramUI/Images.xcassets/Settings/WallpaperColorIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Location/FoursquareAttribution.imageset/Contents.json @@ -6,12 +6,11 @@ }, { "idiom" : "universal", - "filename" : "color@2x.png", + "filename" : "FoursquareAttribution@2x.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "color@3x.png", "scale" : "3x" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Location/FoursquareAttribution.imageset/FoursquareAttribution@2x.png b/submodules/TelegramUI/Images.xcassets/Location/FoursquareAttribution.imageset/FoursquareAttribution@2x.png new file mode 100644 index 0000000000..6b2650f7a3 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/FoursquareAttribution.imageset/FoursquareAttribution@2x.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/HomeIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/HomeIcon.imageset/Contents.json new file mode 100644 index 0000000000..f8498abac3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/HomeIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_maphome.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Location/HomeIcon.imageset/ic_maphome.pdf b/submodules/TelegramUI/Images.xcassets/Location/HomeIcon.imageset/ic_maphome.pdf new file mode 100644 index 0000000000..1cae1ad6c6 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/HomeIcon.imageset/ic_maphome.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/InfoActiveIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/InfoActiveIcon.imageset/Contents.json new file mode 100644 index 0000000000..d19cbdddd5 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/InfoActiveIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_infofilled_svg.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Location/InfoActiveIcon.imageset/ic_infofilled_svg.pdf b/submodules/TelegramUI/Images.xcassets/Location/InfoActiveIcon.imageset/ic_infofilled_svg.pdf new file mode 100644 index 0000000000..c166675c46 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/InfoActiveIcon.imageset/ic_infofilled_svg.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/InfoIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/InfoIcon.imageset/Contents.json new file mode 100644 index 0000000000..e6e063985c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/InfoIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_info.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Location/InfoIcon.imageset/ic_info.pdf b/submodules/TelegramUI/Images.xcassets/Location/InfoIcon.imageset/ic_info.pdf new file mode 100644 index 0000000000..03335ea456 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/InfoIcon.imageset/ic_info.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinBackground.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/PinBackground.imageset/Contents.json new file mode 100644 index 0000000000..3e68b3f0f2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/PinBackground.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LocationPinBackground@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LocationPinBackground@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinBackground.imageset/LocationPinBackground@2x.png b/submodules/TelegramUI/Images.xcassets/Location/PinBackground.imageset/LocationPinBackground@2x.png new file mode 100644 index 0000000000..6e2330dde7 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/PinBackground.imageset/LocationPinBackground@2x.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinBackground.imageset/LocationPinBackground@3x.png b/submodules/TelegramUI/Images.xcassets/Location/PinBackground.imageset/LocationPinBackground@3x.png new file mode 100644 index 0000000000..c99da533a0 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/PinBackground.imageset/LocationPinBackground@3x.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/Contents.json new file mode 100644 index 0000000000..3b8e5cbe3c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LocationPinIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LocationPinIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@2x.png b/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@2x.png new file mode 100644 index 0000000000..ab6fd29ccb Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@2x.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@3x.png b/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@3x.png new file mode 100644 index 0000000000..a57e0d4252 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@3x.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinShadow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/PinShadow.imageset/Contents.json new file mode 100644 index 0000000000..1919f794a1 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/PinShadow.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LocationPinShadow@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LocationPinShadow@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinShadow.imageset/LocationPinShadow@2x.png b/submodules/TelegramUI/Images.xcassets/Location/PinShadow.imageset/LocationPinShadow@2x.png new file mode 100644 index 0000000000..836010f30f Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/PinShadow.imageset/LocationPinShadow@2x.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinShadow.imageset/LocationPinShadow@3x.png b/submodules/TelegramUI/Images.xcassets/Location/PinShadow.imageset/LocationPinShadow@3x.png new file mode 100644 index 0000000000..caffa8f166 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/PinShadow.imageset/LocationPinShadow@3x.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinSmallBackground.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/PinSmallBackground.imageset/Contents.json new file mode 100644 index 0000000000..e68825f21c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/PinSmallBackground.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LocationSmallCircle@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LocationSmallCircle@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinSmallBackground.imageset/LocationSmallCircle@2x.png b/submodules/TelegramUI/Images.xcassets/Location/PinSmallBackground.imageset/LocationSmallCircle@2x.png new file mode 100644 index 0000000000..ae2064ea7c Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/PinSmallBackground.imageset/LocationSmallCircle@2x.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinSmallBackground.imageset/LocationSmallCircle@3x.png b/submodules/TelegramUI/Images.xcassets/Location/PinSmallBackground.imageset/LocationSmallCircle@3x.png new file mode 100644 index 0000000000..5baaa60ba9 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/PinSmallBackground.imageset/LocationSmallCircle@3x.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/SendLiveLocationIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/SendLiveLocationIcon.imageset/Contents.json new file mode 100644 index 0000000000..57e11d6b79 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/SendLiveLocationIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_mappinlive.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Location/SendLiveLocationIcon.imageset/ic_mappinlive.pdf b/submodules/TelegramUI/Images.xcassets/Location/SendLiveLocationIcon.imageset/ic_mappinlive.pdf new file mode 100644 index 0000000000..acaa40c811 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/SendLiveLocationIcon.imageset/ic_mappinlive.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/SendLocationIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/SendLocationIcon.imageset/Contents.json new file mode 100644 index 0000000000..82b9aa1e74 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/SendLocationIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_mappin.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Location/SendLocationIcon.imageset/ic_mappin.pdf b/submodules/TelegramUI/Images.xcassets/Location/SendLocationIcon.imageset/ic_mappin.pdf new file mode 100644 index 0000000000..d2291b336f Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/SendLocationIcon.imageset/ic_mappin.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/TrackIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/TrackIcon.imageset/Contents.json new file mode 100644 index 0000000000..53ff47a50c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/TrackIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_gps (2).pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Location/TrackIcon.imageset/ic_gps (2).pdf b/submodules/TelegramUI/Images.xcassets/Location/TrackIcon.imageset/ic_gps (2).pdf new file mode 100644 index 0000000000..50fc673a03 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/TrackIcon.imageset/ic_gps (2).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/WorkIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/WorkIcon.imageset/Contents.json new file mode 100644 index 0000000000..9739d32002 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/WorkIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_mapwork.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Location/WorkIcon.imageset/ic_mapwork.pdf b/submodules/TelegramUI/Images.xcassets/Location/WorkIcon.imageset/ic_mapwork.pdf new file mode 100644 index 0000000000..0150099b3e Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/WorkIcon.imageset/ic_mapwork.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/SoundOff.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/SoundOff.imageset/Contents.json new file mode 100644 index 0000000000..d2ce704e18 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/SoundOff.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "soundoff (2).pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/SoundOff.imageset/soundoff (2).pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/SoundOff.imageset/soundoff (2).pdf new file mode 100644 index 0000000000..8051a2a4e0 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Gallery/SoundOff.imageset/soundoff (2).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/SoundOn.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/SoundOn.imageset/Contents.json new file mode 100644 index 0000000000..1d93d33a26 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/SoundOn.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "soundon (2).pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/SoundOn.imageset/soundon (2).pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/SoundOn.imageset/soundon (2).pdf new file mode 100644 index 0000000000..20236a226b Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Gallery/SoundOn.imageset/soundon (2).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/Stickers.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/Stickers.imageset/Contents.json new file mode 100644 index 0000000000..3b767d8bbe --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/Stickers.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "GalleryEmbeddedStickersIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "GalleryEmbeddedStickersIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/Stickers.imageset/GalleryEmbeddedStickersIcon@2x.png b/submodules/TelegramUI/Images.xcassets/Media Gallery/Stickers.imageset/GalleryEmbeddedStickersIcon@2x.png new file mode 100644 index 0000000000..dce30d8177 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Gallery/Stickers.imageset/GalleryEmbeddedStickersIcon@2x.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/Stickers.imageset/GalleryEmbeddedStickersIcon@3x.png b/submodules/TelegramUI/Images.xcassets/Media Gallery/Stickers.imageset/GalleryEmbeddedStickersIcon@3x.png new file mode 100644 index 0000000000..6fe4d5e5c3 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Gallery/Stickers.imageset/GalleryEmbeddedStickersIcon@3x.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonAddMember.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonAddMember.imageset/Contents.json new file mode 100644 index 0000000000..196b36b491 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonAddMember.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_addmember.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonAddMember.imageset/ic_pf_addmember.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonAddMember.imageset/ic_pf_addmember.pdf new file mode 100644 index 0000000000..ad2274b415 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonAddMember.imageset/ic_pf_addmember.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonCall.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonCall.imageset/Contents.json new file mode 100644 index 0000000000..94b8f3fdef --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonCall.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_call.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonCall.imageset/ic_pf_call.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonCall.imageset/ic_pf_call.pdf new file mode 100644 index 0000000000..6fdc5ac345 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonCall.imageset/ic_pf_call.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonLeave.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonLeave.imageset/Contents.json new file mode 100644 index 0000000000..46ec950a2e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonLeave.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_leave.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonLeave.imageset/ic_pf_leave.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonLeave.imageset/ic_pf_leave.pdf new file mode 100644 index 0000000000..aa85ce0619 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonLeave.imageset/ic_pf_leave.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/Contents.json new file mode 100644 index 0000000000..c1007d847a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_message.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/ic_pf_message.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/ic_pf_message.pdf new file mode 100644 index 0000000000..64fe2bdbd1 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/ic_pf_message.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMore.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMore.imageset/Contents.json new file mode 100644 index 0000000000..176c3d211d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMore.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_more.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMore.imageset/ic_pf_more.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMore.imageset/ic_pf_more.pdf new file mode 100644 index 0000000000..a105960756 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMore.imageset/ic_pf_more.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMute.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMute.imageset/Contents.json new file mode 100644 index 0000000000..61322e832d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMute.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_mute.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMute.imageset/ic_pf_mute.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMute.imageset/ic_pf_mute.pdf new file mode 100644 index 0000000000..2cabbef68d Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMute.imageset/ic_pf_mute.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonSearch.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonSearch.imageset/Contents.json new file mode 100644 index 0000000000..9b09c7c13b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonSearch.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_search.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonSearch.imageset/ic_search.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonSearch.imageset/ic_search.pdf new file mode 100644 index 0000000000..ece31c0048 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonSearch.imageset/ic_search.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonUnmute.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonUnmute.imageset/Contents.json new file mode 100644 index 0000000000..29259c8539 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonUnmute.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_unmute.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonUnmute.imageset/ic_pf_unmute.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonUnmute.imageset/ic_pf_unmute.pdf new file mode 100644 index 0000000000..617622f59a Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonUnmute.imageset/ic_pf_unmute.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/VerifiedIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/VerifiedIcon.imageset/Contents.json new file mode 100644 index 0000000000..55305eb8ca --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/VerifiedIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_verify_big.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/VerifiedIcon.imageset/ic_verify_big.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/VerifiedIcon.imageset/ic_verify_big.pdf new file mode 100644 index 0000000000..e368606b86 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/VerifiedIcon.imageset/ic_verify_big.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Websites.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Websites.imageset/Contents.json new file mode 100644 index 0000000000..82fd0dc674 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Websites.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_activewebsites@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_activewebsites@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Websites.imageset/ic_activewebsites@2x.png b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Websites.imageset/ic_activewebsites@2x.png new file mode 100644 index 0000000000..6cbee74526 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Websites.imageset/ic_activewebsites@2x.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Websites.imageset/ic_activewebsites@3x.png b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Websites.imageset/ic_activewebsites@3x.png new file mode 100644 index 0000000000..a03840b7c3 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Websites.imageset/ic_activewebsites@3x.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Spark.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Spark.imageset/Contents.json new file mode 100644 index 0000000000..db4ac78550 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Spark.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "spark.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Spark.imageset/spark.png b/submodules/TelegramUI/Images.xcassets/Settings/Spark.imageset/spark.png new file mode 100644 index 0000000000..4565288ae2 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/Spark.imageset/spark.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorAddIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorAddIcon.imageset/Contents.json new file mode 100644 index 0000000000..02e013a01c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorAddIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_input_add.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorAddIcon.imageset/ic_input_add.pdf b/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorAddIcon.imageset/ic_input_add.pdf new file mode 100644 index 0000000000..8c29c43c79 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorAddIcon.imageset/ic_input_add.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorRemoveIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorRemoveIcon.imageset/Contents.json new file mode 100644 index 0000000000..70373c3a41 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorRemoveIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_input_close.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorRemoveIcon.imageset/ic_input_close.pdf b/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorRemoveIcon.imageset/ic_input_close.pdf new file mode 100644 index 0000000000..2c1b2eabc6 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorRemoveIcon.imageset/ic_input_close.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorRotateIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorRotateIcon.imageset/Contents.json new file mode 100644 index 0000000000..64dd68cd8b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorRotateIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_gradrotate.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorRotateIcon.imageset/ic_gradrotate.pdf b/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorRotateIcon.imageset/ic_gradrotate.pdf new file mode 100644 index 0000000000..79d0913c74 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorRotateIcon.imageset/ic_gradrotate.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorSwapIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorSwapIcon.imageset/Contents.json new file mode 100644 index 0000000000..8dec65adad --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorSwapIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_input_change.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorSwapIcon.imageset/ic_input_change.pdf b/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorSwapIcon.imageset/ic_input_change.pdf new file mode 100644 index 0000000000..6151d0db8b Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/ThemeColorSwapIcon.imageset/ic_input_change.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/TransferAuthLaptop.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/TransferAuthLaptop.imageset/Contents.json new file mode 100644 index 0000000000..c55acc952e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/TransferAuthLaptop.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "EmojiComputer.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/TransferAuthLaptop.imageset/EmojiComputer.pdf b/submodules/TelegramUI/Images.xcassets/Settings/TransferAuthLaptop.imageset/EmojiComputer.pdf new file mode 100644 index 0000000000..1bf7b79ae0 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/TransferAuthLaptop.imageset/EmojiComputer.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/WallpaperColorIcon.imageset/color@2x.png b/submodules/TelegramUI/Images.xcassets/Settings/WallpaperColorIcon.imageset/color@2x.png deleted file mode 100644 index ff0220da25..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Settings/WallpaperColorIcon.imageset/color@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/WallpaperColorIcon.imageset/color@3x.png b/submodules/TelegramUI/Images.xcassets/Settings/WallpaperColorIcon.imageset/color@3x.png deleted file mode 100644 index 7cd5551b17..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Settings/WallpaperColorIcon.imageset/color@3x.png and /dev/null differ diff --git a/submodules/TelegramUI/TelegramUI/AccountContext.swift b/submodules/TelegramUI/TelegramUI/AccountContext.swift index 5e0eb615b2..02fefc79c0 100644 --- a/submodules/TelegramUI/TelegramUI/AccountContext.swift +++ b/submodules/TelegramUI/TelegramUI/AccountContext.swift @@ -10,8 +10,10 @@ import TelegramPresentationData import AccountContext import LiveLocationManager import TemporaryCachedPeerDataManager +#if ENABLE_WALLET import WalletCore import WalletUI +#endif import PhoneNumberFormat private final class DeviceSpecificContactImportContext { @@ -106,7 +108,9 @@ public final class AccountContextImpl: AccountContext { } public let account: Account + #if ENABLE_WALLET public let tonContext: StoredTonContext? + #endif public let fetchManager: FetchManager private let prefetchManager: PrefetchManager? @@ -127,14 +131,22 @@ public final class AccountContextImpl: AccountContext { return self._limitsConfiguration.get() } + public var currentContentSettings: Atomic + private let _contentSettings = Promise() + public var contentSettings: Signal { + return self._contentSettings.get() + } + public var watchManager: WatchManager? private var storedPassword: (String, CFAbsoluteTime, SwiftSignalKit.Timer)? private var limitsConfigurationDisposable: Disposable? + private var contentSettingsDisposable: Disposable? private let deviceSpecificContactImportContexts: QueueLocalObject private var managedAppSpecificContactsDisposable: Disposable? + #if ENABLE_WALLET public var hasWallets: Signal { return WalletStorageInterfaceImpl(postbox: self.account.postbox).getWalletRecords() |> map { records in @@ -157,11 +169,15 @@ public final class AccountContextImpl: AccountContext { } |> distinctUntilChanged } + #endif - public init(sharedContext: SharedAccountContextImpl, account: Account, tonContext: StoredTonContext?, limitsConfiguration: LimitsConfiguration) { + public init(sharedContext: SharedAccountContextImpl, account: Account, /*tonContext: StoredTonContext?, */limitsConfiguration: LimitsConfiguration, contentSettings: ContentSettings, temp: Bool = false) + { self.sharedContextImpl = sharedContext self.account = account + #if ENABLE_WALLET self.tonContext = tonContext + #endif self.downloadedMediaStoreManager = DownloadedMediaStoreManagerImpl(postbox: account.postbox, accountManager: sharedContext.accountManager) @@ -171,7 +187,7 @@ public final class AccountContextImpl: AccountContext { self.liveLocationManager = nil } self.fetchManager = FetchManagerImpl(postbox: account.postbox, storeManager: self.downloadedMediaStoreManager) - if sharedContext.applicationBindings.isMainApp { + if sharedContext.applicationBindings.isMainApp && !temp { self.prefetchManager = PrefetchManager(sharedContext: sharedContext, account: account, fetchManager: self.fetchManager) self.wallpaperUploadManager = WallpaperUploadManagerImpl(sharedContext: sharedContext, account: account, presentationData: sharedContext.presentationData) self.themeUpdateManager = ThemeUpdateManagerImpl(sharedContext: sharedContext, account: account) @@ -195,6 +211,16 @@ public final class AccountContextImpl: AccountContext { let _ = currentLimitsConfiguration.swap(value) }) + let updatedContentSettings = getContentSettings(postbox: account.postbox) + self.currentContentSettings = Atomic(value: contentSettings) + self._contentSettings.set(.single(contentSettings) |> then(updatedContentSettings)) + + let currentContentSettings = self.currentContentSettings + self.contentSettingsDisposable = (self._contentSettings.get() + |> deliverOnMainQueue).start(next: { value in + let _ = currentContentSettings.swap(value) + }) + let queue = Queue() self.deviceSpecificContactImportContexts = QueueLocalObject(queue: queue, generate: { return DeviceSpecificContactImportContexts(queue: queue) @@ -214,6 +240,7 @@ public final class AccountContextImpl: AccountContext { deinit { self.limitsConfigurationDisposable?.dispose() self.managedAppSpecificContactsDisposable?.dispose() + self.contentSettingsDisposable?.dispose() } public func storeSecureIdPassword(password: String) { diff --git a/submodules/TelegramUI/TelegramUI/AppDelegate.swift b/submodules/TelegramUI/TelegramUI/AppDelegate.swift index 444072d94a..6b6d402bb3 100644 --- a/submodules/TelegramUI/TelegramUI/AppDelegate.swift +++ b/submodules/TelegramUI/TelegramUI/AppDelegate.swift @@ -5,7 +5,6 @@ import TelegramCore import SyncCore import UserNotifications import Intents -import HockeySDK import Postbox import PushKit import AsyncDisplayKit @@ -26,14 +25,20 @@ import WatchBridge import LegacyDataImport import SettingsUI import AppBundle +#if ENABLE_WALLET import WalletUI +#endif import UrlHandling +#if ENABLE_WALLET import WalletUrl import WalletCore +#endif import OpenSSLEncryptionProvider import AppLock import PresentationDataUtils -import AppIntents +import TelegramIntents +import AccountUtils +import CoreSpotlight #if canImport(BackgroundTasks) import BackgroundTasks @@ -155,12 +160,13 @@ final class SharedApplicationContext { } } -@objc(AppDelegate) class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate, BITHockeyManagerDelegate, UNUserNotificationCenterDelegate, UIAlertViewDelegate { +@objc(AppDelegate) class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate, UNUserNotificationCenterDelegate, UIAlertViewDelegate { @objc var window: UIWindow? var nativeWindow: (UIWindow & WindowHost)? var mainWindow: Window1! private var dataImportSplash: LegacyDataImportSplash? + private var buildConfig: BuildConfig? let episodeId = arc4random() private let isInForegroundPromise = ValuePromise(false, ignoreRepeated: true) @@ -182,6 +188,7 @@ final class SharedApplicationContext { private let logoutDisposable = MetaDisposable() + private let openNotificationSettingsWhenReadyDisposable = MetaDisposable() private let openChatWhenReadyDisposable = MetaDisposable() private let openUrlWhenReadyDisposable = MetaDisposable() @@ -366,12 +373,27 @@ final class SharedApplicationContext { let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) let buildConfig = BuildConfig(baseAppBundleId: baseAppBundleId) + self.buildConfig = buildConfig let signatureDict = BuildConfigExtra.signatureDict() let apiId: Int32 = buildConfig.apiId + let apiHash: String = buildConfig.apiHash let languagesCategory = "ios" - let networkArguments = NetworkInitializationArguments(apiId: apiId, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: PresentationCallManagerImpl.voipMaxLayer, appData: self.deviceToken.get() + let autolockDeadine: Signal + if #available(iOS 10.0, *) { + autolockDeadine = .single(nil) + } else { + autolockDeadine = self.context.get() + |> mapToSignal { context -> Signal in + guard let context = context else { + return .single(nil) + } + return context.context.sharedContext.appLockContext.autolockDeadline + } + } + + let networkArguments = NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: PresentationCallManagerImpl.voipMaxLayer, appData: self.deviceToken.get() |> map { token in let data = buildConfig.bundleData(withAppToken: token, signatureDict: signatureDict) if let data = data, let jsonString = String(data: data, encoding: .utf8) { @@ -380,7 +402,7 @@ final class SharedApplicationContext { Logger.shared.log("data", "can't deserialize") } return data - }, autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()) + }, autolockDeadine: autolockDeadine, encryptionProvider: OpenSSLEncryptionProvider()) guard let appGroupUrl = maybeAppGroupUrl else { UIAlertView(title: nil, message: "Error 2", delegate: nil, cancelButtonTitle: "OK").show() @@ -663,6 +685,7 @@ final class SharedApplicationContext { }) } + #if ENABLE_WALLET let tonKeychain: TonKeychain #if targetEnvironment(simulator) @@ -724,6 +747,7 @@ final class SharedApplicationContext { } }) #endif + #endif let sharedContextSignal = accountManagerSignal |> deliverOnMainQueue @@ -750,7 +774,7 @@ final class SharedApplicationContext { let legacyCache = LegacyCache(path: legacyBasePath + "/Caches") let presentationDataPromise = Promise() - let appLockContext = AppLockContextImpl(rootPath: rootPath, window: self.mainWindow!, applicationBindings: applicationBindings, accountManager: accountManager, presentationDataSignal: presentationDataPromise.get(), lockIconInitialFrame: { + let appLockContext = AppLockContextImpl(rootPath: rootPath, window: self.mainWindow!, rootController: self.window?.rootViewController, applicationBindings: applicationBindings, accountManager: accountManager, presentationDataSignal: presentationDataPromise.get(), lockIconInitialFrame: { return (self.mainWindow?.viewController as? TelegramRootController)?.chatListController?.lockViewFrame }) @@ -799,12 +823,12 @@ final class SharedApplicationContext { return } var exists = false - strongSelf.mainWindow.forEachViewController { controller in - if controller is ThemeSettingsCrossfadeController { + strongSelf.mainWindow.forEachViewController({ controller in + if controller is ThemeSettingsCrossfadeController || controller is ThemeSettingsController { exists = true } return true - } + }) if !exists { strongSelf.mainWindow.present(ThemeSettingsCrossfadeController(), on: .root) @@ -958,15 +982,25 @@ final class SharedApplicationContext { } return true }) - |> mapToSignal { account -> Signal<(Account, LimitsConfiguration, CallListSettings)?, NoError> in - return sharedApplicationContext.sharedContext.accountManager.transaction { transaction -> CallListSettings in - return transaction.getSharedData(ApplicationSpecificSharedDataKeys.callListSettings) as? CallListSettings ?? CallListSettings.defaultSettings + |> mapToSignal { account -> Signal<(Account, LimitsConfiguration, CallListSettings, ContentSettings)?, NoError> in + return sharedApplicationContext.sharedContext.accountManager.transaction { transaction -> CallListSettings? in + return transaction.getSharedData(ApplicationSpecificSharedDataKeys.callListSettings) as? CallListSettings } - |> mapToSignal { callListSettings -> Signal<(Account, LimitsConfiguration, CallListSettings)?, NoError> in + |> reduceLeft(value: nil) { current, updated -> CallListSettings? in + var result: CallListSettings? + if let updated = updated { + result = updated + } else if let current = current { + result = current + } + return result + } + |> mapToSignal { callListSettings -> Signal<(Account, LimitsConfiguration, CallListSettings, ContentSettings)?, NoError> in if let account = account { - return account.postbox.transaction { transaction -> (Account, LimitsConfiguration, CallListSettings)? in + return account.postbox.transaction { transaction -> (Account, LimitsConfiguration, CallListSettings, ContentSettings)? in let limitsConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.limitsConfiguration) as? LimitsConfiguration ?? LimitsConfiguration.defaultValue - return (account, limitsConfiguration, callListSettings) + let contentSettings = getContentSettings(transaction: transaction) + return (account, limitsConfiguration, callListSettings ?? CallListSettings.defaultSettings, contentSettings) } } else { return .single(nil) @@ -975,9 +1009,11 @@ final class SharedApplicationContext { } |> deliverOnMainQueue |> map { accountAndSettings -> AuthorizedApplicationContext? in - return accountAndSettings.flatMap { account, limitsConfiguration, callListSettings in + return accountAndSettings.flatMap { account, limitsConfiguration, callListSettings, contentSettings in + #if ENABLE_WALLET let tonContext = StoredTonContext(basePath: account.basePath, postbox: account.postbox, network: account.network, keychain: tonKeychain) - let context = AccountContextImpl(sharedContext: sharedApplicationContext.sharedContext, account: account, tonContext: tonContext, limitsConfiguration: limitsConfiguration) + #endif + let context = AccountContextImpl(sharedContext: sharedApplicationContext.sharedContext, account: account/*, tonContext: tonContext*/, limitsConfiguration: limitsConfiguration, contentSettings: contentSettings) return AuthorizedApplicationContext(sharedApplicationContext: sharedApplicationContext, mainWindow: self.mainWindow, watchManagerArguments: watchManagerArgumentsPromise.get(), context: context, accountManager: sharedApplicationContext.sharedContext.accountManager, showCallsTab: callListSettings.showTab, reinitializedNotificationSettings: { let _ = (self.context.get() |> take(1) @@ -1129,6 +1165,8 @@ final class SharedApplicationContext { self.registerForNotifications(context: context.context, authorize: authorizeNotifications) self.resetIntentsIfNeeded(context: context.context) + + let _ = storeCurrentCallListTabDefaultValue(accountManager: context.context.sharedContext.accountManager).start() })) } else { self.mainWindow.viewController = nil @@ -1187,7 +1225,7 @@ final class SharedApplicationContext { self.logoutDisposable.set((self.sharedContextPromise.get() |> take(1) - |> mapToSignal { sharedContext -> Signal, NoError> in + |> mapToSignal { sharedContext -> Signal<(AccountManager, Set), NoError> in return sharedContext.sharedContext.activeAccounts |> map { _, accounts, _ -> Set in return Set(accounts.map { $0.1.peerId }) @@ -1198,10 +1236,25 @@ final class SharedApplicationContext { } return updated } - }).start(next: { loggedOutAccountPeerIds in - for peerId in loggedOutAccountPeerIds { - deleteAllSendMessageIntents(accountPeerId: peerId) + |> map { loggedOutAccountPeerIds -> (AccountManager, Set) in + return (sharedContext.sharedContext.accountManager, loggedOutAccountPeerIds) } + }).start(next: { [weak self] accountManager, loggedOutAccountPeerIds in + guard let strongSelf = self else { + return + } + + let _ = (updateIntentsSettingsInteractively(accountManager: accountManager) { current in + var updated = current + for peerId in loggedOutAccountPeerIds { + if peerId == updated.account { + deleteAllSendMessageIntents() + updated = updated.withUpdatedAccount(nil) + break + } + } + return updated + }).start() })) self.watchCommunicationManagerPromise.set(watchCommunicationManager(context: self.context.get() |> flatMap { WatchCommunicationManagerContext(context: $0.context) }, allowBackgroundTimeExtension: { timeout in @@ -1242,7 +1295,22 @@ final class SharedApplicationContext { |> mapToSignal { context -> Signal<[ApplicationShortcutItem], NoError> in if let context = context { let presentationData = context.context.sharedContext.currentPresentationData.with { $0 } - return .single(applicationShortcutItems(strings: presentationData.strings)) + + return activeAccountsAndPeers(context: context.context) + |> take(1) + |> map { primaryAndAccounts -> (Account, Peer, Int32)? in + return primaryAndAccounts.1.first + } + |> map { accountAndPeer -> String? in + if let (_, peer, _) = accountAndPeer { + return peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + } else { + return nil + } + } |> mapToSignal { otherAccountName -> Signal<[ApplicationShortcutItem], NoError> in + let presentationData = context.context.sharedContext.currentPresentationData.with { $0 } + return .single(applicationShortcutItems(strings: presentationData.strings, otherAccountName: otherAccountName)) + } } else { return .single([]) } @@ -1279,52 +1347,11 @@ final class SharedApplicationContext { self.isActivePromise.set(true) } - BITHockeyBaseManager.setPresentAlert({ [weak self] alert in - if let strongSelf = self, let alert = alert { - var actions: [TextAlertAction] = [] - for action in alert.actions { - let isDefault = action.style == .default - actions.append(TextAlertAction(type: isDefault ? .defaultAction : .genericAction, title: action.title ?? "", action: { - if let action = action as? BITAlertAction { - action.invokeAction() - } - })) - } - if let sharedContext = strongSelf.contextValue?.context.sharedContext { - let presentationData = sharedContext.currentPresentationData.with { $0 } - strongSelf.mainWindow.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: alert.title, text: alert.message ?? "", actions: actions), on: .root) - } - } - }) - - BITHockeyBaseManager.setPresentView({ [weak self] controller in - if let strongSelf = self, let controller = controller { - let parent = LegacyController(presentation: .modal(animateIn: true), theme: nil) - let navigationController = UINavigationController(rootViewController: controller) - controller.navigation_setDismiss({ [weak parent] in - parent?.dismiss() - }, rootController: nil) - parent.bind(controller: navigationController) - strongSelf.mainWindow.present(parent, on: .root) - } - }) - - if let hockeyAppId = buildConfig.hockeyAppId, !hockeyAppId.isEmpty { - BITHockeyManager.shared().configure(withIdentifier: hockeyAppId, delegate: self) - BITHockeyManager.shared().crashManager.crashManagerStatus = .alwaysAsk - BITHockeyManager.shared().start() - #if targetEnvironment(simulator) - #else - BITHockeyManager.shared().authenticator.authenticateInstallation() - #endif - } - if UIApplication.shared.isStatusBarHidden { UIApplication.shared.setStatusBarHidden(false, with: .none) } - #if canImport(BackgroundTasks) - if #available(iOS 13.0, *) { + /*if #available(iOS 13.0, *) { BGTaskScheduler.shared.register(forTaskWithIdentifier: baseAppBundleId + ".refresh", using: nil, launchHandler: { task in let _ = (self.sharedContextPromise.get() |> take(1) @@ -1343,8 +1370,9 @@ final class SharedApplicationContext { }) }) }) - } - #endif + }*/ + + self.maybeCheckForUpdates() return true } @@ -1368,12 +1396,12 @@ final class SharedApplicationContext { } } } - self.mainWindow.forEachViewController { controller in + self.mainWindow.forEachViewController({ controller in if let controller = controller as? UndoOverlayController { controller.dismissWithCommitAction() } return true - } + }) } func applicationDidEnterBackground(_ application: UIApplication) { @@ -1429,6 +1457,8 @@ final class SharedApplicationContext { self.isInForegroundPromise.set(true) self.isActiveValue = true self.isActivePromise.set(true) + + self.maybeCheckForUpdates() } func applicationWillTerminate(_ application: UIApplication) { @@ -1673,21 +1703,24 @@ final class SharedApplicationContext { if let proxyData = parseProxyUrl(url) { authContext.rootController.view.endEditing(true) let presentationData = authContext.sharedContext.currentPresentationData.with { $0 } - let controller = ProxyServerActionSheetController(theme: presentationData.theme, strings: presentationData.strings, accountManager: authContext.sharedContext.accountManager, postbox: authContext.account.postbox, network: authContext.account.network, server: proxyData, presentationData: nil) + let controller = ProxyServerActionSheetController(presentationData: presentationData, accountManager: authContext.sharedContext.accountManager, postbox: authContext.account.postbox, network: authContext.account.network, server: proxyData, updatedPresentationData: nil) authContext.rootController.currentWindow?.present(controller, on: PresentationSurfaceLevel.root, blockInteraction: false, completion: {}) } else if let secureIdData = parseSecureIdUrl(url) { let presentationData = authContext.sharedContext.currentPresentationData.with { $0 } - authContext.rootController.currentWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.Passport_NotLoggedInMessage, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Calls_NotNow, action: { + authContext.rootController.currentWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Passport_NotLoggedInMessage, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Calls_NotNow, action: { if let callbackUrl = URL(string: secureIdCallbackUrl(with: secureIdData.callbackUrl, peerId: secureIdData.peerId, result: .cancel, parameters: [:])) { UIApplication.shared.openURL(callbackUrl) } }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), on: .root, blockInteraction: false, completion: {}) } else if let confirmationCode = parseConfirmationCodeUrl(url) { authContext.rootController.applyConfirmationCode(confirmationCode) - } else if let _ = parseWalletUrl(url) { - let presentationData = authContext.sharedContext.currentPresentationData.with { $0 } - authContext.rootController.currentWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "Please log in to your account to use Gram Wallet.", actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), on: .root, blockInteraction: false, completion: {}) } + #if ENABLE_WALLET + if let _ = parseWalletUrl(url) { + let presentationData = authContext.sharedContext.currentPresentationData.with { $0 } + authContext.rootController.currentWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "Please log in to your account to use Gram Wallet.", actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), on: .root, blockInteraction: false, completion: {}) + } + #endif } }) } @@ -1783,6 +1816,53 @@ final class SharedApplicationContext { self.openUrl(url: url) } + if userActivity.activityType == CSSearchableItemActionType { + if let uniqueIdentifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String, uniqueIdentifier.hasPrefix("contact-") { + if let peerIdValue = Int64(String(uniqueIdentifier[uniqueIdentifier.index(uniqueIdentifier.startIndex, offsetBy: "contact-".count)...])) { + let peerId = PeerId(peerIdValue) + + let signal = self.sharedContextPromise.get() + |> take(1) + |> mapToSignal { sharedApplicationContext -> Signal<(AccountRecordId?, [Account?]), NoError> in + return sharedApplicationContext.sharedContext.activeAccounts + |> take(1) + |> mapToSignal { primary, accounts, _ -> Signal<(AccountRecordId?, [Account?]), NoError> in + return combineLatest(accounts.map { _, account, _ -> Signal in + return account.postbox.transaction { transaction -> Account? in + if transaction.getPeer(peerId) != nil { + return account + } else { + return nil + } + } + }) + |> map { accounts -> (AccountRecordId?, [Account?]) in + return (primary?.id, accounts) + } + } + } + let _ = (signal + |> deliverOnMainQueue).start(next: { primary, accounts in + if let primary = primary { + for account in accounts { + if let account = account, account.id == primary { + self.openChatWhenReady(accountId: nil, peerId: peerId) + return + } + } + } + + for account in accounts { + if let account = account { + self.openChatWhenReady(accountId: account.id, peerId: peerId) + return + } + } + }) + } + } + } + return true } @@ -1791,27 +1871,31 @@ final class SharedApplicationContext { let _ = (self.sharedContextPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { sharedContext in + let type = ApplicationShortcutItemType(rawValue: shortcutItem.type) + var immediately = type == .account let proceed: () -> Void = { let _ = (self.context.get() |> take(1) |> deliverOnMainQueue).start(next: { context in if let context = context { - if let type = ApplicationShortcutItemType(rawValue: shortcutItem.type) { + if let type = type { switch type { - case .search: - context.openRootSearch() - case .compose: - context.openRootCompose() - case .camera: - context.openRootCamera() - case .savedMessages: - self.openChatWhenReady(accountId: nil, peerId: context.context.account.peerId) + case .search: + context.openRootSearch() + case .compose: + context.openRootCompose() + case .camera: + context.openRootCamera() + case .savedMessages: + self.openChatWhenReady(accountId: nil, peerId: context.context.account.peerId) + case .account: + context.switchAccount() } } } }) } - if let appLockContext = sharedContext.sharedContext.appLockContext as? AppLockContextImpl { + if let appLockContext = sharedContext.sharedContext.appLockContext as? AppLockContextImpl, !immediately { let _ = (appLockContext.isCurrentlyLocked |> filter { !$0 } |> take(1) @@ -1824,6 +1908,14 @@ final class SharedApplicationContext { }) } + private func openNotificationSettingsWhenReady() { + let signal = (self.authorizedContext() + |> take(1) + |> deliverOnMainQueue).start(next: { context in + context.openNotificationSettings() + }) + } + private func openChatWhenReady(accountId: AccountRecordId?, peerId: PeerId, messageId: MessageId? = nil, activateInput: Bool = false) { let signal = self.sharedContextPromise.get() |> take(1) @@ -1982,10 +2074,9 @@ final class SharedApplicationContext { var carPlayOptions = options carPlayOptions.insert(.allowInCarPlay) - - //if #available(iOS 13.2, *) { - // carPlayOptions.insert(.allowAnnouncement) - //} + if #available(iOS 13.2, *) { + carPlayOptions.insert(.allowAnnouncement) + } unknownMessageCategory = UNNotificationCategory(identifier: "unknown", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options) replyMessageCategory = UNNotificationCategory(identifier: "withReply", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: carPlayOptions) @@ -2081,7 +2172,7 @@ final class SharedApplicationContext { @available(iOS 12.0, *) func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) { - + self.openNotificationSettingsWhenReady() } func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { @@ -2089,6 +2180,60 @@ final class SharedApplicationContext { completionHandler() } + private var lastCheckForUpdatesTimestamp: Double? + private let currentCheckForUpdatesDisposable = MetaDisposable() + + private func maybeCheckForUpdates() { + #if targetEnvironment(simulator) + return; + #endif + + guard let buildConfig = self.buildConfig, !buildConfig.isAppStoreBuild, let appCenterId = buildConfig.appCenterId, !appCenterId.isEmpty else { + return + } + let timestamp = CFAbsoluteTimeGetCurrent() + if self.lastCheckForUpdatesTimestamp == nil || self.lastCheckForUpdatesTimestamp! < timestamp - 10.0 * 60.0 { + self.lastCheckForUpdatesTimestamp = timestamp + + if let url = URL(string: "https://api.appcenter.ms/v0.1/public/sdk/apps/\(appCenterId)/releases/latest") { + self.currentCheckForUpdatesDisposable.set((downloadHTTPData(url: url) + |> deliverOnMainQueue).start(next: { [weak self] data in + guard let strongSelf = self else { + return + } + guard let json = try? JSONSerialization.jsonObject(with: data, options: []) else { + return + } + guard let dict = json as? [String: Any] else { + return + } + guard let versionString = dict["version"] as? String, let version = Int(versionString) else { + return + } + guard let releaseNotesUrl = dict["release_notes_url"] as? String else { + return + } + guard let currentVersionString = Bundle.main.infoDictionary?["CFBundleVersion"] as? String, let currentVersion = Int(currentVersionString) else { + return + } + if currentVersion < version { + let _ = (strongSelf.sharedContextPromise.get() + |> take(1) + |> deliverOnMainQueue).start(next: { sharedContext in + let presentationData = sharedContext.sharedContext.currentPresentationData.with { $0 } + sharedContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "A new build is available", actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: "Show", action: { + sharedContext.sharedContext.applicationBindings.openUrl(releaseNotesUrl) + }) + ]), on: .root, blockInteraction: false, completion: {}) + }) + } + })) + } + } + } + override var next: UIResponder? { if let context = self.contextValue, let controller = context.context.keyShortcutsController { return controller @@ -2113,14 +2258,14 @@ final class SharedApplicationContext { private func resetIntentsIfNeeded(context: AccountContextImpl) { let _ = (context.sharedContext.accountManager.transaction { transaction in let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.intentsSettings) as? IntentsSettings ?? IntentsSettings.defaultSettings - if !settings.initiallyReset { + if !settings.initiallyReset || settings.account == nil { if #available(iOS 10.0, *) { Queue.mainQueue().async { INInteraction.deleteAll() } } - transaction.updateSharedData(ApplicationSpecificSharedDataKeys.intentsSettings, { entry in - return IntentsSettings(initiallyReset: true) + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.intentsSettings, { _ in + return IntentsSettings(initiallyReset: true, account: context.account.peerId, contacts: settings.contacts, privateChats: settings.privateChats, savedMessages: settings.savedMessages, groups: settings.groups, onlyShared: settings.onlyShared) }) } }).start() @@ -2230,3 +2375,29 @@ private func messageIdFromNotification(peerId: PeerId, notification: UNNotificat } return nil } + +private enum DownloadFileError { + case network +} + +private func downloadHTTPData(url: URL) -> Signal { + return Signal { subscriber in + let completed = Atomic(value: false) + let downloadTask = URLSession.shared.downloadTask(with: url, completionHandler: { location, _, error in + let _ = completed.swap(true) + if let location = location, let data = try? Data(contentsOf: location) { + subscriber.putNext(data) + subscriber.putCompletion() + } else { + subscriber.putError(.network) + } + }) + downloadTask.resume() + + return ActionDisposable { + if !completed.with({ $0 }) { + downloadTask.cancel() + } + } + } +} diff --git a/submodules/TelegramUI/TelegramUI/ApplicationContext.swift b/submodules/TelegramUI/TelegramUI/ApplicationContext.swift index 95f59825b2..eeaa5b2e6d 100644 --- a/submodules/TelegramUI/TelegramUI/ApplicationContext.swift +++ b/submodules/TelegramUI/TelegramUI/ApplicationContext.swift @@ -22,6 +22,8 @@ import ImageBlur import WatchBridge import SettingsUI import AppLock +import AccountUtils +import ContextUI final class UnauthorizedApplicationContext { let sharedContext: SharedAccountContextImpl @@ -40,7 +42,7 @@ final class UnauthorizedApplicationContext { var authorizationCompleted: (() -> Void)? - self.rootController = AuthorizationSequenceController(sharedContext: sharedContext, account: account, otherAccountPhoneNumbers: otherAccountPhoneNumbers, strings: presentationData.strings, theme: presentationData.theme, openUrl: sharedContext.applicationBindings.openUrl, apiId: apiId, apiHash: apiHash, authorizationCompleted: { + self.rootController = AuthorizationSequenceController(sharedContext: sharedContext, account: account, otherAccountPhoneNumbers: otherAccountPhoneNumbers, presentationData: presentationData, openUrl: sharedContext.applicationBindings.openUrl, apiId: apiId, apiHash: apiHash, authorizationCompleted: { authorizationCompleted?() }) @@ -57,6 +59,16 @@ final class UnauthorizedApplicationContext { return .never } }) + + DeviceAccess.authorizeAccess(to: .cellularData, presentationData: sharedContext.currentPresentationData.with { $0 }, present: { [weak self] c, a in + if let strongSelf = self { + (strongSelf.rootController.viewControllers.last as? ViewController)?.present(c, in: .window(.root)) + } + }, openSettings: { + sharedContext.applicationBindings.openSettings() + }, { result in + ApplicationSpecificNotice.setPermissionWarning(accountManager: sharedContext.accountManager, permission: .cellularData, value: 0) + }) } } @@ -70,6 +82,7 @@ final class AuthorizedApplicationContext { let rootController: TelegramRootController let notificationController: NotificationContainerController + private var scheduledOpenNotificationSettings: Bool = false private var scheduledOperChatWithPeerId: (PeerId, MessageId?, Bool)? private var scheduledOpenExternalUrl: URL? @@ -137,6 +150,21 @@ final class AuthorizedApplicationContext { self.rootController = TelegramRootController(context: context) + self.rootController.globalOverlayControllersUpdated = { [weak self] in + guard let strongSelf = self else { + return + } + var hasContext = false + for controller in strongSelf.rootController.globalOverlayControllers { + if controller is ContextController { + hasContext = true + break + } + } + + strongSelf.notificationController.updateIsTemporaryHidden(hasContext) + } + if KeyShortcutsController.isAvailable { let keyShortcutsController = KeyShortcutsController { [weak self] f in if let strongSelf = self, let appLockContext = strongSelf.context.sharedContext.appLockContext as? AppLockContextImpl { @@ -148,9 +176,31 @@ final class AuthorizedApplicationContext { } if let tabController = strongSelf.rootController.rootTabController { let selectedController = tabController.controllers[tabController.selectedIndex] - if !f(selectedController) { - return + + if let index = strongSelf.rootController.viewControllers.lastIndex(where: { controller in + guard let controller = controller as? ViewController else { + return false + } + if controller === tabController { + return false + } + switch controller.navigationPresentation { + case .master: + return true + default: + break + } + return false + }), let controller = strongSelf.rootController.viewControllers[index] as? ViewController { + if !f(controller) { + return + } + } else { + if !f(selectedController) { + return + } } + if let controller = strongSelf.rootController.topViewController as? ViewController, controller !== selectedController { if !f(controller) { return @@ -283,7 +333,7 @@ final class AuthorizedApplicationContext { return false } return true - }) + }, excludeNavigationSubControllers: true) if foundOverlay { return true @@ -312,11 +362,11 @@ final class AuthorizedApplicationContext { return false }, expandAction: { expandData in if let strongSelf = self { - let chatController = ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(firstMessage.id.peerId), mode: .overlay) + let chatController = ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(firstMessage.id.peerId), mode: .overlay(strongSelf.rootController)) //chatController.navigation_setNavigationController(strongSelf.rootController) chatController.presentationArguments = ChatControllerOverlayPresentationData(expandData: expandData()) - strongSelf.rootController.pushViewController(chatController) - //(strongSelf.rootController.viewControllers.last as? ViewController)?.present(chatController, in: .window(.root), with: ChatControllerOverlayPresentationData(expandData: expandData())) + //strongSelf.rootController.pushViewController(chatController) + (strongSelf.rootController.viewControllers.last as? ViewController)?.present(chatController, in: .window(.root), with: ChatControllerOverlayPresentationData(expandData: expandData())) } })) } @@ -335,7 +385,7 @@ final class AuthorizedApplicationContext { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } var acceptImpl: ((String?) -> Void)? var declineImpl: (() -> Void)? - let controller = TermsOfServiceController(theme: TermsOfServiceControllerTheme(presentationTheme: presentationData.theme), strings: presentationData.strings, text: termsOfServiceUpdate.text, entities: termsOfServiceUpdate.entities, ageConfirmation: termsOfServiceUpdate.ageConfirmation, signingUp: false, accept: { proccedBot in + let controller = TermsOfServiceController(presentationData: presentationData, text: termsOfServiceUpdate.text, entities: termsOfServiceUpdate.entities, ageConfirmation: termsOfServiceUpdate.ageConfirmation, signingUp: false, accept: { proccedBot in acceptImpl?(proccedBot) }, decline: { declineImpl?() @@ -609,7 +659,7 @@ final class AuthorizedApplicationContext { let showCallsTabSignal = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.callListSettings]) |> map { sharedData -> Bool in - var value = true + var value = CallListSettings.defaultSettings.showTab if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.callListSettings] as? CallListSettings { value = settings.showTab } @@ -685,6 +735,14 @@ final class AuthorizedApplicationContext { self.permissionsDisposable.dispose() } + func openNotificationSettings() { + if self.rootController.rootTabController != nil { + self.rootController.pushViewController(notificationsAndSoundsController(context: self.context, exceptionsList: nil)) + } else { + self.scheduledOpenNotificationSettings = true + } + } + func openChatWithPeerId(peerId: PeerId, messageId: MessageId? = nil, activateInput: Bool = false) { var visiblePeerId: PeerId? if let controller = self.rootController.topViewController as? ChatControllerImpl, case let .peer(peerId) = controller.chatLocation { @@ -723,6 +781,27 @@ final class AuthorizedApplicationContext { self.rootController.openRootCamera() } + func switchAccount() { + let _ = (activeAccountsAndPeers(context: self.context) + |> take(1) + |> map { primaryAndAccounts -> (Account, Peer, Int32)? in + return primaryAndAccounts.1.first + } + |> map { accountAndPeer -> Account? in + if let (account, _, _) = accountAndPeer { + return account + } else { + return nil + } + } + |> deliverOnMainQueue).start(next: { [weak self] account in + guard let strongSelf = self, let account = account else { + return + } + strongSelf.context.sharedContext.switchToAccount(id: account.id, fromSettingsController: nil, withChatListController: nil) + }) + } + private func updateCoveringViewSnaphot(_ visible: Bool) { if visible { let scale: CGFloat = 0.5 diff --git a/submodules/TelegramUI/TelegramUI/ApplicationShortcutItem.swift b/submodules/TelegramUI/TelegramUI/ApplicationShortcutItem.swift index 7746184822..ad690d29d0 100644 --- a/submodules/TelegramUI/TelegramUI/ApplicationShortcutItem.swift +++ b/submodules/TelegramUI/TelegramUI/ApplicationShortcutItem.swift @@ -7,11 +7,13 @@ enum ApplicationShortcutItemType: String { case compose case camera case savedMessages + case account } struct ApplicationShortcutItem: Equatable { let type: ApplicationShortcutItemType let title: String + let subtitle: String? } @available(iOS 9.1, *) @@ -27,16 +29,27 @@ extension ApplicationShortcutItem { icon = UIApplicationShortcutIcon(templateImageName: "Shortcuts/Camera") case .savedMessages: icon = UIApplicationShortcutIcon(templateImageName: "Shortcuts/SavedMessages") + case .account: + icon = UIApplicationShortcutIcon(templateImageName: "Shortcuts/Account") } - return UIApplicationShortcutItem(type: self.type.rawValue, localizedTitle: self.title, localizedSubtitle: nil, icon: icon, userInfo: nil) + return UIApplicationShortcutItem(type: self.type.rawValue, localizedTitle: self.title, localizedSubtitle: self.subtitle, icon: icon, userInfo: nil) } } -func applicationShortcutItems(strings: PresentationStrings) -> [ApplicationShortcutItem] { - return [ - ApplicationShortcutItem(type: .search, title: strings.Common_Search), - ApplicationShortcutItem(type: .compose, title: strings.Compose_NewMessage), - ApplicationShortcutItem(type: .camera, title: strings.Camera_Title), - ApplicationShortcutItem(type: .savedMessages, title: strings.Conversation_SavedMessages) - ] +func applicationShortcutItems(strings: PresentationStrings, otherAccountName: String?) -> [ApplicationShortcutItem] { + if let otherAccountName = otherAccountName { + return [ + ApplicationShortcutItem(type: .search, title: strings.Common_Search, subtitle: nil), + ApplicationShortcutItem(type: .compose, title: strings.Compose_NewMessage, subtitle: nil), + ApplicationShortcutItem(type: .savedMessages, title: strings.Conversation_SavedMessages, subtitle: nil), + ApplicationShortcutItem(type: .account, title: strings.Shortcut_SwitchAccount, subtitle: otherAccountName) + ] + } else { + return [ + ApplicationShortcutItem(type: .search, title: strings.Common_Search, subtitle: nil), + ApplicationShortcutItem(type: .compose, title: strings.Compose_NewMessage, subtitle: nil), + ApplicationShortcutItem(type: .camera, title: strings.Camera_Title, subtitle: nil), + ApplicationShortcutItem(type: .savedMessages, title: strings.Conversation_SavedMessages, subtitle: nil) + ] + } } diff --git a/submodules/TelegramUI/TelegramUI/AuthorizationSequenceCodeEntryController.swift b/submodules/TelegramUI/TelegramUI/AuthorizationSequenceCodeEntryController.swift index c830768a85..8841d92eb3 100644 --- a/submodules/TelegramUI/TelegramUI/AuthorizationSequenceCodeEntryController.swift +++ b/submodules/TelegramUI/TelegramUI/AuthorizationSequenceCodeEntryController.swift @@ -37,9 +37,9 @@ final class AuthorizationSequenceCodeEntryController: ViewController { } } - init(strings: PresentationStrings, theme: PresentationTheme, openUrl: @escaping (String) -> Void, back: @escaping () -> Void) { - self.strings = strings - self.theme = theme + init(presentationData: PresentationData, openUrl: @escaping (String) -> Void, back: @escaping () -> Void) { + self.strings = presentationData.strings + self.theme = presentationData.theme self.openUrl = openUrl super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(theme), strings: NavigationBarStrings(presentationStrings: strings))) @@ -56,8 +56,8 @@ final class AuthorizationSequenceCodeEntryController: ViewController { return false } self.navigationBar?.backPressed = { [weak self] in - self?.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: theme), title: nil, text: strings.Login_CancelPhoneVerification, actions: [TextAlertAction(type: .genericAction, title: strings.Login_CancelPhoneVerificationContinue, action: { - }), TextAlertAction(type: .defaultAction, title: strings.Login_CancelPhoneVerificationStop, action: { + self?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_CancelPhoneVerification, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Login_CancelPhoneVerificationContinue, action: { + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Login_CancelPhoneVerificationStop, action: { back() })]), in: .window(.root)) } diff --git a/submodules/TelegramUI/TelegramUI/AuthorizationSequenceController.swift b/submodules/TelegramUI/TelegramUI/AuthorizationSequenceController.swift index 6ff0635478..9ef68b15a7 100644 --- a/submodules/TelegramUI/TelegramUI/AuthorizationSequenceController.swift +++ b/submodules/TelegramUI/TelegramUI/AuthorizationSequenceController.swift @@ -35,8 +35,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail private let otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]) private let apiId: Int32 private let apiHash: String - private var strings: PresentationStrings - public let theme: PresentationTheme + public var presentationData: PresentationData private let openUrl: (String) -> Void private let authorizationCompleted: () -> Void @@ -51,26 +50,25 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } private var didSetReady = false - public init(sharedContext: SharedAccountContext, account: UnauthorizedAccount, otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]), strings: PresentationStrings, theme: PresentationTheme, openUrl: @escaping (String) -> Void, apiId: Int32, apiHash: String, authorizationCompleted: @escaping () -> Void) { + public init(sharedContext: SharedAccountContext, account: UnauthorizedAccount, otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]), presentationData: PresentationData, openUrl: @escaping (String) -> Void, apiId: Int32, apiHash: String, authorizationCompleted: @escaping () -> Void) { self.sharedContext = sharedContext self.account = account self.otherAccountPhoneNumbers = otherAccountPhoneNumbers self.apiId = apiId self.apiHash = apiHash - self.strings = strings - self.theme = theme + self.presentationData = presentationData self.openUrl = openUrl self.authorizationCompleted = authorizationCompleted let navigationStatusBar: NavigationStatusBarStyle - switch theme.rootController.statusBarStyle { + switch presentationData.theme.rootController.statusBarStyle { case .black: navigationStatusBar = .black case .white: navigationStatusBar = .white } - super.init(mode: .single, theme: NavigationControllerTheme(statusBar: navigationStatusBar, navigationBar: AuthorizationSequenceController.navigationBarTheme(theme), emptyAreaColor: .black)) + super.init(mode: .single, theme: NavigationControllerTheme(statusBar: navigationStatusBar, navigationBar: AuthorizationSequenceController.navigationBarTheme(presentationData.theme), emptyAreaColor: .black)) self.stateDisposable = (account.postbox.stateView() |> map { view -> InnerState in @@ -99,7 +97,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail override public func loadView() { super.loadView() - self.view.backgroundColor = self.theme.list.plainBackgroundColor + self.view.backgroundColor = self.presentationData.theme.list.plainBackgroundColor } private func splashController() -> AuthorizationSequenceSplashController { @@ -114,11 +112,11 @@ public final class AuthorizationSequenceController: NavigationController, MFMail if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequenceSplashController(accountManager: self.sharedContext.accountManager, postbox: self.account.postbox, network: self.account.network, theme: self.theme) + controller = AuthorizationSequenceSplashController(accountManager: self.sharedContext.accountManager, postbox: self.account.postbox, network: self.account.network, theme: self.presentationData.theme) controller.nextPressed = { [weak self] strings in if let strongSelf = self { if let strings = strings { - strongSelf.strings = strings + strongSelf.presentationData = strongSelf.presentationData.withStrings(strings) } let masterDatacenterId = strongSelf.account.masterDatacenterId let isTestingEnvironment = strongSelf.account.testingEnvironment @@ -146,7 +144,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequencePhoneEntryController(sharedContext: self.sharedContext, isTestingEnvironment: self.account.testingEnvironment, otherAccountPhoneNumbers: self.otherAccountPhoneNumbers, network: self.account.network, strings: self.strings, theme: self.theme, openUrl: { [weak self] url in + controller = AuthorizationSequencePhoneEntryController(sharedContext: self.sharedContext, account: self.account, isTestingEnvironment: self.account.testingEnvironment, otherAccountPhoneNumbers: self.otherAccountPhoneNumbers, network: self.account.network, presentationData: self.presentationData, openUrl: { [weak self] url in self?.openUrl(url) }, back: { [weak self] in guard let strongSelf = self else { @@ -162,6 +160,12 @@ public final class AuthorizationSequenceController: NavigationController, MFMail }).start() } }) + controller.accountUpdated = { [weak self] updatedAccount in + guard let strongSelf = self else { + return + } + strongSelf.account = updatedAccount + } controller.loginWithNumber = { [weak self, weak controller] number, syncContacts in if let strongSelf = self { controller?.inProgress = true @@ -176,14 +180,14 @@ public final class AuthorizationSequenceController: NavigationController, MFMail let text: String var actions: [TextAlertAction] = [ - TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {}) + TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) ] switch error { case .limitExceeded: - text = strongSelf.strings.Login_CodeFloodError + text = strongSelf.presentationData.strings.Login_CodeFloodError case .invalidPhoneNumber: - text = strongSelf.strings.Login_InvalidPhoneError - actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.strings.Login_PhoneNumberHelp, action: { [weak controller] in + text = strongSelf.presentationData.strings.Login_InvalidPhoneError + actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in guard let strongSelf = self, let controller = controller else { return } @@ -194,13 +198,13 @@ public final class AuthorizationSequenceController: NavigationController, MFMail let carrier = CTCarrier() let mnc = carrier.mobileNetworkCode ?? "none" - strongSelf.presentEmailComposeController(address: "login@stel.com", subject: strongSelf.strings.Login_InvalidPhoneEmailSubject(formattedNumber).0, body: strongSelf.strings.Login_InvalidPhoneEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).0, from: controller) + strongSelf.presentEmailComposeController(address: "login@stel.com", subject: strongSelf.presentationData.strings.Login_InvalidPhoneEmailSubject(formattedNumber).0, body: strongSelf.presentationData.strings.Login_InvalidPhoneEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).0, from: controller) })) case .phoneLimitExceeded: - text = strongSelf.strings.Login_PhoneFloodError + text = strongSelf.presentationData.strings.Login_PhoneFloodError case .phoneBanned: - text = strongSelf.strings.Login_PhoneBannedError - actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.strings.Login_PhoneNumberHelp, action: { [weak controller] in + text = strongSelf.presentationData.strings.Login_PhoneBannedError + actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in guard let strongSelf = self, let controller = controller else { return } @@ -211,11 +215,11 @@ public final class AuthorizationSequenceController: NavigationController, MFMail let carrier = CTCarrier() let mnc = carrier.mobileNetworkCode ?? "none" - strongSelf.presentEmailComposeController(address: "login@stel.com", subject: strongSelf.strings.Login_PhoneBannedEmailSubject(formattedNumber).0, body: strongSelf.strings.Login_PhoneBannedEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).0, from: controller) + strongSelf.presentEmailComposeController(address: "login@stel.com", subject: strongSelf.presentationData.strings.Login_PhoneBannedEmailSubject(formattedNumber).0, body: strongSelf.presentationData.strings.Login_PhoneBannedEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).0, from: controller) })) case let .generic(info): - text = strongSelf.strings.Login_UnknownError - actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.strings.Login_PhoneNumberHelp, action: { [weak controller] in + text = strongSelf.presentationData.strings.Login_UnknownError + actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in guard let strongSelf = self, let controller = controller else { return } @@ -232,18 +236,18 @@ public final class AuthorizationSequenceController: NavigationController, MFMail errorString = "unknown" } - strongSelf.presentEmailComposeController(address: "login@stel.com", subject: strongSelf.strings.Login_PhoneGenericEmailSubject(formattedNumber).0, body: strongSelf.strings.Login_PhoneGenericEmailBody(formattedNumber, errorString, appVersion, systemVersion, locale, mnc).0, from: controller) + strongSelf.presentEmailComposeController(address: "login@stel.com", subject: strongSelf.presentationData.strings.Login_PhoneGenericEmailSubject(formattedNumber).0, body: strongSelf.presentationData.strings.Login_PhoneGenericEmailBody(formattedNumber, errorString, appVersion, systemVersion, locale, mnc).0, from: controller) })) case .timeout: - text = strongSelf.strings.Login_NetworkError - actions.append(TextAlertAction(type: .genericAction, title: strongSelf.strings.ChatSettings_ConnectionType_UseProxy, action: { [weak controller] in + text = strongSelf.presentationData.strings.Login_NetworkError + actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.ChatSettings_ConnectionType_UseProxy, action: { [weak controller] in guard let strongSelf = self, let controller = controller else { return } - controller.present(proxySettingsController(accountManager: strongSelf.sharedContext.accountManager, postbox: strongSelf.account.postbox, network: strongSelf.account.network, mode: .modal, theme: defaultPresentationTheme, strings: strongSelf.strings, updatedPresentationData: .single((defaultPresentationTheme, strongSelf.strings))), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + controller.present(proxySettingsController(accountManager: strongSelf.sharedContext.accountManager, postbox: strongSelf.account.postbox, network: strongSelf.account.network, mode: .modal, presentationData: strongSelf.sharedContext.currentPresentationData.with { $0 }, updatedPresentationData: strongSelf.sharedContext.presentationData), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) })) } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: nil, text: text, actions: actions), in: .window(.root)) + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: actions), in: .window(.root)) } })) } @@ -267,7 +271,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequenceCodeEntryController(strings: self.strings, theme: self.theme, openUrl: { [weak self] url in + controller = AuthorizationSequenceCodeEntryController(presentationData: self.presentationData, openUrl: { [weak self] url in self?.openUrl(url) }, back: { [weak self] in guard let strongSelf = self else { @@ -298,23 +302,23 @@ public final class AuthorizationSequenceController: NavigationController, MFMail return } var dismissImpl: (() -> Void)? - let alertTheme = AlertControllerTheme(presentationTheme: strongSelf.theme) + let alertTheme = AlertControllerTheme(presentationData: strongSelf.presentationData) let attributedText = stringWithAppliedEntities(termsOfService.text, entities: termsOfService.entities, baseColor: alertTheme.primaryColor, linkColor: alertTheme.accentColor, baseFont: Font.regular(13.0), linkFont: Font.regular(13.0), boldFont: Font.semibold(13.0), italicFont: Font.italic(13.0), boldItalicFont: Font.semiboldItalic(13.0), fixedFont: Font.regular(13.0), blockQuoteFont: Font.regular(13.0)) - let contentNode = TextAlertContentNode(theme: alertTheme, title: NSAttributedString(string: strongSelf.strings.Login_TermsOfServiceHeader, font: Font.medium(17.0), textColor: alertTheme.primaryColor, paragraphAlignment: .center), text: attributedText, actions: [ - TextAlertAction(type: .defaultAction, title: strongSelf.strings.Login_TermsOfServiceAgree, action: { + let contentNode = TextAlertContentNode(theme: alertTheme, title: NSAttributedString(string: strongSelf.presentationData.strings.Login_TermsOfServiceHeader, font: Font.medium(17.0), textColor: alertTheme.primaryColor, paragraphAlignment: .center), text: attributedText, actions: [ + TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_TermsOfServiceAgree, action: { dismissImpl?() guard let strongSelf = self else { return } let _ = beginSignUp(account: strongSelf.account, data: data).start() - }), TextAlertAction(type: .genericAction, title: strongSelf.strings.Login_TermsOfServiceDecline, action: { + }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Login_TermsOfServiceDecline, action: { dismissImpl?() guard let strongSelf = self else { return } - strongSelf.currentWindow?.present(standardTextAlertController(theme: alertTheme, title: strongSelf.strings.Login_TermsOfServiceDecline, text: strongSelf.strings.Login_TermsOfServiceSignupDecline, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_Cancel, action: { + strongSelf.currentWindow?.present(standardTextAlertController(theme: alertTheme, title: strongSelf.presentationData.strings.Login_TermsOfServiceDecline, text: strongSelf.presentationData.strings.Login_TermsOfServiceSignupDecline, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { presentAlertAgainImpl?() - }), TextAlertAction(type: .genericAction, title: strongSelf.strings.Login_TermsOfServiceDecline, action: { + }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Login_TermsOfServiceDecline, action: { guard let strongSelf = self else { return } @@ -355,20 +359,20 @@ public final class AuthorizationSequenceController: NavigationController, MFMail let text: String switch error { case .limitExceeded: - text = strongSelf.strings.Login_CodeFloodError + text = strongSelf.presentationData.strings.Login_CodeFloodError case .invalidCode: - text = strongSelf.strings.Login_InvalidCodeError + text = strongSelf.presentationData.strings.Login_InvalidCodeError case .generic: - text = strongSelf.strings.Login_UnknownError + text = strongSelf.presentationData.strings.Login_UnknownError case .codeExpired: - text = strongSelf.strings.Login_CodeExpired + text = strongSelf.presentationData.strings.Login_CodeExpired let account = strongSelf.account let _ = (strongSelf.account.postbox.transaction { transaction -> Void in transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)) }).start() } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } })) @@ -380,9 +384,9 @@ public final class AuthorizationSequenceController: NavigationController, MFMail if nextType == nil { if MFMailComposeViewController.canSendMail(), let controller = controller { let formattedNumber = formatPhoneNumber(number) - strongSelf.presentEmailComposeController(address: "sms@stel.com", subject: strongSelf.strings.Login_EmailCodeSubject(formattedNumber).0, body: strongSelf.strings.Login_EmailCodeBody(formattedNumber).0, from: controller) + strongSelf.presentEmailComposeController(address: "sms@stel.com", subject: strongSelf.presentationData.strings.Login_EmailCodeSubject(formattedNumber).0, body: strongSelf.presentationData.strings.Login_EmailCodeBody(formattedNumber).0, from: controller) } else { - controller?.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: nil, text: strongSelf.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + controller?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } else { controller?.inProgress = true @@ -396,20 +400,20 @@ public final class AuthorizationSequenceController: NavigationController, MFMail let text: String switch error { case .limitExceeded: - text = strongSelf.strings.Login_CodeFloodError + text = strongSelf.presentationData.strings.Login_CodeFloodError case .invalidPhoneNumber: - text = strongSelf.strings.Login_InvalidPhoneError + text = strongSelf.presentationData.strings.Login_InvalidPhoneError case .phoneLimitExceeded: - text = strongSelf.strings.Login_PhoneFloodError + text = strongSelf.presentationData.strings.Login_PhoneFloodError case .phoneBanned: - text = strongSelf.strings.Login_PhoneBannedError + text = strongSelf.presentationData.strings.Login_PhoneBannedError case .generic: - text = strongSelf.strings.Login_UnknownError + text = strongSelf.presentationData.strings.Login_UnknownError case .timeout: - text = strongSelf.strings.Login_NetworkError + text = strongSelf.presentationData.strings.Login_NetworkError } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } })) } @@ -439,7 +443,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequencePasswordEntryController(strings: self.strings, theme: self.theme, back: { [weak self] in + controller = AuthorizationSequencePasswordEntryController(presentationData: self.presentationData, back: { [weak self] in guard let strongSelf = self else { return } @@ -461,14 +465,14 @@ public final class AuthorizationSequenceController: NavigationController, MFMail let text: String switch error { case .limitExceeded: - text = strongSelf.strings.LoginPassword_FloodError + text = strongSelf.presentationData.strings.LoginPassword_FloodError case .invalidPassword: - text = strongSelf.strings.LoginPassword_InvalidPasswordError + text = strongSelf.presentationData.strings.LoginPassword_InvalidPasswordError case .generic: - text = strongSelf.strings.Login_UnknownError + text = strongSelf.presentationData.strings.Login_UnknownError } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) controller.passwordIsInvalid() } } @@ -491,7 +495,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } }).start() case .none: - strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: nil, text: strongSelf.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) strongController.didForgotWithNoRecovery = true } } @@ -504,9 +508,9 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } controller.reset = { [weak self, weak controller] in if let strongSelf = self, let strongController = controller { - strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: nil, text: suggestReset ? strongSelf.strings.TwoStepAuth_RecoveryFailed : strongSelf.strings.TwoStepAuth_RecoveryUnavailable, actions: [ - TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_Cancel, action: {}), - TextAlertAction(type: .destructiveAction, title: strongSelf.strings.Login_ResetAccountProtected_Reset, action: { + strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: suggestReset ? strongSelf.presentationData.strings.TwoStepAuth_RecoveryFailed : strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [ + TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Login_ResetAccountProtected_Reset, action: { if let strongSelf = self, let strongController = controller { strongController.inProgress = true strongSelf.actionDisposable.set((performAccountReset(account: strongSelf.account) @@ -520,11 +524,11 @@ public final class AuthorizationSequenceController: NavigationController, MFMail let text: String switch error { case .generic: - text = strongSelf.strings.Login_UnknownError + text = strongSelf.presentationData.strings.Login_UnknownError case .limitExceeded: - text = strongSelf.strings.Login_ResetAccountProtected_LimitExceeded + text = strongSelf.presentationData.strings.Login_ResetAccountProtected_LimitExceeded } - strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } })) } @@ -547,7 +551,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequencePasswordRecoveryController(strings: self.strings, theme: self.theme, back: { [weak self] in + controller = AuthorizationSequencePasswordRecoveryController(strings: self.presentationData.strings, theme: self.presentationData.theme, back: { [weak self] in guard let strongSelf = self else { return } @@ -569,14 +573,14 @@ public final class AuthorizationSequenceController: NavigationController, MFMail let text: String switch error { case .limitExceeded: - text = strongSelf.strings.LoginPassword_FloodError + text = strongSelf.presentationData.strings.LoginPassword_FloodError case .invalidCode: - text = strongSelf.strings.Login_InvalidCodeError + text = strongSelf.presentationData.strings.Login_InvalidCodeError case .expired: - text = strongSelf.strings.Login_CodeExpiredError + text = strongSelf.presentationData.strings.Login_CodeExpiredError } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } })) @@ -584,7 +588,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } controller.noAccess = { [weak self, weak controller] in if let strongSelf = self, let controller = controller { - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: nil, text: strongSelf.strings.TwoStepAuth_RecoveryFailed, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_RecoveryFailed, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) let account = strongSelf.account let _ = (strongSelf.account.postbox.transaction { transaction -> Void in if let state = transaction.getState() as? UnauthorizedAccountState, case let .passwordRecovery(hint, number, code, _, syncContacts) = state.contents { @@ -610,7 +614,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequenceAwaitingAccountResetController(strings: self.strings, theme: self.theme, back: { [weak self] in + controller = AuthorizationSequenceAwaitingAccountResetController(strings: self.presentationData.strings, theme: self.presentationData.theme, back: { [weak self] in guard let strongSelf = self else { return } @@ -622,9 +626,9 @@ public final class AuthorizationSequenceController: NavigationController, MFMail }) controller.reset = { [weak self, weak controller] in if let strongSelf = self, let strongController = controller { - strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: nil, text: strongSelf.strings.TwoStepAuth_ResetAccountConfirmation, actions: [ - TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_Cancel, action: {}), - TextAlertAction(type: .destructiveAction, title: strongSelf.strings.Login_ResetAccountProtected_Reset, action: { + strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_ResetAccountConfirmation, actions: [ + TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Login_ResetAccountProtected_Reset, action: { if let strongSelf = self, let strongController = controller { strongController.inProgress = true strongSelf.actionDisposable.set((performAccountReset(account: strongSelf.account) @@ -638,11 +642,11 @@ public final class AuthorizationSequenceController: NavigationController, MFMail let text: String switch error { case .generic: - text = strongSelf.strings.Login_UnknownError + text = strongSelf.presentationData.strings.Login_UnknownError case .limitExceeded: - text = strongSelf.strings.Login_ResetAccountProtected_LimitExceeded + text = strongSelf.presentationData.strings.Login_ResetAccountProtected_LimitExceeded } - strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } })) } @@ -674,7 +678,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequenceSignUpController(strings: self.strings, theme: self.theme, back: { [weak self] in + controller = AuthorizationSequenceSignUpController(presentationData: self.presentationData, back: { [weak self] in guard let strongSelf = self else { return } @@ -697,18 +701,18 @@ public final class AuthorizationSequenceController: NavigationController, MFMail let text: String switch error { case .limitExceeded: - text = strongSelf.strings.Login_CodeFloodError + text = strongSelf.presentationData.strings.Login_CodeFloodError case .codeExpired: - text = strongSelf.strings.Login_CodeExpiredError + text = strongSelf.presentationData.strings.Login_CodeExpiredError case .invalidFirstName: - text = strongSelf.strings.Login_InvalidFirstNameError + text = strongSelf.presentationData.strings.Login_InvalidFirstNameError case .invalidLastName: - text = strongSelf.strings.Login_InvalidLastNameError + text = strongSelf.presentationData.strings.Login_InvalidLastNameError case .generic: - text = strongSelf.strings.Login_UnknownError + text = strongSelf.presentationData.strings.Login_UnknownError } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } })) @@ -814,7 +818,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail controller.view.window?.rootViewController?.present(composeController, animated: true, completion: nil) } else { - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: self.theme), title: nil, text: self.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: self.strings.Common_OK, action: {})]), in: .window(.root)) + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } } diff --git a/submodules/TelegramUI/TelegramUI/AuthorizationSequencePasswordEntryController.swift b/submodules/TelegramUI/TelegramUI/AuthorizationSequencePasswordEntryController.swift index 8c744c5e3b..1f40397b78 100644 --- a/submodules/TelegramUI/TelegramUI/AuthorizationSequencePasswordEntryController.swift +++ b/submodules/TelegramUI/TelegramUI/AuthorizationSequencePasswordEntryController.swift @@ -10,8 +10,7 @@ final class AuthorizationSequencePasswordEntryController: ViewController { return self.displayNode as! AuthorizationSequencePasswordEntryControllerNode } - private let strings: PresentationStrings - private let theme: PresentationTheme + private let presentationData: PresentationData var loginWithPassword: ((String) -> Void)? var forgot: (() -> Void)? @@ -35,26 +34,25 @@ final class AuthorizationSequencePasswordEntryController: ViewController { var inProgress: Bool = false { didSet { if self.inProgress { - let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.theme.rootController.navigationBar.accentTextColor)) + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor)) self.navigationItem.rightBarButtonItem = item } else { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) } self.controllerNode.inProgress = self.inProgress } } - init(strings: PresentationStrings, theme: PresentationTheme, back: @escaping () -> Void) { - self.strings = strings - self.theme = theme + init(presentationData: PresentationData, back: @escaping () -> Void) { + self.presentationData = presentationData - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(theme), strings: NavigationBarStrings(presentationStrings: strings))) + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(presentationData.theme), strings: NavigationBarStrings(presentationStrings: presentationData.strings))) self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.hasActiveInput = true - self.statusBar.statusBarStyle = theme.intro.statusBarStyle.style + self.statusBar.statusBarStyle = presentationData.theme.intro.statusBarStyle.style self.attemptNavigation = { _ in return false @@ -63,7 +61,7 @@ final class AuthorizationSequencePasswordEntryController: ViewController { back() } - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) } required init(coder aDecoder: NSCoder) { @@ -71,7 +69,7 @@ final class AuthorizationSequencePasswordEntryController: ViewController { } override public func loadDisplayNode() { - self.displayNode = AuthorizationSequencePasswordEntryControllerNode(strings: self.strings, theme: self.theme) + self.displayNode = AuthorizationSequencePasswordEntryControllerNode(strings: self.presentationData.strings, theme: self.presentationData.theme) self.displayNodeDidLoad() self.controllerNode.view.disableAutomaticKeyboardHandling = [.forward, .backward] @@ -132,9 +130,9 @@ final class AuthorizationSequencePasswordEntryController: ViewController { func forgotPressed() { if self.suggestReset { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: self.theme), title: nil, text: self.strings.TwoStepAuth_RecoveryFailed, actions: [TextAlertAction(type: .defaultAction, title: self.strings.Common_OK, action: {})]), in: .window(.root)) + self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.TwoStepAuth_RecoveryFailed, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } else if self.didForgotWithNoRecovery { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: self.theme), title: nil, text: self.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: self.strings.Common_OK, action: {})]), in: .window(.root)) + self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } else { self.forgot?() } diff --git a/submodules/TelegramUI/TelegramUI/AuthorizationSequencePhoneEntryController.swift b/submodules/TelegramUI/TelegramUI/AuthorizationSequencePhoneEntryController.swift index 4cdfacc1a9..db77030f94 100644 --- a/submodules/TelegramUI/TelegramUI/AuthorizationSequencePhoneEntryController.swift +++ b/submodules/TelegramUI/TelegramUI/AuthorizationSequencePhoneEntryController.swift @@ -19,11 +19,11 @@ final class AuthorizationSequencePhoneEntryController: ViewController { } private let sharedContext: SharedAccountContext + private var account: UnauthorizedAccount private let isTestingEnvironment: Bool private let otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]) private let network: Network - private let strings: PresentationStrings - private let theme: PresentationTheme + private let presentationData: PresentationData private let openUrl: (String) -> Void private let back: () -> Void @@ -33,37 +33,38 @@ final class AuthorizationSequencePhoneEntryController: ViewController { var inProgress: Bool = false { didSet { if self.inProgress { - let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.theme.rootController.navigationBar.accentTextColor)) + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor)) self.navigationItem.rightBarButtonItem = item } else { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) } self.controllerNode.inProgress = self.inProgress } } var loginWithNumber: ((String, Bool) -> Void)? + var accountUpdated: ((UnauthorizedAccount) -> Void)? private let termsDisposable = MetaDisposable() private let hapticFeedback = HapticFeedback() - init(sharedContext: SharedAccountContext, isTestingEnvironment: Bool, otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]), network: Network, strings: PresentationStrings, theme: PresentationTheme, openUrl: @escaping (String) -> Void, back: @escaping () -> Void) { + init(sharedContext: SharedAccountContext, account: UnauthorizedAccount, isTestingEnvironment: Bool, otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]), network: Network, presentationData: PresentationData, openUrl: @escaping (String) -> Void, back: @escaping () -> Void) { self.sharedContext = sharedContext + self.account = account self.isTestingEnvironment = isTestingEnvironment self.otherAccountPhoneNumbers = otherAccountPhoneNumbers self.network = network - self.strings = strings - self.theme = theme + self.presentationData = presentationData self.openUrl = openUrl self.back = back - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(theme), strings: NavigationBarStrings(presentationStrings: strings))) + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(presentationData.theme), strings: NavigationBarStrings(presentationStrings: presentationData.strings))) self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.hasActiveInput = true - self.statusBar.statusBarStyle = theme.intro.statusBarStyle.style + self.statusBar.statusBarStyle = presentationData.theme.intro.statusBarStyle.style self.attemptNavigation = { _ in return false } @@ -72,9 +73,9 @@ final class AuthorizationSequencePhoneEntryController: ViewController { } if !otherAccountPhoneNumbers.1.isEmpty { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) } - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) } required init(coder aDecoder: NSCoder) { @@ -97,13 +98,21 @@ final class AuthorizationSequencePhoneEntryController: ViewController { } override public func loadDisplayNode() { - self.displayNode = AuthorizationSequencePhoneEntryControllerNode(strings: self.strings, theme: self.theme, debugAction: { [weak self] in + self.displayNode = AuthorizationSequencePhoneEntryControllerNode(sharedContext: self.sharedContext, account: self.account, strings: self.presentationData.strings, theme: self.presentationData.theme, debugAction: { [weak self] in guard let strongSelf = self else { return } strongSelf.view.endEditing(true) self?.present(debugController(sharedContext: strongSelf.sharedContext, context: nil, modal: true), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - }, hasOtherAccounts: self.otherAccountPhoneNumbers.0 != nil) + }, hasOtherAccounts: self.otherAccountPhoneNumbers.0 != nil) + self.controllerNode.accountUpdated = { [weak self] account in + guard let strongSelf = self else { + return + } + strongSelf.account = account + strongSelf.accountUpdated?(account) + } + if let (code, name, number) = self.currentData { self.controllerNode.codeAndNumber = (code, name, number) } @@ -113,7 +122,7 @@ final class AuthorizationSequencePhoneEntryController: ViewController { self.controllerNode.selectCountryCode = { [weak self] in if let strongSelf = self { - let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.strings, theme: strongSelf.theme) + let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme) controller.completeWithCountryCode = { code, name in if let strongSelf = self, let currentData = strongSelf.currentData { strongSelf.updateData(countryCode: Int32(code), countryName: name, number: currentData.2) @@ -163,13 +172,13 @@ final class AuthorizationSequencePhoneEntryController: ViewController { if let (_, id) = existing { var actions: [TextAlertAction] = [] if let (current, _, _) = self.otherAccountPhoneNumbers.0, logInNumber != formatPhoneNumber(current) { - actions.append(TextAlertAction(type: .genericAction, title: self.strings.Login_PhoneNumberAlreadyAuthorizedSwitch, action: { [weak self] in + actions.append(TextAlertAction(type: .genericAction, title: self.presentationData.strings.Login_PhoneNumberAlreadyAuthorizedSwitch, action: { [weak self] in self?.sharedContext.switchToAccount(id: id, fromSettingsController: nil, withChatListController: nil) self?.back() })) } - actions.append(TextAlertAction(type: .defaultAction, title: self.strings.Common_OK, action: {})) - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: self.theme), title: nil, text: self.strings.Login_PhoneNumberAlreadyAuthorized, actions: actions), in: .window(.root)) + actions.append(TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})) + self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Login_PhoneNumberAlreadyAuthorized, actions: actions), in: .window(.root)) } else { self.loginWithNumber?(self.controllerNode.currentNumber, self.controllerNode.syncContacts) } diff --git a/submodules/TelegramUI/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift b/submodules/TelegramUI/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift index af8e2af666..c887a828af 100644 --- a/submodules/TelegramUI/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift @@ -8,6 +8,10 @@ import TelegramPresentationData import PhoneInputNode import CountrySelectionUI import AuthorizationUI +import QrCode +import SwiftSignalKit +import Postbox +import AccountContext private func emojiFlagForISOCountryCode(_ countryCode: NSString) -> String { if countryCode.length != 2 { @@ -201,6 +205,8 @@ private final class ContactSyncNode: ASDisplayNode { } final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { + private let sharedContext: SharedAccountContext + private var account: UnauthorizedAccount private let strings: PresentationStrings private let theme: PresentationTheme private let hasOtherAccounts: Bool @@ -210,6 +216,11 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { private let phoneAndCountryNode: PhoneAndCountryNode private let contactSyncNode: ContactSyncNode + private var qrNode: ASImageNode? + private let exportTokenDisposable = MetaDisposable() + private let tokenEventsDisposable = MetaDisposable() + var accountUpdated: ((UnauthorizedAccount) -> Void)? + private let debugAction: () -> Void var currentNumber: String { @@ -245,7 +256,10 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { } } - init(strings: PresentationStrings, theme: PresentationTheme, debugAction: @escaping () -> Void, hasOtherAccounts: Bool) { + init(sharedContext: SharedAccountContext, account: UnauthorizedAccount, strings: PresentationStrings, theme: PresentationTheme, debugAction: @escaping () -> Void, hasOtherAccounts: Bool) { + self.sharedContext = sharedContext + self.account = account + self.strings = strings self.theme = theme self.debugAction = debugAction @@ -257,7 +271,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { self.titleNode.attributedText = NSAttributedString(string: strings.Login_PhoneTitle, font: Font.light(30.0), textColor: theme.list.itemPrimaryTextColor) self.noticeNode = ASTextNode() - self.noticeNode.isUserInteractionEnabled = false + self.noticeNode.isUserInteractionEnabled = true self.noticeNode.displaysAsynchronously = false self.noticeNode.attributedText = NSAttributedString(string: strings.Login_PhoneAndCountryHelp, font: Font.regular(16.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center) @@ -285,12 +299,25 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { self.phoneAndCountryNode.checkPhone = { [weak self] in self?.checkPhone?() } + + self.tokenEventsDisposable.set((account.updateLoginTokenEvents + |> deliverOnMainQueue).start(next: { [weak self] _ in + self?.refreshQrToken() + })) + } + + deinit { + self.exportTokenDisposable.dispose() + self.tokenEventsDisposable.dispose() } override func didLoad() { super.didLoad() self.titleNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.debugTap(_:)))) + #if DEBUG + self.noticeNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.debugQrTap(_:)))) + #endif } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -356,4 +383,80 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { } } } + + @objc private func debugQrTap(_ recognizer: UITapGestureRecognizer) { + if self.qrNode == nil { + let qrNode = ASImageNode() + qrNode.frame = CGRect(origin: CGPoint(x: 16.0, y: 64.0 + 16.0), size: CGSize(width: 200.0, height: 200.0)) + self.qrNode = qrNode + self.addSubnode(qrNode) + + self.refreshQrToken() + } + } + + private func refreshQrToken() { + let sharedContext = self.sharedContext + let account = self.account + let tokenSignal = sharedContext.activeAccounts + |> castError(ExportAuthTransferTokenError.self) + |> take(1) + |> mapToSignal { activeAccountsAndInfo -> Signal in + let (primary, activeAccounts, _) = activeAccountsAndInfo + var activeProductionUserIds = activeAccounts.map({ $0.1 }).filter({ !$0.testingEnvironment }).map({ $0.peerId.id }) + var activeTestingUserIds = activeAccounts.map({ $0.1 }).filter({ $0.testingEnvironment }).map({ $0.peerId.id }) + + let allProductionUserIds = activeProductionUserIds + let allTestingUserIds = activeTestingUserIds + + return exportAuthTransferToken(accountManager: sharedContext.accountManager, account: account, otherAccountUserIds: account.testingEnvironment ? allTestingUserIds : allProductionUserIds, syncContacts: true) + } + + self.exportTokenDisposable.set((tokenSignal + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + switch result { + case let .displayToken(token): + var tokenString = token.value.base64EncodedString() + print("export token \(tokenString)") + tokenString = tokenString.replacingOccurrences(of: "+", with: "-") + tokenString = tokenString.replacingOccurrences(of: "/", with: "_") + let urlString = "tg://login?token=\(tokenString)" + let _ = (qrCode(string: urlString, color: .black, backgroundColor: .white, icon: .none) + |> deliverOnMainQueue).start(next: { _, generate in + guard let strongSelf = self else { + return + } + + let context = generate(TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: 200.0, height: 200.0), boundingSize: CGSize(width: 200.0, height: 200.0), intrinsicInsets: UIEdgeInsets())) + if let image = context?.generateImage() { + strongSelf.qrNode?.image = image + } + }) + + let timestamp = Int32(Date().timeIntervalSince1970) + let timeout = max(5, token.validUntil - timestamp) + strongSelf.exportTokenDisposable.set((Signal.complete() + |> delay(Double(timeout), queue: .mainQueue())).start(completed: { + guard let strongSelf = self else { + return + } + strongSelf.refreshQrToken() + })) + case let .changeAccountAndRetry(account): + strongSelf.exportTokenDisposable.set(nil) + strongSelf.account = account + strongSelf.accountUpdated?(account) + strongSelf.tokenEventsDisposable.set((account.updateLoginTokenEvents + |> deliverOnMainQueue).start(next: { _ in + self?.refreshQrToken() + })) + strongSelf.refreshQrToken() + case .loggedIn, .passwordRequested: + strongSelf.exportTokenDisposable.set(nil) + } + })) + } } diff --git a/submodules/TelegramUI/TelegramUI/AuthorizationSequenceSignUpController.swift b/submodules/TelegramUI/TelegramUI/AuthorizationSequenceSignUpController.swift index fa85d1cd79..04373b2de0 100644 --- a/submodules/TelegramUI/TelegramUI/AuthorizationSequenceSignUpController.swift +++ b/submodules/TelegramUI/TelegramUI/AuthorizationSequenceSignUpController.swift @@ -16,8 +16,7 @@ final class AuthorizationSequenceSignUpController: ViewController { return self.displayNode as! AuthorizationSequenceSignUpControllerNode } - private let strings: PresentationStrings - private let theme: PresentationTheme + private let presentationData: PresentationData private let back: () -> Void var initialName: (String, String) = ("", "") @@ -30,27 +29,26 @@ final class AuthorizationSequenceSignUpController: ViewController { var inProgress: Bool = false { didSet { if self.inProgress { - let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.theme.rootController.navigationBar.accentTextColor)) + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor)) self.navigationItem.rightBarButtonItem = item } else { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) } self.controllerNode.inProgress = self.inProgress } } - init(strings: PresentationStrings, theme: PresentationTheme, back: @escaping () -> Void, displayCancel: Bool) { - self.strings = strings - self.theme = theme + init(presentationData: PresentationData, back: @escaping () -> Void, displayCancel: Bool) { + self.presentationData = presentationData self.back = back - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(theme), strings: NavigationBarStrings(presentationStrings: strings))) + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(presentationData.theme), strings: NavigationBarStrings(presentationStrings: presentationData.strings))) self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) - self.statusBar.statusBarStyle = theme.intro.statusBarStyle.style + self.statusBar.statusBarStyle = presentationData.theme.intro.statusBarStyle.style - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) self.attemptNavigation = { _ in return false @@ -59,14 +57,14 @@ final class AuthorizationSequenceSignUpController: ViewController { guard let strongSelf = self else { return } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: theme), title: nil, text: strings.Login_CancelSignUpConfirmation, actions: [TextAlertAction(type: .genericAction, title: strings.Login_CancelPhoneVerificationContinue, action: { - }), TextAlertAction(type: .defaultAction, title: strings.Login_CancelPhoneVerificationStop, action: { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_CancelSignUpConfirmation, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Login_CancelPhoneVerificationContinue, action: { + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Login_CancelPhoneVerificationStop, action: { back() })]), in: .window(.root)) } if displayCancel { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) } } @@ -75,8 +73,8 @@ final class AuthorizationSequenceSignUpController: ViewController { } @objc private func cancelPressed() { - self.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: self.theme), title: nil, text: self.strings.Login_CancelSignUpConfirmation, actions: [TextAlertAction(type: .genericAction, title: self.strings.Login_CancelPhoneVerificationContinue, action: { - }), TextAlertAction(type: .defaultAction, title: self.strings.Login_CancelPhoneVerificationStop, action: { [weak self] in + self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Login_CancelSignUpConfirmation, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Login_CancelPhoneVerificationContinue, action: { + }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Login_CancelPhoneVerificationStop, action: { [weak self] in self?.back() })]), in: .window(.root)) } @@ -84,7 +82,7 @@ final class AuthorizationSequenceSignUpController: ViewController { override public func loadDisplayNode() { let currentAvatarMixin = Atomic(value: nil) - self.displayNode = AuthorizationSequenceSignUpControllerNode(theme: self.theme, strings: self.strings, addPhoto: { [weak self] in + self.displayNode = AuthorizationSequenceSignUpControllerNode(theme: self.presentationData.theme, strings: self.presentationData.strings, addPhoto: { [weak self] in presentLegacyAvatarPicker(holder: currentAvatarMixin, signup: true, theme: defaultPresentationTheme, present: { c, a in self?.view.endEditing(true) self?.present(c, in: .window(.root), with: a) @@ -104,7 +102,7 @@ final class AuthorizationSequenceSignUpController: ViewController { return } strongSelf.view.endEditing(true) - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: defaultPresentationTheme), title: strongSelf.strings.Login_TermsOfServiceHeader, text: termsOfService.text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.Login_TermsOfServiceHeader, text: termsOfService.text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } self.controllerNode.updateData(firstName: self.initialName.0, lastName: self.initialName.1, hasTermsOfService: self.termsOfService != nil) diff --git a/submodules/TelegramUI/TelegramUI/ChatAvatarNavigationNode.swift b/submodules/TelegramUI/TelegramUI/ChatAvatarNavigationNode.swift index e8591415aa..3f8549ff58 100644 --- a/submodules/TelegramUI/TelegramUI/ChatAvatarNavigationNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatAvatarNavigationNode.swift @@ -32,6 +32,13 @@ final class ChatAvatarNavigationNode: ASDisplayNode { let avatarNode: AvatarNode var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? + var contextActionIsEnabled: Bool = true { + didSet { + if self.contextActionIsEnabled != oldValue { + self.containerNode.isGestureEnabled = self.contextActionIsEnabled + } + } + } weak var chatController: ChatControllerImpl? { didSet { @@ -41,6 +48,8 @@ final class ChatAvatarNavigationNode: ASDisplayNode { } } + var tapped: (() -> Void)? + override init() { self.containerNode = ContextControllerSourceNode() self.avatarNode = AvatarNode(font: normalFont) @@ -60,6 +69,12 @@ final class ChatAvatarNavigationNode: ASDisplayNode { } strongSelf.contextAction?(strongSelf.containerNode, gesture) } + + self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 37.0, height: 37.0)).offsetBy(dx: 10.0, dy: 1.0) + self.avatarNode.frame = self.containerNode.bounds + + /*self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 37.0, height: 37.0)) + self.avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 37.0, height: 37.0))*/ } override func didLoad() { @@ -67,30 +82,28 @@ final class ChatAvatarNavigationNode: ASDisplayNode { self.view.isOpaque = false (self.view as? ChatAvatarNavigationNodeView)?.targetNode = self (self.view as? ChatAvatarNavigationNodeView)?.chatController = self.chatController + + /*let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.avatarTapGesture(_:))) + self.avatarNode.view.addGestureRecognizer(tapRecognizer)*/ + } + + @objc private func avatarTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + if case .ended = recognizer.state { + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + self.tapped?() + default: + break + } + } + } } override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - if constrainedSize.height.isLessThanOrEqualTo(32.0) { - return CGSize(width: 26.0, height: 26.0) - } else { - return CGSize(width: 37.0, height: 37.0) - } + return CGSize(width: 37.0, height: 37.0) } func onLayout() { - let bounds = self.bounds - if self.bounds.size.height.isLessThanOrEqualTo(26.0) { - if !self.avatarNode.bounds.size.equalTo(bounds.size) { - self.avatarNode.font = smallFont - } - self.containerNode.frame = bounds.offsetBy(dx: 8.0, dy: 0.0) - self.avatarNode.frame = bounds - } else { - if !self.avatarNode.bounds.size.equalTo(bounds.size) { - self.avatarNode.font = normalFont - } - self.containerNode.frame = bounds.offsetBy(dx: 10.0, dy: 1.0) - self.avatarNode.frame = bounds - } } } diff --git a/submodules/TelegramUI/TelegramUI/ChatBotInfoItem.swift b/submodules/TelegramUI/TelegramUI/ChatBotInfoItem.swift index e2c856eeb3..7e6470fbf2 100644 --- a/submodules/TelegramUI/TelegramUI/ChatBotInfoItem.swift +++ b/submodules/TelegramUI/TelegramUI/ChatBotInfoItem.swift @@ -111,13 +111,13 @@ final class ChatBotInfoItemNode: ListViewItemNode { let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) recognizer.tapActionAtPoint = { [weak self] point in if let strongSelf = self { - let tapAction = strongSelf.tapActionAtPoint(point, gesture: .tap) + let tapAction = strongSelf.tapActionAtPoint(point, gesture: .tap, isEstimating: true) switch tapAction { case .none: break case .ignore: return .fail - case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .openMessage, .timecode, .tooltip: + case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .openMessage, .timecode, .bankCard, .tooltip: return .waitForSingleTap } } @@ -264,7 +264,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { } } - func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction { + func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.textNode.frame if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - self.offsetContainer.frame.minX - textNodeFrame.minX, y: point.y - self.offsetContainer.frame.minY - textNodeFrame.minY)) { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { @@ -295,12 +295,12 @@ final class ChatBotInfoItemNode: ListViewItemNode { if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { switch gesture { case .tap: - let tapAction = self.tapActionAtPoint(location, gesture: gesture) + let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false) switch tapAction { case .none, .ignore: break case let .url(url, concealed): - self.item?.controllerInteraction.openUrl(url, concealed, nil) + self.item?.controllerInteraction.openUrl(url, concealed, nil, nil) case let .peerMention(peerId, _): self.item?.controllerInteraction.openPeer(peerId, .chat(textInputState: nil, subject: nil), nil) case let .textMention(name): @@ -314,7 +314,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { } case .longTap, .doubleTap: if let item = self.item, self.backgroundNode.frame.contains(location) { - let tapAction = self.tapActionAtPoint(location, gesture: gesture) + let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false) switch tapAction { case .none, .ignore: break diff --git a/submodules/TelegramUI/TelegramUI/ChatBotStartInputPanelNode.swift b/submodules/TelegramUI/TelegramUI/ChatBotStartInputPanelNode.swift index 75e3b5baf7..76a553af44 100644 --- a/submodules/TelegramUI/TelegramUI/ChatBotStartInputPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatBotStartInputPanelNode.swift @@ -89,7 +89,7 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.sendBotStart(presentationInterfaceState.botStartPayload) } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { if self.presentationInterfaceState != interfaceState { let previousState = self.presentationInterfaceState self.presentationInterfaceState = interfaceState diff --git a/submodules/TelegramUI/TelegramUI/ChatButtonKeyboardInputNode.swift b/submodules/TelegramUI/TelegramUI/ChatButtonKeyboardInputNode.swift index c061e21e35..d5ae6da4f6 100644 --- a/submodules/TelegramUI/TelegramUI/ChatButtonKeyboardInputNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatButtonKeyboardInputNode.swift @@ -73,7 +73,8 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool) -> (CGFloat, CGFloat) { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel))) - if self.theme !== interfaceState.theme { + let updatedTheme = self.theme !== interfaceState.theme + if updatedTheme { self.theme = interfaceState.theme self.separatorNode.backgroundColor = interfaceState.theme.chat.inputButtonPanel.panelSeparatorColor @@ -128,7 +129,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { self.buttonNodes.append(buttonNode) } buttonIndex += 1 - if buttonNode.button != button { + if buttonNode.button != button || updatedTheme { buttonNode.button = button buttonNode.setAttributedTitle(NSAttributedString(string: button.title, font: Font.regular(16.0), textColor: interfaceState.theme.chat.inputButtonPanel.buttonTextColor, paragraphAlignment: .center), for: []) } @@ -162,11 +163,13 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { @objc func buttonPressed(_ button: ASButtonNode) { if let button = button as? ChatButtonKeyboardInputButtonNode, let markupButton = button.button { + var dismissIfOnce = false switch markupButton.action { case .text: self.controllerInteraction.sendMessage(markupButton.title) + dismissIfOnce = true case let .url(url): - self.controllerInteraction.openUrl(url, true, nil) + self.controllerInteraction.openUrl(url, true, nil, nil) case .requestMap: self.controllerInteraction.shareCurrentLocation() case .requestPhone: @@ -208,6 +211,20 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { if let message = self.message { self.controllerInteraction.requestMessageActionUrlAuth(url, message.id, buttonId) } + case let .setupPoll(isQuiz): + self.controllerInteraction.openPollCreation(isQuiz) + } + if dismissIfOnce { + if let message = self.message { + for attribute in message.attributes { + if let attribute = attribute as? ReplyMarkupMessageAttribute { + if attribute.flags.contains(.once) { + self.controllerInteraction.dismissReplyMarkupMessage(message) + } + break + } + } + } } } } diff --git a/submodules/TelegramUI/TelegramUI/ChatChannelSubscriberInputPanelNode.swift b/submodules/TelegramUI/TelegramUI/ChatChannelSubscriberInputPanelNode.swift index abe3761c10..33a435c828 100644 --- a/submodules/TelegramUI/TelegramUI/ChatChannelSubscriberInputPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatChannelSubscriberInputPanelNode.swift @@ -9,6 +9,7 @@ import SwiftSignalKit import TelegramPresentationData import AlertUI import PresentationDataUtils +import PeerInfoUI private enum SubscriberAction { case join @@ -118,40 +119,45 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { } switch action { - case .join: - self.activityIndicator.isHidden = false - self.activityIndicator.startAnimating() - self.actionDisposable.set((context.peerChannelMemberCategoriesContextsManager.join(account: context.account, peerId: peer.id) - |> afterDisposed { [weak self] in - Queue.mainQueue().async { - if let strongSelf = self { - strongSelf.activityIndicator.isHidden = true - strongSelf.activityIndicator.stopAnimating() - } + case .join: + self.activityIndicator.isHidden = false + self.activityIndicator.startAnimating() + self.actionDisposable.set((context.peerChannelMemberCategoriesContextsManager.join(account: context.account, peerId: peer.id) + |> afterDisposed { [weak self] in + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.activityIndicator.isHidden = true + strongSelf.activityIndicator.stopAnimating() } - }).start(error: { [weak self] error in - guard let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer else { - return - } - let text: String - switch error { - case .tooMuchJoined: - text = presentationInterfaceState.strings.Join_ChannelsTooMuch - default: - if let channel = peer as? TelegramChannel, case .broadcast = channel.info { - text = presentationInterfaceState.strings.Channel_ErrorAccessDenied - } else { - text = presentationInterfaceState.strings.Group_ErrorAccessDenied - } - } - strongSelf.interfaceInteraction?.presentController(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationInterfaceState.strings.Common_OK, action: {})]), nil) - })) - case .kicked: - break - case .muteNotifications, .unmuteNotifications: - if let context = self.context, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer { - self.actionDisposable.set(togglePeerMuted(account: context.account, peerId: peer.id).start()) } + }).start(error: { [weak self] error in + guard let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer else { + return + } + let text: String + switch error { + case .tooMuchJoined: + strongSelf.interfaceInteraction?.getNavigationController()?.pushViewController(oldChannelsController(context: context, intent: .join, completed: { value in + if value { + self?.buttonPressed() + } + })) + return + default: + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + text = presentationInterfaceState.strings.Channel_ErrorAccessDenied + } else { + text = presentationInterfaceState.strings.Group_ErrorAccessDenied + } + } + strongSelf.interfaceInteraction?.presentController(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationInterfaceState.strings.Common_OK, action: {})]), nil) + })) + case .kicked: + break + case .muteNotifications, .unmuteNotifications: + if let context = self.context, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer { + self.actionDisposable.set(togglePeerMuted(account: context.account, peerId: peer.id).start()) + } } } @@ -161,7 +167,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { self.layoutData = (width, leftInset, rightInset) if self.presentationInterfaceState != interfaceState { @@ -169,7 +175,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { self.presentationInterfaceState = interfaceState if previousState?.theme !== interfaceState.theme { - self.badgeBackground.image = PresentationResourcesChatList.badgeBackgroundActive(interfaceState.theme) + self.badgeBackground.image = PresentationResourcesChatList.badgeBackgroundActive(interfaceState.theme, diameter: 20.0) } if previousState?.peerDiscussionId != interfaceState.peerDiscussionId { diff --git a/submodules/TelegramUI/TelegramUI/ChatContextResultPeekContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatContextResultPeekContentNode.swift index bf3529fd36..f7ee6abaa1 100644 --- a/submodules/TelegramUI/TelegramUI/ChatContextResultPeekContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatContextResultPeekContentNode.swift @@ -225,7 +225,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont if updatedImageResource { if let imageResource = imageResource { let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: Int32(fittedImageDimensions.width * 2.0), height: Int32(fittedImageDimensions.height * 2.0)), resource: imageResource) - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) updateImageSignal = chatMessagePhoto(postbox: self.account.postbox, photoReference: .standalone(media: tmpImage)) } else { updateImageSignal = .complete() diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index db51319f4e..b9cb8fcc48 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -49,13 +49,15 @@ import ReactionSelectionNode import AvatarNode import MessageReactionListUI import AppBundle +#if ENABLE_WALLET import WalletUI import WalletUrl +#endif import LocalizedPeerData import PhoneNumberFormat import SettingsUI import UrlWhitelist -import AppIntents +import TelegramIntents public enum ChatControllerPeekActions { case standard @@ -112,7 +114,7 @@ private func isTopmostChatController(_ controller: ChatControllerImpl) -> Bool { if let _ = controller.navigationController { var hasOther = false controller.window?.forEachController({ c in - if c is ChatControllerImpl { + if c is ChatControllerImpl && controller !== c { hasOther = true } }) @@ -182,9 +184,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private let controllerNavigationDisposable = MetaDisposable() private let sentMessageEventsDisposable = MetaDisposable() private let failedMessageEventsDisposable = MetaDisposable() + private weak var currentFailedMessagesAlertController: ViewController? private let messageActionCallbackDisposable = MetaDisposable() private let messageActionUrlAuthDisposable = MetaDisposable() private let editMessageDisposable = MetaDisposable() + private let editMessageErrorsDisposable = MetaDisposable() private let enqueueMediaMessageDisposable = MetaDisposable() private var resolvePeerByNameDisposable: MetaDisposable? private var shareStatusDisposable: MetaDisposable? @@ -270,7 +274,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var raiseToListen: RaiseToListenManager? private var voicePlaylistDidEndTimestamp: Double = 0.0 - + + private weak var sendingOptionsTooltipController: TooltipController? private weak var searchResultsTooltipController: TooltipController? private weak var messageTooltipController: TooltipController? private weak var videoUnmuteTooltipController: TooltipController? @@ -286,7 +291,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private weak var sendMessageActionsController: ChatSendMessageActionSheetController? private var searchResultsController: ChatSearchResultsController? - private var screenCaptureEventsDisposable: Disposable? + private var screenCaptureManager: ScreenCaptureDetectionManager? private let chatAdditionalDataDisposable = MetaDisposable() private var reportIrrelvantGeoNoticePromise = Promise() @@ -304,6 +309,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var updateSlowmodeStatusTimerValue: Int32? private var isDismissed = false + + private var focusOnSearchAfterAppearance: Bool = false + + private let keepPeerInfoScreenDataHotDisposable = MetaDisposable() public override var customData: Any? { return self.chatLocation @@ -343,7 +352,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G isScheduledMessages = true } - self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.fontSize, accountPeerId: context.account.peerId, mode: mode, chatLocation: chatLocation, isScheduledMessages: isScheduledMessages) + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: mode, chatLocation: chatLocation, isScheduledMessages: isScheduledMessages) var mediaAccessoryPanelVisibility = MediaAccessoryPanelVisibility.none if case .standard = mode { @@ -360,13 +369,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } super.init(context: context, navigationBarPresentationData: navigationBarPresentationData, mediaAccessoryPanelVisibility: mediaAccessoryPanelVisibility, locationBroadcastPanelSource: locationBroadcastPanelSource) - switch mode { - case .overlay: - self.navigationPresentation = .standaloneModal - default: - break - } - self.blocksBackgroundWhenInOverlay = true self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) @@ -385,7 +387,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return true } if let _ = strongSelf.presentationInterfaceState.inputTextPanelState.mediaRecordingState { - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_DiscardVoiceMessageDescription, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_DiscardVoiceMessageDescription, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Conversation_DiscardVoiceMessageAction, action: { self?.stopMediaRecorder() action() })]), in: .window(.root)) @@ -437,7 +439,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, present: { c, a in self?.present(c, in: .window(.root), with: a, blockInteraction: true) }, transitionNode: { messageId, media in - var selectedNode: (ASDisplayNode, () -> (UIView?, UIView?))? + var selectedNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { @@ -454,7 +456,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } strongSelf.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.chatDisplayNode.historyNode.view) }, openUrl: { url in - self?.openUrl(url, concealed: false) + self?.openUrl(url, concealed: false, message: nil) }, openPeer: { peer, navigation in self?.openPeer(peerId: peer.id, navigation: navigation, fromMessage: nil) }, callPeer: { peerId in @@ -505,7 +507,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, actionInteraction: GalleryControllerActionInteraction(openUrl: { [weak self] url, concealed in if let strongSelf = self { - strongSelf.controllerInteraction?.openUrl(url, concealed, nil) + strongSelf.controllerInteraction?.openUrl(url, concealed, nil, nil) } }, openUrlIn: { [weak self] url in if let strongSelf = self { @@ -560,10 +562,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G break } } - let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), loadedStickerPack(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, reference: .animatedEmoji, forceActualized: false)).start(next: { actions, animatedEmojiStickers in + let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), loadedStickerPack(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, reference: .animatedEmoji, forceActualized: false), ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager) + ).start(next: { actions, animatedEmojiStickers, chatTextSelectionTips in guard let strongSelf = self, !actions.isEmpty else { return } + var reactionItems: [ReactionContextItem] = [] + /*let reactions: [(String, String, String)] = [ ("😔", "Sad", "sad"), ("😳", "Surprised", "surprised"), @@ -579,16 +584,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G ("😊", "Smile", "smile") ] - var reactionItems: [ReactionContextItem] = [] for (value, text, name) in reactions { if let path = getAppBundle().path(forResource: name, ofType: "tgs") { reactionItems.append(ReactionContextItem(value: value, text: text, path: path)) } - } + }*/ if Namespaces.Message.allScheduled.contains(message.id.namespace) { reactionItems = [] - }*/ - let controller = ContextController(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, message: message)), items: .single(actions), reactionItems: [], recognizer: recognizer) + } + + let numberOfComponents = message.text.components(separatedBy: CharacterSet.whitespacesAndNewlines).count + let displayTextSelectionTip = numberOfComponents >= 3 && !message.text.isEmpty && chatTextSelectionTips < 3 + if displayTextSelectionTip { + let _ = ApplicationSpecificNotice.incrementChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager).start() + } + + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, message: message)), items: .single(actions), reactionItems: reactionItems, recognizer: recognizer, displayTextSelectionTip: displayTextSelectionTip) strongSelf.currentContextController = controller controller.reactionSelected = { [weak controller] value in guard let strongSelf = self, let message = updatedMessages.first else { @@ -620,7 +631,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G gesture?.cancel() }, navigateToMessage: { [weak self] fromId, id in self?.navigateToMessage(from: fromId, to: .id(id)) - }, clickThroughMessage: { [weak self] in + }, tapMessage: nil, clickThroughMessage: { [weak self] in self?.chatDisplayNode.dismissInput() }, toggleMessagesSelection: { [weak self] ids, value in guard let strongSelf = self, strongSelf.isNodeLoaded else { @@ -926,9 +937,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: inputString)), subject: nil), fromMessage: nil) } - }, openUrl: { [weak self] url, concealed, _ in + }, openUrl: { [weak self] url, concealed, _, message in if let strongSelf = self { - strongSelf.openUrl(url, concealed: concealed) + strongSelf.openUrl(url, concealed: concealed, message: message) } }, shareCurrentLocation: { [weak self] in if let strongSelf = self { @@ -1178,7 +1189,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else if canOpenIn { openText = strongSelf.presentationData.strings.Conversation_FileOpenIn } - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: cleanUrl)) @@ -1213,14 +1224,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) case let .peerMention(peerId, mention): - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] if !mention.isEmpty { items.append(ActionSheetTextItem(title: mention)) @@ -1238,14 +1249,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) case let .mention(mention): - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: mention), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in @@ -1259,14 +1270,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G UIPasteboard.general.string = mention }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) case let .command(command): - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: command)) if canSendMessagesToChat(strongSelf.presentationInterfaceState) { @@ -1282,14 +1293,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G UIPasteboard.general.string = command })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) case let .hashtag(hashtag): - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: hashtag), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in @@ -1316,7 +1327,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G UIPasteboard.general.string = hashtag }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -1326,7 +1337,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let message = message else { return } - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: text), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in @@ -1340,12 +1351,44 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G UIPasteboard.general.string = text }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(actionSheet, in: .window(.root)) + case let .bankCard(number): + guard let message = message else { + return + } + + let _ = (getBankCardInfo(account: strongSelf.context.account, cardNumber: number) + |> deliverOnMainQueue).start(next: { [weak self] info in + if let strongSelf = self, let info = info { + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) + var items: [ActionSheetItem] = [] + items.append(ActionSheetTextItem(title: info.title)) + for url in info.urls { + items.append(ActionSheetButtonItem(title: url.title, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.controllerInteraction?.openUrl(url.url, false, false, message) + } + })) + } + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = number + })) + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.present(actionSheet, in: .window(.root)) + } + }) + strongSelf.chatDisplayNode.dismissInput() } } }, openCheckoutOrReceipt: { [weak self] messageId in @@ -1422,7 +1465,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }))) - let controller = ContextController(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, message: message)), items: .single(actions), reactionItems: [], recognizer: nil) + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, message: message)), items: .single(actions), reactionItems: [], recognizer: nil) strongSelf.currentContextController = controller strongSelf.window?.presentInGlobalOverlay(controller) }) @@ -1488,12 +1531,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) }, action: { [weak self] controller, f in if let strongSelf = self { - let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: [id], type: .forLocalPeer).start() + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: [id], type: .forLocalPeer).start() } f(.dismissWithoutContent) }))) - let controller = ContextController(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, message: topMessage)), items: .single(actions), reactionItems: [], recognizer: nil) + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, message: topMessage)), items: .single(actions), reactionItems: [], recognizer: nil) strongSelf.currentContextController = controller strongSelf.window?.presentInGlobalOverlay(controller) }) @@ -1522,16 +1565,42 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.dismissInput() strongSelf.present(controller, in: .window(.root)) } - }, requestSelectMessagePollOption: { [weak self] id, opaqueIdentifier in + }, requestSelectMessagePollOptions: { [weak self] id, opaqueIdentifiers in guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else { return } + guard !strongSelf.presentationInterfaceState.isScheduledMessages else { strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_PollUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return } if controllerInteraction.pollActionState.pollMessageIdsInProgress[id] == nil { - controllerInteraction.pollActionState.pollMessageIdsInProgress[id] = opaqueIdentifier + #if DEBUG + if false { + var found = false + strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in + if !found, let itemNode = itemNode as? ChatMessageBubbleItemNode, itemNode.item?.message.id == id { + found = true + if strongSelf.selectPollOptionFeedback == nil { + strongSelf.selectPollOptionFeedback = HapticFeedback() + } + strongSelf.selectPollOptionFeedback?.error() + itemNode.animateQuizInvalidOptionSelected() + } + } + return; + } + if false { + if strongSelf.selectPollOptionFeedback == nil { + strongSelf.selectPollOptionFeedback = HapticFeedback() + } + strongSelf.selectPollOptionFeedback?.success() + strongSelf.chatDisplayNode.animateQuizCorrectOptionSelected() + return; + } + #endif + + controllerInteraction.pollActionState.pollMessageIdsInProgress[id] = opaqueIdentifiers strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) let disposables: DisposableDict if let current = strongSelf.selectMessagePollOptionDisposables { @@ -1540,9 +1609,53 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G disposables = DisposableDict() strongSelf.selectMessagePollOptionDisposables = disposables } - let signal = requestMessageSelectPollOption(account: strongSelf.context.account, messageId: id, opaqueIdentifier: opaqueIdentifier) + let signal = requestMessageSelectPollOption(account: strongSelf.context.account, messageId: id, opaqueIdentifiers: opaqueIdentifiers) disposables.set((signal - |> deliverOnMainQueue).start(error: { _ in + |> deliverOnMainQueue).start(next: { resultPoll in + guard let strongSelf = self, let resultPoll = resultPoll else { + return + } + guard let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) else { + return + } + + switch resultPoll.kind { + case .poll: + if strongSelf.selectPollOptionFeedback == nil { + strongSelf.selectPollOptionFeedback = HapticFeedback() + } + strongSelf.selectPollOptionFeedback?.success() + case .quiz: + if let voters = resultPoll.results.voters { + for voter in voters { + if voter.selected { + if voter.isCorrect { + if strongSelf.selectPollOptionFeedback == nil { + strongSelf.selectPollOptionFeedback = HapticFeedback() + } + strongSelf.selectPollOptionFeedback?.success() + + strongSelf.chatDisplayNode.animateQuizCorrectOptionSelected() + } else { + var found = false + strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in + if !found, let itemNode = itemNode as? ChatMessageBubbleItemNode, itemNode.item?.message.id == id { + found = true + if strongSelf.selectPollOptionFeedback == nil { + strongSelf.selectPollOptionFeedback = HapticFeedback() + } + strongSelf.selectPollOptionFeedback?.error() + + itemNode.animateQuizInvalidOptionSelected() + } + } + } + break + } + } + } + } + }, error: { _ in guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else { return } @@ -1554,14 +1667,31 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } if controllerInteraction.pollActionState.pollMessageIdsInProgress.removeValue(forKey: id) != nil { - strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) + Queue.mainQueue().after(1.0, { + + strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) + }) } - if strongSelf.selectPollOptionFeedback == nil { - strongSelf.selectPollOptionFeedback = HapticFeedback() - } - strongSelf.selectPollOptionFeedback?.success() }), forKey: id) } + }, requestOpenMessagePollResults: { [weak self] messageId, pollId in + guard let strongSelf = self, pollId.namespace == Namespaces.Media.CloudPoll else { + return + } + let _ = (strongSelf.context.account.postbox.transaction { transaction -> Message? in + return transaction.getMessage(messageId) + } + |> deliverOnMainQueue).start(next: { message in + guard let message = message else { + return + } + for media in message.media { + if let poll = media as? TelegramMediaPoll, poll.pollId == pollId { + strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, poll: poll)) + break + } + } + }) }, openAppStorePage: { [weak self] in if let strongSelf = self { strongSelf.context.sharedContext.applicationBindings.openAppStorePage() @@ -1570,7 +1700,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { if let node = node { strongSelf.messageTooltipController?.dismiss() - let tooltipController = TooltipController(content: .text(text), dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true) + let tooltipController = TooltipController(content: .text(text), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true) strongSelf.messageTooltipController = tooltipController tooltipController.dismissed = { [weak tooltipController] _ in if let strongSelf = self, let tooltipController = tooltipController, strongSelf.messageTooltipController === tooltipController { @@ -1609,25 +1739,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } }, scheduleCurrentMessage: { [weak self] in - if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - let mode: ChatScheduleTimeControllerMode - if peer.id == strongSelf.context.account.peerId { - mode = .reminders - } else { - mode = .scheduledMessages(sendWhenOnlineAvailable: peer.id.namespace == Namespaces.Peer.CloudUser) - } - - let controller = ChatScheduleTimeController(context: strongSelf.context, mode: mode, minimalTime: strongSelf.presentationInterfaceState.slowmodeState?.timeout, completion: { [weak self] scheduleTime in + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(completion: { [weak self] time in if let strongSelf = self { - strongSelf.chatDisplayNode.sendCurrentMessage(scheduleTime: scheduleTime, completion: { [weak self] in - if let strongSelf = self, !strongSelf.presentationInterfaceState.isScheduledMessages { - strongSelf.openScheduledMessages() + strongSelf.chatDisplayNode.sendCurrentMessage(scheduleTime: time, completion: { [weak self] in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } + }) + + if !strongSelf.presentationInterfaceState.isScheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() + } } }) } }) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(controller, in: .window(.root)) } }, sendScheduledMessagesNow: { [weak self] messageIds in if let strongSelf = self { @@ -1641,21 +1768,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } }, editScheduledMessagesTime: { [weak self] messageIds in - if let strongSelf = self, let messageId = messageIds.first, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - let mode: ChatScheduleTimeControllerMode - if peer.id == strongSelf.context.account.peerId { - mode = .reminders - } else { - mode = .scheduledMessages(sendWhenOnlineAvailable: peer.id.namespace == Namespaces.Peer.CloudUser) - } - + if let strongSelf = self, let messageId = messageIds.first { let _ = (strongSelf.context.account.postbox.transaction { transaction -> Message? in return transaction.getMessage(messageId) } |> deliverOnMainQueue).start(next: { [weak self] message in guard let strongSelf = self, let message = message else { return } - let controller = ChatScheduleTimeController(context: strongSelf.context, mode: mode, currentTime: message.timestamp, minimalTime: strongSelf.presentationInterfaceState.slowmodeState?.timeout, completion: { [weak self] scheduleTime in + strongSelf.presentScheduleTimePicker(selectedTime: message.timestamp, completion: { [weak self] time in if let strongSelf = self { var entities: TextEntitiesMessageAttribute? for attribute in message.attributes { @@ -1664,14 +1784,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G break } } - let signal = requestEditMessage(account: strongSelf.context.account, messageId: messageId, text: message.text, media: .keep, entities: entities, disableUrlPreview: false, scheduleTime: scheduleTime) + let signal = requestEditMessage(account: strongSelf.context.account, messageId: messageId, text: message.text, media: .keep, entities: entities, disableUrlPreview: false, scheduleTime: time) strongSelf.editMessageDisposable.set((signal |> deliverOnMainQueue).start(next: { result in }, error: { error in })) } }) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(controller, in: .window(.root)) }) } }, performTextSelectionAction: { [weak self] _, text, action in @@ -1735,8 +1853,44 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }, displaySwipeToReplyHint: { [weak self] in if let strongSelf = self, let validLayout = strongSelf.validLayout, min(validLayout.size.width, validLayout.size.height) > 320.0 { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .swipeToReply(title: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintTitle, text: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintText), elevatedLayout: true, action: { _ in }), in: .window(.root)) + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .swipeToReply(title: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintTitle, text: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintText), elevatedLayout: true, action: { _ in return false }), in: .window(.root)) } + }, dismissReplyMarkupMessage: { [weak self] message in + guard let strongSelf = self, strongSelf.presentationInterfaceState.keyboardButtonsMessage?.id == message.id else { + return + } + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedInputMode({ _ in .text }).updatedInterfaceState({ + $0.withUpdatedMessageActionsState({ value in + var value = value + value.closedButtonKeyboardMessageId = message.id + return value + }) + }) + }) + }, openMessagePollResults: { [weak self] messageId, optionOpaqueIdentifier in + guard let strongSelf = self else { + return + } + let _ = (strongSelf.context.account.postbox.transaction { transaction -> Message? in + return transaction.getMessage(messageId) + } + |> deliverOnMainQueue).start(next: { message in + guard let message = message else { + return + } + for media in message.media { + if let poll = media as? TelegramMediaPoll, poll.pollId.namespace == Namespaces.Media.CloudPoll { + strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, poll: poll, focusOnOptionWithOpaqueIdentifier: optionOpaqueIdentifier)) + break + } + } + }) + }, openPollCreation: { [weak self] isQuiz in + guard let strongSelf = self else { + return + } + strongSelf.presentPollCreation(isQuiz: isQuiz) }, requestMessageUpdate: { [weak self] id in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) @@ -1748,12 +1902,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.controllerInteraction = controllerInteraction + if case let .peer(peerId) = chatLocation, peerId != context.account.peerId, subject != .scheduledMessages { + self.navigationBar?.userInfo = PeerInfoNavigationSourceTag(peerId: peerId) + } + self.chatTitleView = ChatTitleView(account: self.context.account, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder) self.navigationItem.titleView = self.chatTitleView self.chatTitleView?.pressed = { [weak self] in if let strongSelf = self { if strongSelf.chatLocation == .peer(strongSelf.context.account.peerId) { - strongSelf.effectiveNavigationController?.pushViewController(PeerMediaCollectionController(context: strongSelf.context, peerId: strongSelf.context.account.peerId)) + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer, let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: true) { + strongSelf.effectiveNavigationController?.pushViewController(infoController) + } } else { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { return $0.updatedTitlePanelContext { @@ -1778,6 +1938,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } + self.chatTitleView?.longPressed = { [weak self] in + self?.interfaceInteraction?.beginMessageSearch(.everything, "") + } let chatInfoButtonItem: UIBarButtonItem switch chatLocation { @@ -1795,20 +1958,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let items: [ContextMenuItem] = [ .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, icon: { _ in nil }, action: { _, f in f(.dismissWithoutContent) - self?.navigationButtonAction(.openChatInfo) + self?.navigationButtonAction(.openChatInfo(expandAvatar: true)) + })), + .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_Search, icon: { _ in nil }, action: { _, f in + f(.dismissWithoutContent) + self?.interfaceInteraction?.beginMessageSearch(.everything, "") })) ] - let contextController = ContextController(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)! - /*case .group: - chatInfoButtonItem = UIBarButtonItem(customDisplayNode: ChatMultipleAvatarsNavigationNode())!*/ } chatInfoButtonItem.target = self chatInfoButtonItem.action = #selector(self.rightNavigationButtonAction) chatInfoButtonItem.accessibilityLabel = self.presentationData.strings.Conversation_Info - self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo, buttonItem: chatInfoButtonItem) + self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo(expandAvatar: true), buttonItem: chatInfoButtonItem) + + self.navigationItem.titleView = self.chatTitleView + self.chatTitleView?.pressed = { [weak self] in + self?.navigationButtonAction(.openChatInfo(expandAvatar: false)) + } self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in if let botStart = botStart, case .interactive = botStart.behavior { @@ -1887,7 +2057,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { if let peer = peerViewMainPeer(peerView) { strongSelf.chatTitleView?.titleContent = .peer(peerView: peerView, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages) - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, peer: peer, overrideImage: peer.isDeleted ? .deletedIcon : .none) + let imageOverride: AvatarNodeImageOverride? + if strongSelf.context.account.peerId == peer.id { + imageOverride = .savedMessagesIcon + } else if peer.isDeleted { + imageOverride = .deletedIcon + } else { + imageOverride = nil + } + (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: peer, overrideImage: imageOverride) + (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil } if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages { @@ -1953,9 +2132,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var contactStatus: ChatContactStatus? if let peer = peerView.peers[peerView.peerId] { if let cachedData = peerView.cachedData as? CachedUserData { - if !peer.isDeleted { - contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings) - } + contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings) } else if let cachedData = peerView.cachedData as? CachedGroupData { contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings) } else if let cachedData = peerView.cachedData as? CachedChannelData { @@ -2344,6 +2521,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.messageActionCallbackDisposable.dispose() self.messageActionUrlAuthDisposable.dispose() self.editMessageDisposable.dispose() + self.editMessageErrorsDisposable.dispose() self.enqueueMediaMessageDisposable.dispose() self.resolvePeerByNameDisposable?.dispose() self.shareStatusDisposable?.dispose() @@ -2373,7 +2551,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.applicationInForegroundDisposable?.dispose() self.canReadHistoryDisposable?.dispose() self.networkStateDisposable?.dispose() - self.screenCaptureEventsDisposable?.dispose() self.chatAdditionalDataDisposable.dispose() self.shareStatusDisposable?.dispose() self.context.sharedContext.mediaManager.galleryHiddenMediaManager.removeTarget(self) @@ -2381,6 +2558,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.reportIrrelvantGeoDisposable?.dispose() self.reminderActivity?.invalidate() self.updateSlowmodeStatusDisposable.dispose() + self.keepPeerInfoScreenDataHotDisposable.dispose() } public func updatePresentationMode(_ mode: ChatControllerPresentationMode) { @@ -2415,10 +2593,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G state = state.updatedStrings(self.presentationData.strings) state = state.updatedDateTimeFormat(self.presentationData.dateTimeFormat) state = state.updatedChatWallpaper(self.presentationData.chatWallpaper) + state = state.updatedBubbleCorners(self.presentationData.chatBubbleCorners) return state }) - self.currentContextController?.updateTheme(theme: self.presentationData.theme) + self.currentContextController?.updateTheme(presentationData: self.presentationData) } override public func loadDisplayNode() { @@ -2683,6 +2862,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatDisplayNode.historyNode.contentPositionChanged = { [weak self] offset in if let strongSelf = self { let offsetAlpha: CGFloat + let plainInputSeparatorAlpha: CGFloat switch offset { case let .known(offset): if offset < 40.0 { @@ -2690,13 +2870,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { offsetAlpha = 1.0 } + if offset < 4.0 { + plainInputSeparatorAlpha = 0.0 + } else { + plainInputSeparatorAlpha = 1.0 + } case .unknown: offsetAlpha = 1.0 + plainInputSeparatorAlpha = 1.0 case .none: offsetAlpha = 0.0 + plainInputSeparatorAlpha = 0.0 } strongSelf.chatDisplayNode.navigateButtons.displayDownButton = !offsetAlpha.isZero + strongSelf.chatDisplayNode.updatePlainInputSeparatorAlpha(plainInputSeparatorAlpha, transition: .animated(duration: 0.2, curve: .easeInOut)) } } @@ -2706,7 +2894,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(index.id) { let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId) controllerInteraction.highlightedState = highlightedState - strongSelf.updateItemNodesHighlightedStates(animated: true) + strongSelf.updateItemNodesHighlightedStates(animated: false) strongSelf.messageContextDisposable.set((Signal.complete() |> delay(0.7, queue: Queue.mainQueue())).start(completed: { if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { @@ -2806,7 +2994,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let errorText = errorText { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return } } @@ -2830,7 +3018,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) - donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, peerIds: [peerId]) + donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, intentContext: .chat, peerIds: [peerId]) } } @@ -2859,10 +3047,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self, let editMessageState = strongSelf.presentationInterfaceState.editMessageState, case let .media(options) = editMessageState.content else { return } - strongSelf.presentAttachmentMenu(editMediaOptions: options) + var originalMediaReference: AnyMediaReference? + if let message = message { + for media in message.media { + if let image = media as? TelegramMediaImage { + originalMediaReference = .message(message: MessageReference(message), media: image) + } else if let file = media as? TelegramMediaFile { + if file.isVideo || file.isAnimated { + originalMediaReference = .message(message: MessageReference(message), media: file) + } + } + } + } + strongSelf.presentAttachmentMenu(editMediaOptions: options, editMediaReference: originalMediaReference) }) } else { - strongSelf.presentAttachmentMenu(editMediaOptions: nil) + strongSelf.presentAttachmentMenu(editMediaOptions: nil, editMediaReference: nil) } } self.chatDisplayNode.paste = { [weak self] data in @@ -2949,7 +3149,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.WebSearch_RecentSectionClear, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -2959,7 +3159,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = clearPeerUnseenPersonalMessagesInteractively(account: strongSelf.context.account, peerId: peerId).start() }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -2990,7 +3190,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G state = state.updatedEditMessageState(nil) return state }, completion: completion) - strongSelf.editMessageDisposable.set(nil) return } @@ -3083,10 +3282,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } if isAction && (actions.options == .deleteGlobally || actions.options == .deleteLocally) { - let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: Array(messageIds), type: actions.options == .deleteLocally ? .forLocalPeer : .forEveryone).start() + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: actions.options == .deleteLocally ? .forLocalPeer : .forEveryone).start() completion(.dismissWithoutContent) } else if (messages.first?.flags.isSending ?? false) { - let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: Array(messageIds), type: .forEveryone, deleteAllInGroup: true).start() + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone, deleteAllInGroup: true).start() completion(.dismissWithoutContent) } else { if actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty { @@ -3203,65 +3402,44 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G media = .keep } - strongSelf.editMessageDisposable.set((requestEditMessage(account: strongSelf.context.account, messageId: editMessage.messageId, text: text.string, media: media - , entities: entitiesAttribute, disableUrlPreview: disableUrlPreview) |> deliverOnMainQueue |> afterDisposed({ - editingMessage.set(nil) - })).start(next: { result in - guard let strongSelf = self else { - return - } - switch result { - case let .progress(value): - editingMessage.set(value) - case .done: - editingMessage.set(nil) - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in - var state = state - state = state.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) }) - state = state.updatedEditMessageState(nil) - return state - }) - } - }, error: { error in - guard let strongSelf = self else { - return - } - - editingMessage.set(nil) - - let text: String - switch error { - case .generic: - text = strongSelf.presentationData.strings.Channel_EditMessageErrorGeneric - case .restricted: - text = strongSelf.presentationData.strings.Group_ErrorSendRestrictedMedia - } - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) - })) + strongSelf.context.account.pendingUpdateMessageManager.add(messageId: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, disableUrlPreview: disableUrlPreview) + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + var state = state + state = state.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) }) + state = state.updatedEditMessageState(nil) + return state + }) } }, beginMessageSearch: { [weak self] domain, query in - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in - return current.updatedTitlePanelContext { - if let index = $0.firstIndex(where: { - switch $0 { - case .chatInfo: - return true - default: - return false - } - }) { - var updatedContexts = $0 - updatedContexts.remove(at: index) - return updatedContexts - } else { - return $0 - } - }.updatedSearch(current.search == nil ? ChatSearchData(domain: domain).withUpdatedQuery(query) : current.search?.withUpdatedDomain(domain).withUpdatedQuery(query)) - }) - strongSelf.updateItemNodesSearchTextHighlightStates() + guard let strongSelf = self else { + return } + var interactive = true + if strongSelf.chatDisplayNode.isInputViewFocused { + interactive = false + strongSelf.context.sharedContext.mainWindow?.doNotAnimateLikelyKeyboardAutocorrectionSwitch() + } + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: interactive, { current in + return current.updatedTitlePanelContext { + if let index = $0.firstIndex(where: { + switch $0 { + case .chatInfo: + return true + default: + return false + } + }) { + var updatedContexts = $0 + updatedContexts.remove(at: index) + return updatedContexts + } else { + return $0 + } + }.updatedSearch(current.search == nil ? ChatSearchData(domain: domain).withUpdatedQuery(query) : current.search?.withUpdatedDomain(domain).withUpdatedQuery(query)) + }) + strongSelf.updateItemNodesSearchTextHighlightStates() }, dismissMessageSearch: { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in @@ -3377,18 +3555,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation { strongSelf.chatDisplayNode.dismissInput() - let controller = ChatDateSelectionSheet(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, completion: { timestamp in - if let strongSelf = self { - strongSelf.loadingMessage.set(true) - strongSelf.messageIndexDisposable.set((searchMessageIdByTimestamp(account: strongSelf.context.account, peerId: peerId, timestamp: timestamp) |> deliverOnMainQueue).start(next: { messageId in - if let strongSelf = self { - strongSelf.loadingMessage.set(false) - if let messageId = messageId { - strongSelf.navigateToMessage(from: nil, to: .id(messageId)) - } - } - })) + let controller = ChatDateSelectionSheet(presentationData: strongSelf.presentationData, completion: { timestamp in + guard let strongSelf = self else { + return } + strongSelf.loadingMessage.set(true) + strongSelf.messageIndexDisposable.set((searchMessageIdByTimestamp(account: strongSelf.context.account, peerId: peerId, timestamp: timestamp) |> deliverOnMainQueue).start(next: { messageId in + if let strongSelf = self { + strongSelf.loadingMessage.set(false) + if let messageId = messageId { + strongSelf.navigateToMessage(from: nil, to: .id(messageId), forceInCurrentChat: true) + } + } + })) }) strongSelf.present(controller, in: .window(.root)) } @@ -3422,7 +3601,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), subject: nil, keepStack: .always)) } }, openPeerInfo: { [weak self] in - self?.navigationButtonAction(.openChatInfo) + self?.navigationButtonAction(.openChatInfo(expandAvatar: false)) }, togglePeerNotifications: { [weak self] in if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation { let _ = togglePeerMuted(account: strongSelf.context.account, peerId: peerId).start() @@ -3625,7 +3804,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G tooltipController.updateContent(.text(banDescription), animated: true, extendTimer: true) } else if let rect = rect { strongSelf.mediaRestrictedTooltipController?.dismiss() - let tooltipController = TooltipController(content: .text(banDescription)) + let tooltipController = TooltipController(content: .text(banDescription), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize) strongSelf.mediaRestrictedTooltipController = tooltipController strongSelf.mediaRestrictedTooltipControllerMode = isStickers tooltipController.dismissed = { [weak tooltipController] _ in @@ -3661,7 +3840,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let location = location, let icon = icon { strongSelf.videoUnmuteTooltipController?.dismiss() - let tooltipController = TooltipController(content: .iconAndText(icon, strongSelf.presentationInterfaceState.strings.Conversation_PressVolumeButtonForSound), timeout: 3.5, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true) + let tooltipController = TooltipController(content: .iconAndText(icon, strongSelf.presentationInterfaceState.strings.Conversation_PressVolumeButtonForSound), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, timeout: 3.5, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true) strongSelf.videoUnmuteTooltipController = tooltipController tooltipController.dismissed = { [weak tooltipController] _ in if let strongSelf = self, let tooltipController = tooltipController, strongSelf.videoUnmuteTooltipController === tooltipController { @@ -3916,7 +4095,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let tooltipController = strongSelf.silentPostTooltipController { tooltipController.updateContent(.text(text), animated: true, extendTimer: true) } else if let rect = rect { - let tooltipController = TooltipController(content: .text(text)) + let tooltipController = TooltipController(content: .text(text), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize) strongSelf.silentPostTooltipController = tooltipController tooltipController.dismissed = { [weak tooltipController] _ in if let strongSelf = self, let tooltipController = tooltipController, strongSelf.silentPostTooltipController === tooltipController { @@ -3944,7 +4123,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let controller = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil)) strongSelf.present(controller, in: .window(.root)) - let signal = requestMessageSelectPollOption(account: strongSelf.context.account, messageId: id, opaqueIdentifier: nil) + let signal = requestMessageSelectPollOption(account: strongSelf.context.account, messageId: id, opaqueIdentifiers: []) |> afterDisposed { [weak controller] in Queue.mainQueue().async { controller?.dismiss() @@ -3962,14 +4141,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.selectPollOptionFeedback?.success() }), forKey: id) }, requestStopPollInMessage: { [weak self] id in - guard let strongSelf = self else { + guard let strongSelf = self, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) else { return } - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + var maybePoll: TelegramMediaPoll? + for media in message.media { + if let poll = media as? TelegramMediaPoll { + maybePoll = poll + break + } + } + + guard let poll = maybePoll else { + return + } + + let actionTitle: String + let actionButtonText: String + switch poll.kind { + case .poll: + actionTitle = strongSelf.presentationData.strings.Conversation_StopPollConfirmationTitle + actionButtonText = strongSelf.presentationData.strings.Conversation_StopPollConfirmation + case .quiz: + actionTitle = strongSelf.presentationData.strings.Conversation_StopQuizConfirmationTitle + actionButtonText = strongSelf.presentationData.strings.Conversation_StopQuizConfirmation + } + + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: strongSelf.presentationData.strings.Conversation_StopPollConfirmationTitle), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_StopPollConfirmation, color: .destructive, action: { [weak self, weak actionSheet] in + ActionSheetTextItem(title: actionTitle), + ActionSheetButtonItem(title: actionButtonText, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() guard let strongSelf = self else { return @@ -4005,7 +4207,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }), forKey: id) }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -4109,7 +4311,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.slowmodeTooltipController = nil slowmodeTooltipController.dismiss() } - let slowmodeTooltipController = ChatSlowmodeHintController(strings: strongSelf.presentationData.strings, slowmodeState: + let slowmodeTooltipController = ChatSlowmodeHintController(presentationData: strongSelf.presentationData, slowmodeState: slowmodeState) slowmodeTooltipController.presentationArguments = TooltipControllerPresentationArguments(sourceNodeAndRect: { if let strongSelf = self { @@ -4120,17 +4322,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.slowmodeTooltipController = slowmodeTooltipController strongSelf.window?.presentInGlobalOverlay(slowmodeTooltipController) - }, displaySendMessageOptions: { [weak self] in - if let strongSelf = self, let sendButtonFrame = strongSelf.chatDisplayNode.sendButtonFrame(), let textInputNode = strongSelf.chatDisplayNode.textInputNode(), let layout = strongSelf.validLayout { + }, displaySendMessageOptions: { [weak self] node, gesture in + if let strongSelf = self, let textInputNode = strongSelf.chatDisplayNode.textInputNode(), let layout = strongSelf.validLayout { + let previousSupportedOrientations = strongSelf.supportedOrientations if layout.size.width > layout.size.height { strongSelf.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .landscape) } else { strongSelf.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) } - let controller = ChatSendMessageActionSheetController(context: strongSelf.context, controllerInteraction: strongSelf.controllerInteraction, interfaceState: strongSelf.presentationInterfaceState, sendButtonFrame: strongSelf.chatDisplayNode.convert(sendButtonFrame, to: nil), textInputNode: textInputNode, completion: { [weak self] in + let _ = ApplicationSpecificNotice.incrementChatMessageOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager, count: 4).start() + + let controller = ChatSendMessageActionSheetController(context: strongSelf.context, controllerInteraction: strongSelf.controllerInteraction, interfaceState: strongSelf.presentationInterfaceState, gesture: gesture, sendButtonFrame: node.view.convert(node.bounds, to: nil), textInputNode: textInputNode, completion: { [weak self] in if let strongSelf = self { - strongSelf.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all) + strongSelf.supportedOrientations = previousSupportedOrientations } }) strongSelf.sendMessageActionsController = controller @@ -4147,7 +4352,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, displaySearchResultsTooltip: { [weak self] node, nodeRect in if let strongSelf = self { strongSelf.searchResultsTooltipController?.dismiss() - let tooltipController = TooltipController(content: .text(strongSelf.presentationData.strings.ChatSearch_ResultsTooltip), dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true) + let tooltipController = TooltipController(content: .text(strongSelf.presentationData.strings.ChatSearch_ResultsTooltip), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true) strongSelf.searchResultsTooltipController = tooltipController tooltipController.dismissed = { [weak tooltipController] _ in if let strongSelf = self, let tooltipController = tooltipController, strongSelf.searchResultsTooltipController === tooltipController { @@ -4259,19 +4464,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - self.sentMessageEventsDisposable.set(self.context.account.pendingMessageManager.deliveredMessageEvents(peerId: peerId).start(next: { [weak self] _ in + self.sentMessageEventsDisposable.set((self.context.account.pendingMessageManager.deliveredMessageEvents(peerId: peerId) + |> deliverOnMainQueue).start(next: { [weak self] namespace in if let strongSelf = self { - let inAppNotificationSettings: InAppNotificationSettings = strongSelf.context.sharedContext.currentInAppNotificationSettings.with { $0 } - + let inAppNotificationSettings = strongSelf.context.sharedContext.currentInAppNotificationSettings.with { $0 } if inAppNotificationSettings.playSounds { serviceSoundManager.playMessageDeliveredSound() } + if !strongSelf.presentationInterfaceState.isScheduledMessages && namespace == Namespaces.Message.ScheduledCloud { + strongSelf.openScheduledMessages() + } } })) self.failedMessageEventsDisposable.set((self.context.account.pendingMessageManager.failedMessageEvents(peerId: peerId) |> deliverOnMainQueue).start(next: { [weak self] reason in - if let strongSelf = self { + if let strongSelf = self, strongSelf.currentFailedMessagesAlertController == nil { let text: String let moreInfo: Bool switch reason { @@ -4299,12 +4507,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { actions = [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})] } - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: actions), in: .window(.root)) + let controller = textAlertController(context: strongSelf.context, title: nil, text: text, actions: actions) + strongSelf.currentFailedMessagesAlertController = controller + strongSelf.present(controller, in: .window(.root)) } })) } self.interfaceInteraction = interfaceInteraction + + if self.focusOnSearchAfterAppearance { + self.focusOnSearchAfterAppearance = false + self.interfaceInteraction?.beginMessageSearch(.everything, "") + } + self.chatDisplayNode.interfaceInteraction = interfaceInteraction self.context.sharedContext.mediaManager.galleryHiddenMediaManager.addTarget(self) @@ -4397,7 +4613,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return a && b }) - self.chatDisplayNode.loadInputPanels(theme: self.presentationInterfaceState.theme, strings: self.presentationInterfaceState.strings) + self.chatDisplayNode.loadInputPanels(theme: self.presentationInterfaceState.theme, strings: self.presentationInterfaceState.strings, fontSize: self.presentationInterfaceState.fontSize) self.recentlyUsedInlineBotsDisposable = (recentlyUsedInlineBots(postbox: self.context.account.postbox) |> deliverOnMainQueue).start(next: { [weak self] peers in self?.recentlyUsedInlineBotsValue = peers.filter({ $0.1 >= 0.14 }).map({ $0.0 }) @@ -4484,10 +4700,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if !self.checkedPeerChatServiceActions { self.checkedPeerChatServiceActions = true - if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { - self.screenCaptureEventsDisposable = screenCaptureEvents().start(next: { [weak self] _ in + if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat, self.screenCaptureManager == nil { + self.screenCaptureManager = ScreenCaptureDetectionManager(check: { [weak self] in if let strongSelf = self, strongSelf.canReadHistoryValue, strongSelf.traceVisibility() { let _ = addSecretChatMessageScreenshot(account: strongSelf.context.account, peerId: peerId).start() + return true + } else { + return false } }) } @@ -4496,44 +4715,80 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = checkPeerChatServiceActions(postbox: self.context.account.postbox, peerId: peerId).start() } - if self.chatDisplayNode.frameForInputActionButton() != nil, self.presentationInterfaceState.interfaceState.mediaRecordingMode == .audio { - var canSendMedia = false - if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel { - if channel.hasBannedPermission(.banSendMedia) == nil { + if self.chatDisplayNode.frameForInputActionButton() != nil { + let inputText = self.presentationInterfaceState.interfaceState.effectiveInputState.inputText.string + if !inputText.isEmpty { + if inputText.count > 4 { + let _ = (ApplicationSpecificNotice.getChatMessageOptionsTip(accountManager: context.sharedContext.accountManager) + |> deliverOnMainQueue).start(next: { [weak self] counter in + if let strongSelf = self, counter < 3 { + let _ = ApplicationSpecificNotice.incrementChatMessageOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager).start() + strongSelf.displaySendingOptionsTooltip() + } + }) + } + } else if self.presentationInterfaceState.interfaceState.mediaRecordingMode == .audio { + var canSendMedia = false + if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel { + if channel.hasBannedPermission(.banSendMedia) == nil { + canSendMedia = true + } + } else if let group = self.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup { + if !group.hasBannedPermission(.banSendMedia) { + canSendMedia = true + } + } else { canSendMedia = true } - } else if let group = self.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup { - if !group.hasBannedPermission(.banSendMedia) { - canSendMedia = true + if canSendMedia { + let _ = (ApplicationSpecificNotice.getChatMediaMediaRecordingTips(accountManager: self.context.sharedContext.accountManager) + |> deliverOnMainQueue).start(next: { [weak self] counter in + guard let strongSelf = self else { + return + } + var displayTip = false + if counter == 0 { + displayTip = true + } else if counter < 3 && arc4random_uniform(4) == 1 { + displayTip = true + } + if displayTip { + let _ = ApplicationSpecificNotice.incrementChatMediaMediaRecordingTips(accountManager: strongSelf.context.sharedContext.accountManager).start() + strongSelf.displayMediaRecordingTooltip() + } + }) } - } else { - canSendMedia = true } - if canSendMedia { - let _ = (ApplicationSpecificNotice.getChatMediaMediaRecordingTips(accountManager: self.context.sharedContext.accountManager) - |> deliverOnMainQueue).start(next: { [weak self] counter in - guard let strongSelf = self else { - return - } - var displayTip = false - if counter == 0 { - displayTip = true - } else if counter < 3 && arc4random_uniform(4) == 1 { - displayTip = true - } - if displayTip { - let _ = ApplicationSpecificNotice.incrementChatMediaMediaRecordingTips(accountManager: strongSelf.context.sharedContext.accountManager).start() - strongSelf.displayMediaRecordingTooltip() - } - }) + } + + self.editMessageErrorsDisposable.set((self.context.account.pendingUpdateMessageManager.errors + |> deliverOnMainQueue).start(next: { [weak self] (_, error) in + guard let strongSelf = self else { + return } + + let text: String + switch error { + case .generic: + text = strongSelf.presentationData.strings.Channel_EditMessageErrorGeneric + case .restricted: + text = strongSelf.presentationData.strings.Group_ErrorSendRestrictedMedia + } + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) + })) + + if case let .peer(peerId) = self.chatLocation { + self.keepPeerInfoScreenDataHotDisposable.set(keepPeerInfoScreenDataHot(context: self.context, peerId: peerId).start()) } } - /*if let subject = self.subject, case .scheduledMessages = subject { - self.chatDisplayNode.animateIn() - self.updateTransitionWhenPresentedAsModal?(1.0, .animated(duration: 0.5, curve: .spring)) - }*/ + if self.focusOnSearchAfterAppearance { + self.focusOnSearchAfterAppearance = false + if let searchNode = self.navigationBar?.contentNode as? ChatSearchNavigationContentNode { + searchNode.activate() + } + } } override public func viewWillDisappear(_ animated: Bool) { @@ -4603,7 +4858,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .standard, .inline: break case .overlay: - if layout.safeInsets.top.isZero { + if case .Ignore = self.statusBar.statusBarStyle { + } else if layout.safeInsets.top.isZero { self.statusBar.statusBarStyle = .Hide } else { self.statusBar.statusBarStyle = .Ignore @@ -5054,18 +5310,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G statusText = strongSelf.presentationData.strings.Undo_ChatCleared } - strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: statusText), elevatedLayout: true, action: { shouldCommit in - if shouldCommit { + strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: statusText), elevatedLayout: true, action: { value in + if value == .commit { let _ = clearHistoryInteractively(postbox: account.postbox, peerId: peerId, type: type).start(completed: { self?.chatDisplayNode.historyNode.historyAppearsCleared = false }) - } else { + return true + } else if value == .undo { self?.chatDisplayNode.historyNode.historyAppearsCleared = false + return true } + return false }), in: .current) } - let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] if self.presentationInterfaceState.isScheduledMessages { @@ -5076,7 +5335,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: { @@ -5093,7 +5352,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText, actions: [ + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText, actions: [ TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: { @@ -5114,7 +5373,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: { @@ -5125,7 +5384,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -5133,18 +5392,29 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatDisplayNode.dismissInput() self.present(actionSheet, in: .window(.root)) } - case .openChatInfo: + case let .openChatInfo(expandAvatar): switch self.chatLocationInfoData { - case let .peer(peerView): - self.navigationActionDisposable.set((peerView.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] peerView in - if let strongSelf = self, let peer = peerView.peers[peerView.peerId], peer.restrictionText(platform: "ios") == nil && !strongSelf.presentationInterfaceState.isNotAccessible { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + case let .peer(peerView): + self.navigationActionDisposable.set((peerView.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] peerView in + if let strongSelf = self, let peer = peerView.peers[peerView.peerId], peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil && !strongSelf.presentationInterfaceState.isNotAccessible { + if peer.id == strongSelf.context.account.peerId { + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer, let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: true) { + strongSelf.effectiveNavigationController?.pushViewController(infoController) + } + //strongSelf.effectiveNavigationController?.pushViewController(PeerMediaCollectionController(context: strongSelf.context, peerId: strongSelf.context.account.peerId)) + } else { + var expandAvatar = expandAvatar + if peer.smallProfileImage == nil { + expandAvatar = false + } + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: expandAvatar, fromChat: true) { strongSelf.effectiveNavigationController?.pushViewController(infoController) } } - })) + } + })) } case .search: self.interfaceInteraction?.beginMessageSearch(.everything, "") @@ -5180,7 +5450,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } let presentationData = strongSelf.presentationData - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } @@ -5324,7 +5594,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G disposable.set((signal |> deliverOnMainQueue).start(completed: { [weak self] in if let strongSelf = self, let layout = strongSelf.validLayout { - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", stringForDeviceType()).0), elevatedLayout: true, action: { _ in }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", stringForDeviceType()).0), elevatedLayout: true, action: { _ in return false }), in: .current) } })) @@ -5352,6 +5622,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) })) } + case .toggleInfoPanel: + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if let index = $0.firstIndex(where: { + switch $0 { + case .chatInfo: + return true + default: + return false + } + }) { + var updatedContexts = $0 + updatedContexts.remove(at: index) + return updatedContexts + } else { + var updatedContexts = $0 + updatedContexts.append(.chatInfo) + return updatedContexts.sorted() + } + } + }) } } @@ -5372,6 +5663,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } return state }) + self.interfaceInteraction?.editMessage() } } @@ -5386,7 +5678,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func presentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?) { + private func presentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) { let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings return entry ?? GeneratedMediaStoreSettings.defaultSettings @@ -5425,12 +5717,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G banDescription = strongSelf.presentationInterfaceState.strings.Conversation_DefaultRestrictedMedia } - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: banDescription)) items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_Location, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() - self?.presentMapPicker(editingMessage: false) + self?.presentLocationPicker() })) if canSendPolls { items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.AttachmentMenu_Poll, color: .accent, action: { [weak actionSheet] in @@ -5443,7 +5735,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self?.presentContactPicker() })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -5468,13 +5760,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText let menuEditMediaOptions = editMediaOptions.flatMap { options -> LegacyAttachmentMenuMediaEditing in - var result: LegacyAttachmentMenuMediaEditing = [] + var result: LegacyAttachmentMenuMediaEditing = .none if options.contains(.imageOrVideo) { - result.insert(.imageOrVideo) + result = .imageOrVideo(editMediaReference) } return result } + var slowModeEnabled = false + if let channel = peer as? TelegramChannel, channel.isRestrictedBySlowmode { + slowModeEnabled = true + } + let controller = legacyAttachmentMenu(context: strongSelf.context, peer: peer, editMediaOptions: menuEditMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, hasSchedule: !strongSelf.presentationInterfaceState.isScheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, canSendPolls: canSendPolls, presentationData: strongSelf.presentationData, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, initialCaption: inputText.string, openGallery: { self?.presentMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, completion: { signals, silentPosting, scheduleTime in if !inputText.string.isEmpty { @@ -5503,29 +5800,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { if let (host, port, username, password, secret) = parseProxyUrl(code) { strongSelf.openResolved(ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret)) - } else if let url = URL(string: code), let parsedWalletUrl = parseWalletUrl(url) { + }/* else if let url = URL(string: code), let parsedWalletUrl = parseWalletUrl(url) { //strongSelf.openResolved(ResolvedUrl.wallet(address: parsedWalletUrl.address, amount: parsedWalletUrl.amount, comment: parsedWalletUrl.comment)) - } + }*/ } }, presentSchedulePicker: { [weak self] done in - guard let strongSelf = self else { - return - } - let mode: ChatScheduleTimeControllerMode - if peer.id == strongSelf.context.account.peerId { - mode = .reminders - } else { - mode = .scheduledMessages(sendWhenOnlineAvailable: peer.id.namespace == Namespaces.Peer.CloudUser) - } - let controller = ChatScheduleTimeController(context: strongSelf.context, mode: mode, minimalTime: strongSelf.presentationInterfaceState.slowmodeState?.timeout, completion: { [weak self] time in - if let strongSelf = self { - done(time) - if !strongSelf.presentationInterfaceState.isScheduledMessages { - strongSelf.openScheduledMessages() + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(completion: { [weak self] time in + if let strongSelf = self { + done(time) + if !strongSelf.presentationInterfaceState.isScheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() + } } - } - }) - strongSelf.present(controller, in: .window(.root)) + }) + } }) } }, openFileGallery: { @@ -5533,7 +5822,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, openWebSearch: { self?.presentWebSearch(editingMessage : editMediaOptions != nil) }, openMap: { - self?.presentMapPicker(editingMessage: editMediaOptions != nil) + self?.presentLocationPicker() }, openContacts: { self?.presentContactPicker() }, openPoll: { @@ -5542,31 +5831,29 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentLimitReached, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + let text: String + if slowModeEnabled { + text = strongSelf.presentationData.strings.Chat_SlowmodeAttachmentLimitReached + } else { + text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached + } + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }, presentCantSendMultipleFiles: { guard let strongSelf = self else { return } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentMultipleFilesDisabled, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentMultipleFilesDisabled, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }, presentSchedulePicker: { [weak self] done in - guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { - return + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(completion: { [weak self] time in + if let strongSelf = self { + done(time) + if !strongSelf.presentationInterfaceState.isScheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() + } + } + }) } - let mode: ChatScheduleTimeControllerMode - if peer.id == strongSelf.context.account.peerId { - mode = .reminders - } else { - mode = .scheduledMessages(sendWhenOnlineAvailable: peer.id.namespace == Namespaces.Peer.CloudUser) - } - let controller = ChatScheduleTimeController(context: strongSelf.context, mode: mode, minimalTime: strongSelf.presentationInterfaceState.slowmodeState?.timeout, completion: { [weak self] time in - if let strongSelf = self { - done(time) - if !strongSelf.presentationInterfaceState.isScheduledMessages { - strongSelf.openScheduledMessages() - } - } - }) - strongSelf.present(controller, in: .window(.root)) }, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in if !inputText.string.isEmpty { //strongSelf.clearInputText() @@ -5584,6 +5871,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }) } + }, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) }) controller.didDismiss = { [weak legacyController] _ in legacyController?.dismiss() @@ -5605,7 +5894,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } private func presentFileMediaPickerOptions(editingMessage: Bool) { - let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: self.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: self.presentationData.strings.Conversation_FilePhotoOrVideo, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() @@ -5670,7 +5959,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -5694,8 +5983,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText var selectionLimit: Int = 100 + var slowModeEnabled = false if let channel = peer as? TelegramChannel, channel.isRestrictedBySlowmode { selectionLimit = 10 + slowModeEnabled = true } let _ = legacyAssetPicker(context: strongSelf.context, presentationData: strongSelf.presentationData, editingMedia: editingMedia, fileMode: fileMode, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, selectionLimit: selectionLimit).start(next: { generator in @@ -5737,26 +6028,26 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentLimitReached, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, presentSchedulePicker: { [weak self] done in - guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { - return - } - let mode: ChatScheduleTimeControllerMode - if peer.id == strongSelf.context.account.peerId { - mode = .reminders + + let text: String + if slowModeEnabled { + text = strongSelf.presentationData.strings.Chat_SlowmodeAttachmentLimitReached } else { - mode = .scheduledMessages(sendWhenOnlineAvailable: peer.id.namespace == Namespaces.Peer.CloudUser) + text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached + } + + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, presentSchedulePicker: { [weak self] done in + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(completion: { [weak self] time in + if let strongSelf = self { + done(time) + if !strongSelf.presentationInterfaceState.isScheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() + } + } + }) } - let controller = ChatScheduleTimeController(context: strongSelf.context, mode: mode, minimalTime: strongSelf.presentationInterfaceState.slowmodeState?.timeout, completion: { [weak self] time in - if let strongSelf = self { - done(time) - if !strongSelf.presentationInterfaceState.isScheduledMessages { - strongSelf.openScheduledMessages() - } - } - }) - strongSelf.present(controller, in: .window(.root)) }) controller.descriptionGenerator = legacyAssetPickerItemGenerator() controller.completionBlock = { [weak legacyController] signals, silentPosting, scheduleTime in @@ -5811,7 +6102,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - private func presentMapPicker(editingMessage: Bool) { + private func presentLocationPicker() { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } @@ -5823,23 +6114,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let _ = (self.context.account.postbox.transaction { transaction -> Peer? in return transaction.getPeer(selfPeerId) - } - |> deliverOnMainQueue).start(next: { [weak self] selfPeer in - guard let strongSelf = self, let selfPeer = selfPeer else { - return } - - strongSelf.chatDisplayNode.dismissInput() - strongSelf.effectiveNavigationController?.pushViewController(legacyLocationPickerController(context: strongSelf.context, selfPeer: selfPeer, peer: peer, sendLocation: { coordinate, venue, _ in - guard let strongSelf = self else { + |> deliverOnMainQueue).start(next: { [weak self] selfPeer in + guard let strongSelf = self, let selfPeer = selfPeer else { return } - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: venue, liveBroadcastingTimeout: nil)), replyToMessageId: replyMessageId, localGroupingKey: nil) - - if editingMessage { - strongSelf.editMessageMediaWithMessages([message]) - } else { + let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && !strongSelf.presentationInterfaceState.isScheduledMessages + let controller = LocationPickerController(context: strongSelf.context, mode: .share(peer: peer, selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _ in + guard let strongSelf = self else { + return + } + let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: replyMessageId, localGroupingKey: nil) strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { @@ -5848,27 +6134,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) strongSelf.sendMessages([message]) - } - }, sendLiveLocation: { [weak self] coordinate, period in - guard let strongSelf = self else { - return - } - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: period)), replyToMessageId: replyMessageId, localGroupingKey: nil) - if editingMessage { - strongSelf.editMessageMediaWithMessages([message]) - } else { - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } - }) - } - }) - strongSelf.sendMessages([message]) - } - }, theme: strongSelf.presentationData.theme, hasLiveLocation: !strongSelf.presentationInterfaceState.isScheduledMessages)) - }) + }) + strongSelf.effectiveNavigationController?.pushViewController(controller) + strongSelf.chatDisplayNode.dismissInput() + }) } private func presentContactPicker() { @@ -5885,7 +6154,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { return } - let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: []) + let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") let context = strongSelf.context dataSignal = (strongSelf.context.sharedContext.contactDataManager?.basicData() ?? .single([:])) |> take(1) @@ -5962,9 +6231,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } - private func presentPollCreation() { - if case let .peer(peerId) = self.chatLocation { - self.effectiveNavigationController?.pushViewController(createPollController(context: self.context, peerId: peerId, completion: { [weak self] message in + private func presentPollCreation(isQuiz: Bool? = nil) { + if case .peer = self.chatLocation, let peer = self.presentationInterfaceState.renderedPeer?.peer { + self.effectiveNavigationController?.pushViewController(createPollController(context: self.context, peer: peer, isQuiz: isQuiz, completion: { [weak self] message in guard let strongSelf = self else { return } @@ -6027,21 +6296,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) - donateSendMessageIntent(account: self.context.account, sharedContext: self.context.sharedContext, peerIds: [peerId]) + donateSendMessageIntent(account: self.context.account, sharedContext: self.context.sharedContext, intentContext: .chat, peerIds: [peerId]) } else { - let mode: ChatScheduleTimeControllerMode - if peerId == self.context.account.peerId { - mode = .reminders - } else { - mode = .scheduledMessages(sendWhenOnlineAvailable: peerId.namespace == Namespaces.Peer.CloudUser) - } - let controller = ChatScheduleTimeController(context: self.context, mode: mode, minimalTime: self.presentationInterfaceState.slowmodeState?.timeout, dismissByTapOutside: false, completion: { [weak self] time in + self.presentScheduleTimePicker(dismissByTapOutside: false, completion: { [weak self] time in if let strongSelf = self { strongSelf.sendMessages(strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: time), commit: true) } }) - self.chatDisplayNode.dismissInput() - self.present(controller, in: .window(.root)) } } @@ -6207,7 +6468,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.recorderFeedback?.prepareImpact(.light) } - self.videoRecorder.set(.single(legacyInstantVideoController(theme: self.presentationData.theme, panelFrame: self.view.convert(currentInputPanelFrame, to: nil), context: self.context, peerId: peerId, slowmodeState: !self.presentationInterfaceState.isScheduledMessages ? self.presentationInterfaceState.slowmodeState : nil, send: { [weak self] message in + self.videoRecorder.set(.single(legacyInstantVideoController(theme: self.presentationData.theme, panelFrame: self.view.convert(currentInputPanelFrame, to: nil), context: self.context, peerId: peerId, slowmodeState: !self.presentationInterfaceState.isScheduledMessages ? self.presentationInterfaceState.slowmodeState : nil, hasSchedule: !self.presentationInterfaceState.isScheduledMessages && peerId.namespace != Namespaces.Peer.SecretChat, send: { [weak self] message in if let strongSelf = self { let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ @@ -6222,6 +6483,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, displaySlowmodeTooltip: { [weak self] node, rect in self?.interfaceInteraction?.displaySlowmodeTooltip(node, rect) + }, presentSchedulePicker: { [weak self] done in + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(completion: { [weak self] time in + if let strongSelf = self { + done(time) + if !strongSelf.presentationInterfaceState.isScheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() + } + } + }) + } }))) } } @@ -6297,7 +6569,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G videoRecorderValue.completeVideo() self.videoRecorder.set(.single(nil)) } else { - if videoRecorderValue.stopVideo() { + if case .preview = updatedAction, videoRecorderValue.stopVideo() { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInputTextPanelState { panelState in return panelState.withUpdatedMediaRecordingState(.video(status: .editing, isLocked: false)) @@ -6547,7 +6819,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } public func navigateToMessage(messageLocation: NavigateToMessageLocation, animated: Bool, forceInCurrentChat: Bool = false, completion: (() -> Void)? = nil, customPresentProgress: ((ViewController, Any?) -> Void)? = nil) { - self.navigateToMessage(from: nil, to: messageLocation, rememberInStack: false, forceInCurrentChat: forceInCurrentChat, animated: animated, completion: completion, customPresentProgress: customPresentProgress) + let scrollPosition: ListViewScrollPosition + if case .upperBound = messageLocation { + scrollPosition = .top(0.0) + } else { + scrollPosition = .center(.bottom) + } + self.navigateToMessage(from: nil, to: messageLocation, scrollPosition: scrollPosition, rememberInStack: false, forceInCurrentChat: forceInCurrentChat, animated: animated, completion: completion, customPresentProgress: customPresentProgress) } private func navigateToMessage(from fromId: MessageId?, to messageLocation: NavigateToMessageLocation, scrollPosition: ListViewScrollPosition = .center(.bottom), rememberInStack: Bool = true, forceInCurrentChat: Bool = false, animated: Bool = true, completion: (() -> Void)? = nil, customPresentProgress: ((ViewController, Any?) -> Void)? = nil) { @@ -6567,18 +6845,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(messageId.peerId), subject: .message(messageId), keepStack: .always)) } } else if case let .peer(peerId) = self.chatLocation, (messageLocation.peerId == peerId || forceInCurrentChat) { + if let _ = fromId, let fromIndex = fromIndex, rememberInStack { + self.historyNavigationStack.add(fromIndex) + } + + let scrollFromIndex: MessageIndex? if let fromIndex = fromIndex { - if let _ = fromId, rememberInStack { - self.historyNavigationStack.add(fromIndex) - } - + scrollFromIndex = fromIndex + } else if let message = self.chatDisplayNode.historyNode.lastVisbleMesssage() { + scrollFromIndex = message.index + } else { + scrollFromIndex = nil + } + + if let scrollFromIndex = scrollFromIndex { if let messageId = messageLocation.messageId, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { self.loadingMessage.set(false) self.messageIndexDisposable.set(nil) - self.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: message.index, animated: animated, scrollPosition: scrollPosition) + self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, scrollPosition: scrollPosition) completion?() } else if case let .index(index) = messageLocation, index.id.id == 0 && index.timestamp > 0, self.presentationInterfaceState.isScheduledMessages { - self.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: index, animated: animated, scrollPosition: scrollPosition) + self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, scrollPosition: scrollPosition) } else { self.loadingMessage.set(true) let searchLocation: ChatHistoryInitialSearchLocation @@ -6644,12 +6931,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } |> deliverOnMainQueue).start(next: { [weak self] index in if let strongSelf = self, let index = index.0 { - strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: index, animated: animated, scrollPosition: scrollPosition) + strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, scrollPosition: scrollPosition) completion?() } else if index.1 { if !progressStarted { progressStarted = true - progressDisposable.set(progressSignal.start()) + progressDisposable.set(progressSignal.start()) } } }, completed: { [weak self] in @@ -6721,7 +7008,48 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } private func forwardMessages(messageIds: [MessageId], resetCurrent: Bool = false) { - let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.onlyWriteable, .excludeDisabled, .includeSavedMessages])) + let _ = (self.context.account.postbox.transaction { transaction -> [Message] in + return messageIds.compactMap(transaction.getMessage) + } + |> deliverOnMainQueue).start(next: { [weak self] messages in + self?.forwardMessages(messages: messages, resetCurrent: resetCurrent) + }) + } + + private func forwardMessages(messages: [Message], resetCurrent: Bool) { + var filter: ChatListNodePeersFilter = [.onlyWriteable, .includeSavedMessages, .excludeDisabled] + var hasPublicPolls = false + var hasPublicQuiz = false + for message in messages { + for media in message.media { + if let poll = media as? TelegramMediaPoll, case .public = poll.publicity { + hasPublicPolls = true + if case .quiz = poll.kind { + hasPublicQuiz = true + } + filter.insert(.excludeChannels) + break + } + } + } + var attemptSelectionImpl: ((Peer) -> Void)? + let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: filter, attemptSelection: { peer in + attemptSelectionImpl?(peer) + })) + let context = self.context + attemptSelectionImpl = { [weak controller] peer in + guard let controller = controller else { + return + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + if hasPublicPolls { + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + controller.present(textAlertController(context: context, title: nil, text: hasPublicQuiz ? presentationData.strings.Forward_ErrorPublicQuizDisabledInChannels : presentationData.strings.Forward_ErrorPublicPollDisabledInChannels, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + return + } + } + controller.present(textAlertController(context: context, title: nil, text: presentationData.strings.Forward_ErrorDisabledForChat, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } controller.peerSelected = { [weak self, weak controller] peerId in guard let strongSelf = self, let strongController = controller else { return @@ -6732,11 +7060,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if case .peer(peerId) = strongSelf.chatLocation, strongSelf.parentController == nil { - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(messageIds).withoutSelectionState() }) }) + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(messages.map { $0.id }).withoutSelectionState() }) }) strongController.dismiss() } else if peerId == strongSelf.context.account.peerId { - let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messageIds.map { id -> EnqueueMessage in - return .forward(source: id, grouping: .auto, attributes: []) + let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messages.map { message -> EnqueueMessage in + return .forward(source: message.id, grouping: .auto, attributes: []) }) |> deliverOnMainQueue).start(next: { messageIds in if let strongSelf = self { @@ -6772,9 +7100,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = (strongSelf.context.account.postbox.transaction({ transaction -> Void in transaction.updatePeerChatInterfaceState(peerId, update: { currentState in if let currentState = currentState as? ChatInterfaceState { - return currentState.withUpdatedForwardMessageIds(messageIds) + return currentState.withUpdatedForwardMessageIds(messages.map { $0.id }) } else { - return ChatInterfaceState().withUpdatedForwardMessageIds(messageIds) + return ChatInterfaceState().withUpdatedForwardMessageIds(messages.map { $0.id }) } }) }) |> deliverOnMainQueue).start(completed: { @@ -6802,11 +7130,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.effectiveNavigationController?.pushViewController(controller) } - private func openPeer(peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: Message?) { + private func openPeer(peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: Message?, expandAvatar: Bool = false) { if case let .peer(currentPeerId) = self.chatLocation, peerId == currentPeerId { switch navigation { case .info: - self.navigationButtonAction(.openChatInfo) + self.navigationButtonAction(.openChatInfo(expandAvatar: expandAvatar)) case let .chat(textInputState, _): if let textInputState = textInputState { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { @@ -6838,7 +7166,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } self.navigationActionDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self, let peer = peer { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: expandAvatar, fromChat: false) { strongSelf.effectiveNavigationController?.pushViewController(infoController) } } @@ -7019,7 +7347,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatDisplayNode.dismissInput() if let peer = peer as? TelegramChannel, let username = peer.username, !username.isEmpty { - let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ReportSpamAndLeave, color: .destructive, action: { [weak self, weak actionSheet] in @@ -7029,70 +7357,65 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) self.present(actionSheet, in: .window(.root)) - - /*self.present(peerReportOptionsController(context: self.context, subject: .peer(peer.id), present: { [weak self] c, a in - self?.present(c, in: .window(.root)) - }, completion: { [weak self] success in - guard let strongSelf = self, success else { - return - } - let _ = removePeerChat(account: strongSelf.context.account, peerId: chatPeer.id, reportChatSpam: false).start() - (strongSelf.navigationController as? NavigationController)?.filterController(strongSelf, animated: true) - }), in: .window(.root))*/ } else if let _ = peer as? TelegramUser { let presentationData = self.presentationData - let controller = ActionSheetController(presentationTheme: presentationData.theme) + let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } var reportSpam = true var deleteChat = true - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: presentationData.strings.UserInfo_BlockConfirmationTitle(peer.compactDisplayTitle).0), - ActionSheetCheckboxItem(title: presentationData.strings.Conversation_Moderate_Report, label: "", value: reportSpam, action: { [weak controller] checkValue in - reportSpam = checkValue - controller?.updateItem(groupIndex: 0, itemIndex: 1, { item in - if let item = item as? ActionSheetCheckboxItem { - return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) - } - return item - }) - }), - ActionSheetCheckboxItem(title: presentationData.strings.ReportSpam_DeleteThisChat, label: "", value: deleteChat, action: { [weak controller] checkValue in - deleteChat = checkValue - controller?.updateItem(groupIndex: 0, itemIndex: 2, { item in - if let item = item as? ActionSheetCheckboxItem { - return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) - } - return item - }) - }), - ActionSheetButtonItem(title: presentationData.strings.UserInfo_BlockActionTitle(peer.compactDisplayTitle).0, color: .destructive, action: { [weak self] in - dismissAction() - guard let strongSelf = self else { - return - } - let _ = requestUpdatePeerIsBlocked(account: strongSelf.context.account, peerId: peer.id, isBlocked: true).start() - if let _ = chatPeer as? TelegramSecretChat { - let _ = (strongSelf.context.account.postbox.transaction { transaction in - terminateSecretChat(transaction: transaction, peerId: chatPeer.id) - }).start() - } - if deleteChat { - let _ = removePeerChat(account: strongSelf.context.account, peerId: chatPeer.id, reportChatSpam: reportSpam).start() - strongSelf.effectiveNavigationController?.filterController(strongSelf, animated: true) - } else if reportSpam { - let _ = TelegramCore.reportPeer(account: strongSelf.context.account, peerId: peer.id, reason: .spam).start() + var items: [ActionSheetItem] = [] + if !peer.isDeleted { + items.append(ActionSheetTextItem(title: presentationData.strings.UserInfo_BlockConfirmationTitle(peer.compactDisplayTitle).0)) + } + items.append(contentsOf: [ + ActionSheetCheckboxItem(title: presentationData.strings.Conversation_Moderate_Report, label: "", value: reportSpam, action: { [weak controller] checkValue in + reportSpam = checkValue + controller?.updateItem(groupIndex: 0, itemIndex: 1, { item in + if let item = item as? ActionSheetCheckboxItem { + return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) } + return item }) - ]), + }), + ActionSheetCheckboxItem(title: presentationData.strings.ReportSpam_DeleteThisChat, label: "", value: deleteChat, action: { [weak controller] checkValue in + deleteChat = checkValue + controller?.updateItem(groupIndex: 0, itemIndex: 2, { item in + if let item = item as? ActionSheetCheckboxItem { + return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) + } + return item + }) + }), + ActionSheetButtonItem(title: presentationData.strings.UserInfo_BlockActionTitle(peer.compactDisplayTitle).0, color: .destructive, action: { [weak self] in + dismissAction() + guard let strongSelf = self else { + return + } + let _ = requestUpdatePeerIsBlocked(account: strongSelf.context.account, peerId: peer.id, isBlocked: true).start() + if let _ = chatPeer as? TelegramSecretChat { + let _ = (strongSelf.context.account.postbox.transaction { transaction in + terminateSecretChat(transaction: transaction, peerId: chatPeer.id) + }).start() + } + if deleteChat { + let _ = removePeerChat(account: strongSelf.context.account, peerId: chatPeer.id, reportChatSpam: reportSpam).start() + strongSelf.effectiveNavigationController?.filterController(strongSelf, animated: true) + } else if reportSpam { + let _ = TelegramCore.reportPeer(account: strongSelf.context.account, peerId: peer.id, reason: .spam).start() + } + }) + ] as [ActionSheetItem]) + + controller.setItemGroups([ + ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) self.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) @@ -7113,7 +7436,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G title = self.presentationData.strings.Conversation_ReportSpam infoString = self.presentationData.strings.Conversation_ReportSpamConfirmation } - let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] if let infoString = infoString { @@ -7126,7 +7449,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -7148,7 +7471,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumberConfirmation(formatPhoneNumber(phoneNumber), peer.compactDisplayTitle).0)) items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumber, action: { [weak actionSheet] in @@ -7171,7 +7494,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -7241,7 +7564,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } - private func openResolved(_ result: ResolvedUrl) { + private func openResolved(_ result: ResolvedUrl, message: Message? = nil) { self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .chat, navigationController: self.effectiveNavigationController, openPeer: { [weak self] peerId, navigation in guard let strongSelf = self else { return @@ -7259,8 +7582,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.navigationActionDisposable.set((strongSelf.context.account.postbox.loadedPeerWithId(peerId) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peer in - if let strongSelf = self, peer.restrictionText(platform: "ios") == nil { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + if let strongSelf = self, peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { strongSelf.effectiveNavigationController?.pushViewController(infoController) } } @@ -7283,10 +7606,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self?.present(c, in: .window(.root), with: a) }, dismissInput: { [weak self] in self?.chatDisplayNode.dismissInput() - }) + }, contentContext: message) } - private func openUrl(_ url: String, concealed: Bool) { + private func openUrl(_ url: String, concealed: Bool, message: Message? = nil) { self.commitPurposefulAction() let openImpl: () -> Void = { [weak self] in @@ -7329,7 +7652,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { - strongSelf.openResolved(result) + strongSelf.openResolved(result, message: message) } })) } @@ -7405,7 +7728,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let (message, content) = result { switch content { case let .media(media): - var selectedTransitionNode: (ASDisplayNode, () -> (UIView?, UIView?))? + var selectedTransitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? self.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { if let result = itemNode.transitionNode(id: message.id, media: media) { @@ -7476,7 +7799,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(animated: false, transitionArguments: { [weak self] messageId, media in if let strongSelf = self { - var selectedTransitionNode: (ASDisplayNode, () -> (UIView?, UIView?))? + var selectedTransitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { if let result = itemNode.transitionNode(id: messageId, media: media) { @@ -7590,7 +7913,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatDisplayNode.dismissInput() self.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { [weak self] messageId, media in if let strongSelf = self { - var transitionNode: (ASDisplayNode, () -> (UIView?, UIView?))? + var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { if let result = itemNode.transitionNode(id: messageId, media: media) { @@ -7617,7 +7940,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { let canBan = participant?.canBeBannedBy(peerId: accountPeerId) ?? true - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] var actions = Set([0]) @@ -7672,7 +7995,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }).start() let _ = clearAuthorHistory(account: strongSelf.context.account, peerId: peerId, memberId: author.id).start() } else if actions.contains(0) { - let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: Array(messageIds), type: .forEveryone).start() + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start() } if actions.contains(1) { let _ = removePeerMember(account: strongSelf.context.account, peerId: peerId, memberId: author.id).start() @@ -7681,7 +8004,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -7693,7 +8016,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } private func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions, contextController: ContextController?, completion: @escaping (ContextMenuActionResult) -> Void) { - let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] var personalPeerName: String? var isChannel = false @@ -7710,7 +8033,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: Array(messageIds), type: .forEveryone).start() + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start() } })) } @@ -7737,7 +8060,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G contextItems.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { [weak self] _, f in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: Array(messageIds), type: .forEveryone).start() + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start() f(.dismissWithoutContent) } }))) @@ -7745,7 +8068,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: Array(messageIds), type: .forEveryone).start() + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start() } })) } @@ -7767,7 +8090,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G contextItems.append(.action(ContextMenuActionItem(text: localOptionText, textColor: .destructive, icon: { _ in nil }, action: { [weak self] _, f in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).start() + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).start() f(.dismissWithoutContent) } }))) @@ -7775,7 +8098,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).start() + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).start() } })) } @@ -7784,7 +8107,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G contextController.setItems(.single(contextItems)) } else { actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -7800,7 +8123,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) - let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: peer, chatPeer: peer, action: .clearCacheSuggestion, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder)) @@ -7884,7 +8207,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let tooltipController = self.mediaRecordingModeTooltipController { tooltipController.updateContent(.text(text), animated: true, extendTimer: true) } else if let rect = rect { - let tooltipController = TooltipController(content: .text(text)) + let tooltipController = TooltipController(content: .text(text), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize) self.mediaRecordingModeTooltipController = tooltipController tooltipController.dismissed = { [weak self, weak tooltipController] _ in if let strongSelf = self, let tooltipController = tooltipController, strongSelf.mediaRecordingModeTooltipController === tooltipController { @@ -7900,7 +8223,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + private func displaySendingOptionsTooltip() { + guard let rect = self.chatDisplayNode.frameForInputActionButton(), self.effectiveNavigationController?.topViewController === self else { + return + } + self.sendingOptionsTooltipController?.dismiss() + let tooltipController = TooltipController(content: .text(self.presentationData.strings.Conversation_SendingOptionsTooltip), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, timeout: 3.0, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true) + self.sendingOptionsTooltipController = tooltipController + tooltipController.dismissed = { [weak self, weak tooltipController] _ in + if let strongSelf = self, let tooltipController = tooltipController, strongSelf.sendingOptionsTooltipController === tooltipController { + strongSelf.sendingOptionsTooltipController = nil + } + } + self.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + if let strongSelf = self { + return (strongSelf.chatDisplayNode, rect) + } + return nil + })) + } + private func dismissAllTooltips() { + self.sendingOptionsTooltipController?.dismiss() self.searchResultsTooltipController?.dismiss() self.messageTooltipController?.dismiss() self.videoUnmuteTooltipController?.dismiss() @@ -8060,7 +8404,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } public func getTransitionInfo(messageId: MessageId, media: Media) -> ((UIView) -> Void, ASDisplayNode, () -> (UIView?, UIView?))? { - var selectedNode: (ASDisplayNode, () -> (UIView?, UIView?))? + var selectedNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? self.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { if let result = itemNode.transitionNode(id: messageId, media: media) { @@ -8068,7 +8412,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } - if let (node, get) = selectedNode { + if let (node, contentBounds, get) = selectedNode { return ({ [weak self] view in guard let strongSelf = self else { return @@ -8158,9 +8502,41 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } private func openScheduledMessages() { + guard let navigationController = self.effectiveNavigationController, navigationController.topViewController == self else { + return + } let controller = ChatControllerImpl(context: self.context, chatLocation: self.chatLocation, subject: .scheduledMessages) controller.navigationPresentation = .modal - self.effectiveNavigationController?.pushViewController(controller) + navigationController.pushViewController(controller) + } + + private func presentScheduleTimePicker(selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) { + guard case let .peer(peerId) = self.chatLocation else { + return + } + let _ = (self.context.account.viewTracker.peerView(peerId) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] peerView in + guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else { + return + } + var sendWhenOnlineAvailable = false + if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case .present = presence.status { + sendWhenOnlineAvailable = true + } + + let mode: ChatScheduleTimeControllerMode + if peerId == strongSelf.context.account.peerId { + mode = .reminders + } else { + mode = .scheduledMessages(sendWhenOnlineAvailable: sendWhenOnlineAvailable) + } + let controller = ChatScheduleTimeController(context: strongSelf.context, peerId: peerId, mode: mode, currentTime: selectedTime, minimalTime: strongSelf.presentationInterfaceState.slowmodeState?.timeout, dismissByTapOutside: dismissByTapOutside, completion: { time in + completion(time) + }) + strongSelf.chatDisplayNode.dismissInput() + strongSelf.present(controller, in: .window(.root)) + }) } private var effectiveNavigationController: NavigationController? { @@ -8168,10 +8544,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return navigationController } else if case let .inline(navigationController) = self.presentationInterfaceState.mode { return navigationController + } else if case let .overlay(navigationController) = self.presentationInterfaceState.mode { + return navigationController } else { return nil } } + + func activateSearch() { + self.focusOnSearchAfterAppearance = true + self.interfaceInteraction?.beginMessageSearch(.everything, "") + } } private final class ContextControllerContentSourceImpl: ContextControllerContentSource { diff --git a/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift b/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift index a711a465b9..e9b15f43af 100644 --- a/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift @@ -42,10 +42,11 @@ public enum ChatControllerInteractionLongTapAction { case command(String) case hashtag(String) case timecode(Double, String) + case bankCard(String) } struct ChatInterfacePollActionState: Equatable { - var pollMessageIdsInProgress: [MessageId: Data] = [:] + var pollMessageIdsInProgress: [MessageId: [Data]] = [:] } public final class ChatControllerInteraction { @@ -55,6 +56,7 @@ public final class ChatControllerInteraction { let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void let navigateToMessage: (MessageId, MessageId) -> Void + let tapMessage: ((Message) -> Void)? let clickThroughMessage: () -> Void let toggleMessagesSelection: ([MessageId], Bool) -> Void let sendCurrentMessage: (Bool) -> Void @@ -64,7 +66,7 @@ public final class ChatControllerInteraction { let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool) -> Void let requestMessageActionUrlAuth: (String, MessageId, Int32) -> Void let activateSwitchInline: (PeerId?, String) -> Void - let openUrl: (String, Bool, Bool?) -> Void + let openUrl: (String, Bool, Bool?, Message?) -> Void let shareCurrentLocation: () -> Void let shareAccountContact: () -> Void let sendBotCommand: (MessageId?, String) -> Void @@ -90,7 +92,8 @@ public final class ChatControllerInteraction { let requestRedeliveryOfFailedMessages: (MessageId) -> Void let addContact: (String) -> Void let rateCall: (Message, CallId) -> Void - let requestSelectMessagePollOption: (MessageId, Data) -> Void + let requestSelectMessagePollOptions: (MessageId, [Data]) -> Void + let requestOpenMessagePollResults: (MessageId, MediaId) -> Void let openAppStorePage: () -> Void let displayMessageTooltip: (MessageId, String, ASDisplayNode?, CGRect?) -> Void let seekToTimecode: (Message, Double, Bool) -> Void @@ -98,9 +101,12 @@ public final class ChatControllerInteraction { let sendScheduledMessagesNow: ([MessageId]) -> Void let editScheduledMessagesTime: ([MessageId]) -> Void let performTextSelectionAction: (UInt32, String, TextSelectionAction) -> Void - let updateMessageReaction: (MessageId, String) -> Void + let updateMessageReaction: (MessageId, String?) -> Void let openMessageReactions: (MessageId) -> Void let displaySwipeToReplyHint: () -> Void + let dismissReplyMarkupMessage: (Message) -> Void + let openMessagePollResults: (MessageId, Data) -> Void + let openPollCreation: (Bool?) -> Void let requestMessageUpdate: (MessageId) -> Void let cancelInteractiveKeyboardGestures: () -> Void @@ -114,14 +120,16 @@ public final class ChatControllerInteraction { var stickerSettings: ChatInterfaceStickerSettings var searchTextHighightState: (String, [MessageIndex])? var seenOneTimeAnimatedMedia = Set() + var seenDicePointsValue = [MessageId: Int]() - init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { + init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { self.openMessage = openMessage self.openPeer = openPeer self.openPeerMention = openPeerMention self.openMessageContextMenu = openMessageContextMenu self.openMessageContextActions = openMessageContextActions self.navigateToMessage = navigateToMessage + self.tapMessage = tapMessage self.clickThroughMessage = clickThroughMessage self.toggleMessagesSelection = toggleMessagesSelection self.sendCurrentMessage = sendCurrentMessage @@ -157,7 +165,9 @@ public final class ChatControllerInteraction { self.requestRedeliveryOfFailedMessages = requestRedeliveryOfFailedMessages self.addContact = addContact self.rateCall = rateCall - self.requestSelectMessagePollOption = requestSelectMessagePollOption + self.requestSelectMessagePollOptions = requestSelectMessagePollOptions + self.requestOpenMessagePollResults = requestOpenMessagePollResults + self.openPollCreation = openPollCreation self.openAppStorePage = openAppStorePage self.displayMessageTooltip = displayMessageTooltip self.seekToTimecode = seekToTimecode @@ -168,6 +178,8 @@ public final class ChatControllerInteraction { self.updateMessageReaction = updateMessageReaction self.openMessageReactions = openMessageReactions self.displaySwipeToReplyHint = displaySwipeToReplyHint + self.dismissReplyMarkupMessage = dismissReplyMarkupMessage + self.openMessagePollResults = openMessagePollResults self.requestMessageUpdate = requestMessageUpdate self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures @@ -180,7 +192,7 @@ public final class ChatControllerInteraction { static var `default`: ChatControllerInteraction { return ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, navigationController: { return nil }, chatControllerNode: { @@ -194,7 +206,8 @@ public final class ChatControllerInteraction { }, requestRedeliveryOfFailedMessages: { _ in }, addContact: { _ in }, rateCall: { _, _ in - }, requestSelectMessagePollOption: { _, _ in + }, requestSelectMessagePollOptions: { _, _ in + }, requestOpenMessagePollResults: { _, _ in }, openAppStorePage: { }, displayMessageTooltip: { _, _, _, _ in }, seekToTimecode: { _, _, _ in @@ -205,6 +218,9 @@ public final class ChatControllerInteraction { }, updateMessageReaction: { _, _ in }, openMessageReactions: { _ in }, displaySwipeToReplyHint: { + }, dismissReplyMarkupMessage: { _ in + }, openMessagePollResults: { _, _ in + }, openPollCreation: { _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift index 8babf32cbc..53a7b86c40 100644 --- a/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift @@ -77,6 +77,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } let backgroundNode: WallpaperBackgroundNode + let backgroundDisposable = MetaDisposable() let historyNode: ChatHistoryListNode let reactionContainerNode: ReactionSelectionParentNode let historyNodeContainer: ASDisplayNode @@ -92,11 +93,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private let inputPanelBackgroundNode: ASDisplayNode private let inputPanelBackgroundSeparatorNode: ASDisplayNode + private var plainInputSeparatorAlpha: CGFloat? + private var usePlainInputSeparator: Bool private let titleAccessoryPanelContainer: ChatControllerTitlePanelNodeContainer private var titleAccessoryPanelNode: ChatTitleAccessoryPanelNode? private var inputPanelNode: ChatInputPanelNode? + private weak var currentDismissedInputPanelNode: ASDisplayNode? + private var secondaryInputPanelNode: ChatInputPanelNode? private var accessoryPanelNode: AccessoryPanelNode? private var inputContextPanelNode: ChatInputContextPanelNode? private let inputContextPanelContainer: ChatControllerTitlePanelNodeContainer @@ -133,6 +138,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } + private let updatingMessageMediaPromise = Promise<[MessageId: ChatUpdatingMessageMedia]>([:]) + var updatingMessageMedia: [MessageId: ChatUpdatingMessageMedia] = [:] { + didSet { + if self.updatingMessageMedia != oldValue { + self.updatingMessageMediaPromise.set(.single(self.updatingMessageMedia)) + } + } + } + var requestUpdateChatInterfaceState: (Bool, Bool, (ChatInterfaceState) -> ChatInterfaceState) -> Void = { _, _, _ in } var requestUpdateInterfaceState: (ContainedViewLayoutTransition, Bool, (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void = { _, _, _ in } var sendMessages: ([EnqueueMessage], Bool?, Int32?, Bool) -> Void = { _, _, _, _ in } @@ -211,10 +225,17 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.reactionContainerNode = ReactionSelectionParentNode(account: context.account, theme: chatPresentationInterfaceState.theme) - self.loadingNode = ChatLoadingNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper) + self.loadingNode = ChatLoadingNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, bubbleCorners: self.chatPresentationInterfaceState.bubbleCorners) self.inputPanelBackgroundNode = ASDisplayNode() - self.inputPanelBackgroundNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColor + if case let .color(color) = self.chatPresentationInterfaceState.chatWallpaper, UIColor(rgb: color).isEqual(self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper) { + self.inputPanelBackgroundNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper + self.usePlainInputSeparator = true + } else { + self.inputPanelBackgroundNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColor + self.usePlainInputSeparator = false + self.plainInputSeparatorAlpha = nil + } self.inputPanelBackgroundNode.isLayerBacked = true self.inputPanelBackgroundSeparatorNode = ASDisplayNode() @@ -260,10 +281,20 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - self.backgroundNode.image = chatControllerBackgroundImage(theme: chatPresentationInterfaceState.theme, wallpaper: chatPresentationInterfaceState.chatWallpaper, mediaBox: context.sharedContext.accountManager.mediaBox, knockoutMode: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper) + self.backgroundDisposable.set(chatControllerBackgroundImageSignal(wallpaper: chatPresentationInterfaceState.chatWallpaper, mediaBox: context.sharedContext.accountManager.mediaBox, accountMediaBox: context.account.postbox.mediaBox).start(next: { [weak self] image in + if let strongSelf = self, let (image, final) = image { + strongSelf.backgroundNode.image = image + } + })) + if case .gradient = chatPresentationInterfaceState.chatWallpaper { + self.backgroundNode.imageContentMode = .scaleToFill + } else { + self.backgroundNode.imageContentMode = .scaleAspectFill + } self.backgroundNode.motionEnabled = chatPresentationInterfaceState.chatWallpaper.settings?.motion ?? false self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.5, alpha: 0.8) + self.historyNode.enableExtractedBackgrounds = true self.addSubnode(self.backgroundNode) self.addSubnode(self.historyNodeContainer) @@ -471,7 +502,14 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.navigationBar?.isHidden = true } if self.overlayNavigationBar == nil { - let overlayNavigationBar = ChatOverlayNavigationBar(theme: self.chatPresentationInterfaceState.theme, strings: self.chatPresentationInterfaceState.strings, nameDisplayOrder: self.chatPresentationInterfaceState.nameDisplayOrder, close: { [weak self] in + let overlayNavigationBar = ChatOverlayNavigationBar(theme: self.chatPresentationInterfaceState.theme, strings: self.chatPresentationInterfaceState.strings, nameDisplayOrder: self.chatPresentationInterfaceState.nameDisplayOrder, tapped: { [weak self] in + if let strongSelf = self { + strongSelf.dismissAsOverlay() + if case let .peer(id) = strongSelf.chatPresentationInterfaceState.chatLocation { + strongSelf.interfaceInteraction?.navigateToChat(id) + } + } + }, close: { [weak self] in self?.dismissAsOverlay() }) overlayNavigationBar.peerView = self.peerView @@ -546,6 +584,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let inputPanelNode = self.inputPanelNode { previousInputPanelOrigin.y -= inputPanelNode.bounds.size.height } + if let secondaryInputPanelNode = self.secondaryInputPanelNode { + previousInputPanelOrigin.y -= secondaryInputPanelNode.bounds.size.height + } self.containerLayoutAndNavigationBarHeight = (layout, navigationBarHeight) var dismissedTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode? @@ -567,7 +608,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var inputPanelNodeBaseHeight: CGFloat = 0.0 if let inputPanelNode = self.inputPanelNode { - inputPanelNodeBaseHeight = inputPanelNode.minimalHeight(interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) + inputPanelNodeBaseHeight += inputPanelNode.minimalHeight(interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) + } + if let secondaryInputPanelNode = self.secondaryInputPanelNode { + inputPanelNodeBaseHeight += secondaryInputPanelNode.minimalHeight(interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) } let maximumInputNodeHeight = layout.size.height - max(navigationBarHeight, layout.safeInsets.top) - inputPanelNodeBaseHeight @@ -579,7 +623,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { if inputPanelNode.isFocused { self.context.sharedContext.mainWindow?.simulateKeyboardDismiss(transition: .animated(duration: 0.5, curve: .spring)) - //inputTextPanelNode.ensureUnfocused() } } if let inputMediaNode = inputNode as? ChatMediaInputNode, self.inputMediaNode == nil { @@ -640,6 +683,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } var dismissedInputPanelNode: ASDisplayNode? + var dismissedSecondaryInputPanelNode: ASDisplayNode? var dismissedAccessoryPanelNode: ASDisplayNode? var dismissedInputContextPanelNode: ChatInputContextPanelNode? var dismissedOverlayContextPanelNode: ChatInputContextPanelNode? @@ -653,29 +697,54 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var inputPanelSize: CGSize? var immediatelyLayoutInputPanelAndAnimateAppearance = false - if let inputPanelNode = inputPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.inputPanelNode, textInputPanelNode: self.textInputPanelNode, interfaceInteraction: self.interfaceInteraction), !previewing { + var secondaryInputPanelSize: CGSize? + var immediatelyLayoutSecondaryInputPanelAndAnimateAppearance = false + + let inputPanelNodes = inputPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.inputPanelNode, currentSecondaryPanel: self.secondaryInputPanelNode, textInputPanelNode: self.textInputPanelNode, interfaceInteraction: self.interfaceInteraction) + + if let inputPanelNode = inputPanelNodes.primary, !previewing { if inputPanelNode !== self.inputPanelNode { if let inputTextPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { if inputTextPanelNode.isFocused { self.context.sharedContext.mainWindow?.simulateKeyboardDismiss(transition: .animated(duration: 0.5, curve: .spring)) - //inputTextPanelNode.ensureUnfocused() } - let _ = inputTextPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: layout.size.height - insets.top - insets.bottom, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) + let _ = inputTextPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: layout.size.height - insets.top - insets.bottom, isSecondary: false, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) } dismissedInputPanelNode = self.inputPanelNode - immediatelyLayoutInputPanelAndAnimateAppearance = true - let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: layout.size.height - insets.top - insets.bottom, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) + let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: layout.size.height - insets.top - insets.bottom, isSecondary: false, transition: inputPanelNode.supernode == nil ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) inputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) self.inputPanelNode = inputPanelNode - self.insertSubnode(inputPanelNode, aboveSubnode: self.inputPanelBackgroundNode) + if inputPanelNode.supernode == nil { + immediatelyLayoutInputPanelAndAnimateAppearance = true + self.insertSubnode(inputPanelNode, aboveSubnode: self.inputPanelBackgroundNode) + } } else { - let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: layout.size.height - insets.top - insets.bottom, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) + let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: layout.size.height - insets.top - insets.bottom, isSecondary: false, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) inputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) } } else { dismissedInputPanelNode = self.inputPanelNode self.inputPanelNode = nil } + + if let secondaryInputPanelNode = inputPanelNodes.secondary, !previewing { + if secondaryInputPanelNode !== self.secondaryInputPanelNode { + dismissedSecondaryInputPanelNode = self.secondaryInputPanelNode + let inputPanelHeight = secondaryInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: layout.size.height - insets.top - insets.bottom, isSecondary: true, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) + secondaryInputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) + self.secondaryInputPanelNode = secondaryInputPanelNode + if secondaryInputPanelNode.supernode == nil { + immediatelyLayoutSecondaryInputPanelAndAnimateAppearance = true + self.insertSubnode(secondaryInputPanelNode, aboveSubnode: self.inputPanelBackgroundNode) + } + } else { + let inputPanelHeight = secondaryInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: layout.size.height - insets.top - insets.bottom, isSecondary: true, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) + secondaryInputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) + } + } else { + dismissedSecondaryInputPanelNode = self.secondaryInputPanelNode + self.secondaryInputPanelNode = nil + } if let inputMediaNode = self.inputMediaNode, inputMediaNode != self.inputNode { let _ = inputMediaNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, inputHeight: layout.inputHeight ?? 0.0, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelSize?.height ?? 0.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, deviceMetrics: layout.deviceMetrics, isVisible: false) @@ -691,21 +760,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { insets.top += panelHeight } - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - let contentBounds = CGRect(x: 0.0, y: -bottomOverflowOffset, width: layout.size.width - wrappingInsets.left - wrappingInsets.right, height: layout.size.height - wrappingInsets.top - wrappingInsets.bottom) if let backgroundEffectNode = self.backgroundEffectNode { @@ -713,6 +767,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } transition.updateFrame(node: self.backgroundNode, frame: contentBounds) + self.backgroundNode.updateLayout(size: contentBounds.size, transition: transition) transition.updateFrame(node: self.historyNodeContainer, frame: contentBounds) transition.updateBounds(node: self.historyNode, bounds: CGRect(origin: CGPoint(), size: contentBounds.size)) transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.size.width / 2.0, y: contentBounds.size.height / 2.0)) @@ -724,12 +779,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { restrictedNode.updateLayout(size: contentBounds.size, transition: transition) } - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) var accessoryPanelSize: CGSize? var immediatelyLayoutAccessoryPanelAndAnimateAppearance = false @@ -800,8 +850,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var inputPanelsHeight: CGFloat = 0.0 var inputPanelFrame: CGRect? + var secondaryInputPanelFrame: CGRect? + if self.inputPanelNode != nil { - assert(inputPanelSize != nil) inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - bottomOverflowOffset - inputPanelsHeight - inputPanelSize!.height), size: CGSize(width: layout.size.width, height: inputPanelSize!.height)) if self.dismissedAsOverlay { inputPanelFrame!.origin.y = layout.size.height @@ -809,6 +860,14 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { inputPanelsHeight += inputPanelSize!.height } + if self.secondaryInputPanelNode != nil { + secondaryInputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - bottomOverflowOffset - inputPanelsHeight - secondaryInputPanelSize!.height), size: CGSize(width: layout.size.width, height: secondaryInputPanelSize!.height)) + if self.dismissedAsOverlay { + secondaryInputPanelFrame!.origin.y = layout.size.height + } + inputPanelsHeight += secondaryInputPanelSize!.height + } + var accessoryPanelFrame: CGRect? if self.accessoryPanelNode != nil { assert(accessoryPanelSize != nil) @@ -1028,7 +1087,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { listInsets.top = listInsets.top + messageActionSheetControllerAdditionalInset } - listViewTransaction(ListViewUpdateSizeAndInsets(size: contentBounds.size, insets: listInsets, scrollIndicatorInsets: listScrollIndicatorInsets, duration: duration, curve: listViewCurve, ensureTopInsetForOverlayHighlightedItems: ensureTopInsetForOverlayHighlightedItems), additionalScrollDistance, scrollToTop, { [weak self] in + listViewTransaction(ListViewUpdateSizeAndInsets(size: contentBounds.size, insets: listInsets, scrollIndicatorInsets: listScrollIndicatorInsets, duration: duration, curve: curve, ensureTopInsetForOverlayHighlightedItems: ensureTopInsetForOverlayHighlightedItems), additionalScrollDistance, scrollToTop, { [weak self] in if let strongSelf = self { strongSelf.notifyTransitionCompletionListeners(transition: transition) } @@ -1041,6 +1100,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } var apparentInputPanelFrame = inputPanelFrame + var apparentSecondaryInputPanelFrame = secondaryInputPanelFrame var apparentInputBackgroundFrame = inputBackgroundFrame var apparentNavigateButtonsFrame = navigateButtonsFrame if case let .media(_, maybeExpanded) = self.chatPresentationInterfaceState.inputMode, let expanded = maybeExpanded, case .search = expanded, let inputPanelFrame = inputPanelFrame { @@ -1068,11 +1128,26 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { inputPanelNode.frame = apparentInputPanelFrame.offsetBy(dx: 0.0, dy: apparentInputPanelFrame.height + previousInputPanelBackgroundFrame.maxY - apparentInputBackgroundFrame.maxY) inputPanelNode.alpha = 0.0 } - + if !transition.isAnimated { + inputPanelNode.layer.removeAllAnimations() + if let currentDismissedInputPanelNode = self.currentDismissedInputPanelNode, inputPanelNode is ChatSearchInputPanelNode { + currentDismissedInputPanelNode.layer.removeAllAnimations() + } + } transition.updateFrame(node: inputPanelNode, frame: apparentInputPanelFrame) transition.updateAlpha(node: inputPanelNode, alpha: 1.0) } + if let secondaryInputPanelNode = self.secondaryInputPanelNode, let apparentSecondaryInputPanelFrame = apparentSecondaryInputPanelFrame, !secondaryInputPanelNode.frame.equalTo(apparentSecondaryInputPanelFrame) { + if immediatelyLayoutSecondaryInputPanelAndAnimateAppearance { + secondaryInputPanelNode.frame = apparentSecondaryInputPanelFrame.offsetBy(dx: 0.0, dy: apparentSecondaryInputPanelFrame.height + previousInputPanelBackgroundFrame.maxY - apparentSecondaryInputPanelFrame.maxY) + secondaryInputPanelNode.alpha = 0.0 + } + + transition.updateFrame(node: secondaryInputPanelNode, frame: apparentSecondaryInputPanelFrame) + transition.updateAlpha(node: secondaryInputPanelNode, alpha: 1.0) + } + if let accessoryPanelNode = self.accessoryPanelNode, let accessoryPanelFrame = accessoryPanelFrame, !accessoryPanelNode.frame.equalTo(accessoryPanelFrame) { if immediatelyLayoutAccessoryPanelAndAnimateAppearance { var startAccessoryPanelFrame = accessoryPanelFrame @@ -1147,15 +1222,22 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { }) } - if let dismissedInputPanelNode = dismissedInputPanelNode { + if let dismissedInputPanelNode = dismissedInputPanelNode, dismissedInputPanelNode !== self.secondaryInputPanelNode { var frameCompleted = false var alphaCompleted = false + self.currentDismissedInputPanelNode = dismissedInputPanelNode let completed = { [weak self, weak dismissedInputPanelNode] in - if let strongSelf = self, let dismissedInputPanelNode = dismissedInputPanelNode, strongSelf.inputPanelNode === dismissedInputPanelNode { + guard let strongSelf = self, let dismissedInputPanelNode = dismissedInputPanelNode else { + return + } + if strongSelf.currentDismissedInputPanelNode === dismissedInputPanelNode { + strongSelf.currentDismissedInputPanelNode = nil + } + if strongSelf.inputPanelNode === dismissedInputPanelNode { return } if frameCompleted && alphaCompleted { - dismissedInputPanelNode?.removeFromSupernode() + dismissedInputPanelNode.removeFromSupernode() } } let transitionTargetY = layout.size.height - insets.bottom @@ -1170,6 +1252,29 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { }) } + if let dismissedSecondaryInputPanelNode = dismissedSecondaryInputPanelNode, dismissedSecondaryInputPanelNode !== self.inputPanelNode { + var frameCompleted = false + var alphaCompleted = false + let completed = { [weak self, weak dismissedSecondaryInputPanelNode] in + if let strongSelf = self, let dismissedSecondaryInputPanelNode = dismissedSecondaryInputPanelNode, strongSelf.secondaryInputPanelNode === dismissedSecondaryInputPanelNode { + return + } + if frameCompleted && alphaCompleted { + dismissedSecondaryInputPanelNode?.removeFromSupernode() + } + } + let transitionTargetY = layout.size.height - insets.bottom + transition.updateFrame(node: dismissedSecondaryInputPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: transitionTargetY), size: dismissedSecondaryInputPanelNode.frame.size), completion: { _ in + frameCompleted = true + completed() + }) + + transition.updateAlpha(node: dismissedSecondaryInputPanelNode, alpha: 0.0, completion: { _ in + alphaCompleted = true + completed() + }) + } + if let dismissedAccessoryPanelNode = dismissedAccessoryPanelNode { var frameCompleted = false var alphaCompleted = false @@ -1301,6 +1406,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.performAnimateInAsOverlay(from: scheduledAnimateInAsOverlayFromNode, transition: animatedTransition) } + self.updatePlainInputSeparator(transition: transition) + self.derivedLayoutState = ChatControllerNodeDerivedLayoutState(inputContextPanelsFrame: inputContextPanelsFrame, inputContextPanelsOverMainPanelFrame: inputContextPanelsOverMainPanelFrame, inputNodeHeight: inputNodeHeightAndOverflow?.0, upperInputPositionBound: inputNodeHeightAndOverflow?.0 != nil ? self.upperInputPositionBound : nil) //self.notifyTransitionCompletionListeners(transition: transition) @@ -1342,7 +1449,17 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let themeUpdated = self.chatPresentationInterfaceState.theme !== chatPresentationInterfaceState.theme if self.chatPresentationInterfaceState.chatWallpaper != chatPresentationInterfaceState.chatWallpaper { + self.backgroundDisposable.set(chatControllerBackgroundImageSignal(wallpaper: chatPresentationInterfaceState.chatWallpaper, mediaBox: self.context.sharedContext.accountManager.mediaBox, accountMediaBox: self.context.account.postbox.mediaBox).start(next: { [weak self] image in + if let strongSelf = self, let (image, final) = image { + strongSelf.backgroundNode.image = image + } + })) self.backgroundNode.image = chatControllerBackgroundImage(theme: chatPresentationInterfaceState.theme, wallpaper: chatPresentationInterfaceState.chatWallpaper, mediaBox: context.sharedContext.accountManager.mediaBox, knockoutMode: self.context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper) + if case .gradient = chatPresentationInterfaceState.chatWallpaper { + self.backgroundNode.imageContentMode = .scaleToFill + } else { + self.backgroundNode.imageContentMode = .scaleAspectFill + } self.backgroundNode.motionEnabled = chatPresentationInterfaceState.chatWallpaper.settings?.motion ?? false } @@ -1355,7 +1472,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.navigateButtons.updateTheme(theme: chatPresentationInterfaceState.theme) if themeUpdated { - self.inputPanelBackgroundNode.backgroundColor = chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColor + if case let .color(color) = self.chatPresentationInterfaceState.chatWallpaper, UIColor(rgb: color).isEqual(self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper) { + self.inputPanelBackgroundNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper + self.usePlainInputSeparator = true + } else { + self.inputPanelBackgroundNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColor + self.usePlainInputSeparator = false + self.plainInputSeparatorAlpha = nil + } + self.updatePlainInputSeparator(transition: .immediate) self.inputPanelBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelSeparatorColor } @@ -1376,7 +1501,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } var restrictionText: String? - if let peer = chatPresentationInterfaceState.renderedPeer?.peer, let restrictionTextValue = peer.restrictionText(platform: "ios"), !restrictionTextValue.isEmpty { + if let peer = chatPresentationInterfaceState.renderedPeer?.peer, let restrictionTextValue = peer.restrictionText(platform: "ios", contentSettings: self.context.currentContentSettings.with { $0 }), !restrictionTextValue.isEmpty { restrictionText = restrictionTextValue } else if chatPresentationInterfaceState.isNotAccessible { if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = peer.info { @@ -1388,7 +1513,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let restrictionText = restrictionText { if self.restrictedNode == nil { - let restrictedNode = ChatRecentActionsEmptyNode(theme: chatPresentationInterfaceState.theme, chatWallpaper: chatPresentationInterfaceState.chatWallpaper) + let restrictedNode = ChatRecentActionsEmptyNode(theme: chatPresentationInterfaceState.theme, chatWallpaper: chatPresentationInterfaceState.chatWallpaper, chatBubbleCorners: chatPresentationInterfaceState.bubbleCorners) self.historyNodeContainer.supernode?.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer) self.restrictedNode = restrictedNode } @@ -1450,7 +1575,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { if inputPanelNode.isFocused { self.context.sharedContext.mainWindow?.simulateKeyboardDismiss(transition: .animated(duration: 0.5, curve: .spring)) - //inputTextPanelNode.ensureUnfocused() } } } @@ -1564,13 +1688,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.setNeedsLayout() } - func loadInputPanels(theme: PresentationTheme, strings: PresentationStrings) { + func loadInputPanels(theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { if self.inputMediaNode == nil { var peerId: PeerId? if case let .peer(id) = self.chatPresentationInterfaceState.chatLocation { peerId = id } - let inputNode = ChatMediaInputNode(context: self.context, peerId: peerId, controllerInteraction: self.controllerInteraction, theme: theme, strings: strings, gifPaneIsActiveUpdated: { [weak self] value in + let inputNode = ChatMediaInputNode(context: self.context, peerId: peerId, controllerInteraction: self.controllerInteraction, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, theme: theme, strings: strings, fontSize: fontSize, gifPaneIsActiveUpdated: { [weak self] value in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { interfaceInteraction.updateInputModeAndDismissedButtonKeyboardMessageId { state in if case let .media(_, expanded) = state.inputMode { @@ -2155,9 +2279,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { textInputPanelNode.text = "" strongSelf.requestUpdateChatInterfaceState(false, true, { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil).withUpdatedComposeDisableUrlPreview(nil) }) strongSelf.ignoreUpdateHeight = false - completion() } }) + completion() if let forwardMessageIds = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds { for id in forwardMessageIds { @@ -2184,4 +2308,153 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { completion?() }) } + + func setEnablePredictiveTextInput(_ value: Bool) { + self.textInputPanelNode?.enablePredictiveInput = value + } + + func updatePlainInputSeparatorAlpha(_ value: CGFloat, transition: ContainedViewLayoutTransition) { + if self.plainInputSeparatorAlpha != value { + let immediate = self.plainInputSeparatorAlpha == nil + self.plainInputSeparatorAlpha = value + self.updatePlainInputSeparator(transition: immediate ? .immediate : transition) + } + } + + func updatePlainInputSeparator(transition: ContainedViewLayoutTransition) { + let resolvedValue: CGFloat + if self.accessoryPanelNode != nil { + resolvedValue = 1.0 + } else if self.usePlainInputSeparator { + resolvedValue = self.plainInputSeparatorAlpha ?? 0.0 + } else { + resolvedValue = 1.0 + } + + if resolvedValue != self.inputPanelBackgroundSeparatorNode.alpha { + transition.updateAlpha(node: self.inputPanelBackgroundSeparatorNode, alpha: resolvedValue, beginWithCurrentState: true) + } + } + + func animateQuizCorrectOptionSelected() { + self.view.insertSubview(ConfettiView(frame: self.view.bounds), aboveSubview: self.historyNode.view) + + /*class ConfettiView: UIView { + private let direction: Bool + private let confettiViewEmitterLayer = CAEmitterLayer() + private let confettiViewEmitterCell = CAEmitterCell() + + init(frame: CGRect, direction: Bool) { + self.direction = direction + + super.init(frame: frame) + + self.isUserInteractionEnabled = false + + self.setupConfettiEmitterLayer() + + self.confettiViewEmitterLayer.frame = self.bounds + self.confettiViewEmitterLayer.emitterCells = generateConfettiEmitterCells() + self.layer.addSublayer(self.confettiViewEmitterLayer) + + let animation = CAKeyframeAnimation(keyPath: #keyPath(CAEmitterLayer.birthRate)) + animation.duration = 0.5 + animation.timingFunction = CAMediaTimingFunction(name: .easeIn) + animation.fillMode = .forwards + animation.values = [1, 0, 0] + animation.keyTimes = [0, 0.5, 1] + animation.isRemovedOnCompletion = false + + self.confettiViewEmitterLayer.beginTime = CACurrentMediaTime() + self.confettiViewEmitterLayer.birthRate = 1.0 + + CATransaction.begin() + CATransaction.setCompletionBlock { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, delay: 1.0, removeOnCompletion: false, completion: { _ in + self?.removeFromSuperview() + }) + } + self.confettiViewEmitterLayer.add(animation, forKey: nil) + CATransaction.commit() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupConfettiEmitterLayer() { + let emitterWidth: CGFloat = self.bounds.width / 4.0 + self.confettiViewEmitterLayer.emitterSize = CGSize(width: emitterWidth, height: 2.0) + self.confettiViewEmitterLayer.emitterShape = .line + self.confettiViewEmitterLayer.emitterPosition = CGPoint(x: direction ? 0.0 : (self.bounds.width - emitterWidth * 0.0), y: self.bounds.height) + } + + private func generateConfettiEmitterCells() -> [CAEmitterCell] { + var cells = [CAEmitterCell]() + + let cellImageCircle = generateFilledCircleImage(diameter: 4.0, color: .white)!.cgImage! + let cellImageLine = generateImage(CGSize(width: 4.0, height: 10.0), opaque: false, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.width))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height - size.width), size: CGSize(width: size.width, height: size.width))) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.width / 2.0), size: CGSize(width: size.width, height: size.height - size.width))) + })!.cgImage! + + for index in 0 ..< 4 { + let cell = CAEmitterCell() + cell.color = self.nextColor(i: index).cgColor + cell.contents = index % 2 == 0 ? cellImageCircle : cellImageLine + cell.birthRate = 60.0 + cell.lifetime = 14.0 + cell.lifetimeRange = 0 + if index % 2 == 0 { + cell.scale = 0.8 + cell.scaleRange = 0.4 + } else { + cell.scale = 0.5 + cell.scaleRange = 0.1 + } + cell.velocity = -self.randomVelocity + cell.velocityRange = abs(cell.velocity) * 0.3 + cell.yAcceleration = 3000.0 + cell.emissionLongitude = (self.direction ? -1.0 : 1.0) * (CGFloat.pi * 0.95) + cell.emissionRange = 0.2 + cell.spin = 5.5 + cell.spinRange = 1.0 + + cells.append(cell) + } + + return cells + } + + var randomNumber: Int { + let dimension = 4 + return Int(arc4random_uniform(UInt32(dimension))) + } + + var randomVelocity: CGFloat { + let velocities: [CGFloat] = [100.0, 120.0, 130.0, 140.0] + return velocities[self.randomNumber] * 12.0 + } + + private let colors: [UIColor] = ([ + 0x56CE6B, + 0xCD89D0, + 0x1E9AFF, + 0xFF8724 + ] as [UInt32]).map(UIColor.init(rgb:)) + + private func nextColor(i: Int) -> UIColor { + return self.colors[i % self.colors.count] + } + } + + self.view.insertSubview(ConfettiView(frame: self.view.bounds, direction: true), aboveSubview: self.historyNode.view) + self.view.insertSubview(ConfettiView(frame: self.view.bounds, direction: false), aboveSubview: self.historyNode.view)*/ + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatDateSelectionSheet.swift b/submodules/TelegramUI/TelegramUI/ChatDateSelectionSheet.swift index 8d2dd7a994..36a3ef2686 100644 --- a/submodules/TelegramUI/TelegramUI/ChatDateSelectionSheet.swift +++ b/submodules/TelegramUI/TelegramUI/ChatDateSelectionSheet.swift @@ -15,20 +15,20 @@ final class ChatDateSelectionSheet: ActionSheetController { return self._ready } - init(theme: PresentationTheme, strings: PresentationStrings, completion: @escaping (Int32) -> Void) { - self.strings = strings + init(presentationData: PresentationData, completion: @escaping (Int32) -> Void) { + self.strings = presentationData.strings - super.init(theme: ActionSheetControllerTheme(presentationTheme: theme)) + super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) self._ready.set(.single(true)) var updatedValue: Int32? self.setItemGroups([ ActionSheetItemGroup(items: [ - ChatDateSelectorItem(strings: strings, valueChanged: { value in + ChatDateSelectorItem(strings: self.strings, valueChanged: { value in updatedValue = value }), - ActionSheetButtonItem(title: strings.Common_Search, action: { [weak self] in + ActionSheetButtonItem(title: self.strings.Common_Search, action: { [weak self] in self?.dismissAnimated() if let updatedValue = updatedValue { completion(updatedValue) @@ -36,7 +36,7 @@ final class ChatDateSelectionSheet: ActionSheetController { }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in + ActionSheetButtonItem(title: self.strings.Common_Cancel, action: { [weak self] in self?.dismissAnimated() }), ]) diff --git a/submodules/TelegramUI/TelegramUI/ChatEditMessageMediaContext.swift b/submodules/TelegramUI/TelegramUI/ChatEditMessageMediaContext.swift new file mode 100644 index 0000000000..a20222c70b --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/ChatEditMessageMediaContext.swift @@ -0,0 +1,32 @@ +import Foundation +import SwiftSignalKit +import Postbox +import SyncCore +import TelegramCore +import AccountContext + +private final class MessageContext { + let disposable: Disposable + + init(disposable: Disposable) { + self.disposable = disposable + } + + deinit { + self.disposable.dispose() + } +} + +final class ChatEditMessageMediaContext { + private let context: AccountContext + + private let contexts: [MessageId: MessageContext] = [:] + + init(context: AccountContext) { + self.context = context + } + + func update(id: MessageId, text: String, entities: TextEntitiesMessageAttribute?, disableUrlPreview: Bool, media: RequestEditMessageMedia) { + + } +} diff --git a/submodules/TelegramUI/TelegramUI/ChatEmptyNode.swift b/submodules/TelegramUI/TelegramUI/ChatEmptyNode.swift index 57b0fef12d..c0f8623b98 100644 --- a/submodules/TelegramUI/TelegramUI/ChatEmptyNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatEmptyNode.swift @@ -121,7 +121,7 @@ private final class ChatEmptyNodeSecretChatContent: ASDisplayNode, ChatEmptyNode let lines: [NSAttributedString] = strings.map { NSAttributedString(string: $0, font: messageFont, textColor: serviceColor.primaryText) } - let graphics = PresentationResourcesChat.additionalGraphics(interfaceState.theme, wallpaper: interfaceState.chatWallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(interfaceState.theme, wallpaper: interfaceState.chatWallpaper, bubbleCorners: interfaceState.bubbleCorners) let lockIcon = graphics.chatEmptyItemLockIcon for i in 0 ..< lines.count { @@ -237,7 +237,7 @@ private final class ChatEmptyNodeGroupChatContent: ASDisplayNode, ChatEmptyNodeC let lines: [NSAttributedString] = strings.map { NSAttributedString(string: $0, font: messageFont, textColor: serviceColor.primaryText) } - let graphics = PresentationResourcesChat.additionalGraphics(interfaceState.theme, wallpaper: interfaceState.chatWallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(interfaceState.theme, wallpaper: interfaceState.chatWallpaper, bubbleCorners: interfaceState.bubbleCorners) let lockIcon = graphics.emptyChatListCheckIcon for i in 0 ..< lines.count { @@ -453,7 +453,7 @@ final class ChatEmptyNode: ASDisplayNode { self.currentTheme = interfaceState.theme self.currentStrings = interfaceState.strings - let graphics = PresentationResourcesChat.additionalGraphics(interfaceState.theme, wallpaper: interfaceState.chatWallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(interfaceState.theme, wallpaper: interfaceState.chatWallpaper, bubbleCorners: interfaceState.bubbleCorners) self.backgroundNode.image = graphics.chatEmptyItemBackgroundImage } diff --git a/submodules/TelegramUI/TelegramUI/ChatFeedNavigationInputPanelNode.swift b/submodules/TelegramUI/TelegramUI/ChatFeedNavigationInputPanelNode.swift index 0fadbb4b0d..215edbb68f 100644 --- a/submodules/TelegramUI/TelegramUI/ChatFeedNavigationInputPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatFeedNavigationInputPanelNode.swift @@ -54,7 +54,7 @@ final class ChatFeedNavigationInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.navigateFeed() } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState } diff --git a/submodules/TelegramUI/TelegramUI/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/TelegramUI/ChatHistoryEntriesForView.swift index bca596e929..3fedeb8987 100644 --- a/submodules/TelegramUI/TelegramUI/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/TelegramUI/ChatHistoryEntriesForView.swift @@ -5,7 +5,7 @@ import SyncCore import TemporaryCachedPeerDataManager import Emoji -func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, includeUnreadEntry: Bool, includeEmptyEntry: Bool, includeChatInfoEntry: Bool, includeSearchEntry: Bool, reverse: Bool, groupMessages: Bool, selectedMessages: Set?, presentationData: ChatPresentationData, historyAppearsCleared: Bool, associatedData: ChatMessageItemAssociatedData) -> [ChatHistoryEntry] { +func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, includeUnreadEntry: Bool, includeEmptyEntry: Bool, includeChatInfoEntry: Bool, includeSearchEntry: Bool, reverse: Bool, groupMessages: Bool, selectedMessages: Set?, presentationData: ChatPresentationData, historyAppearsCleared: Bool, associatedData: ChatMessageItemAssociatedData, updatingMedia: [MessageId: ChatUpdatingMessageMedia]) -> [ChatHistoryEntry] { if historyAppearsCleared { return [] } @@ -48,7 +48,7 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, if presentationData.largeEmoji, entry.message.media.isEmpty { if stickersEnabled && entry.message.text.count == 1, let _ = associatedData.animatedEmojiStickers[entry.message.text.basicEmoji.0] { contentTypeHint = .animatedEmoji - } else if messageIsElligibleForLargeEmoji(entry.message) { + } else if entry.message.text.count < 10 && messageIsElligibleForLargeEmoji(entry.message) { contentTypeHint = .largeEmoji } } @@ -65,7 +65,7 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, } else { selection = .none } - groupBucket.append((entry.message, entry.isRead, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint))) + groupBucket.append((entry.message, entry.isRead, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[entry.message.id]))) } else { let selection: ChatHistoryMessageSelection if let selectedMessages = selectedMessages { @@ -73,7 +73,7 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, } else { selection = .none } - entries.append(.MessageEntry(entry.message, presentationData, entry.isRead, entry.monthLocation, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint))) + entries.append(.MessageEntry(entry.message, presentationData, entry.isRead, entry.monthLocation, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[entry.message.id]))) } } else { let selection: ChatHistoryMessageSelection @@ -82,7 +82,7 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, } else { selection = .none } - entries.append(.MessageEntry(entry.message, presentationData, entry.isRead, entry.monthLocation, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint))) + entries.append(.MessageEntry(entry.message, presentationData, entry.isRead, entry.monthLocation, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[entry.message.id]))) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatHistoryEntry.swift b/submodules/TelegramUI/TelegramUI/ChatHistoryEntry.swift index 89a6fb2242..05488d9409 100644 --- a/submodules/TelegramUI/TelegramUI/ChatHistoryEntry.swift +++ b/submodules/TelegramUI/TelegramUI/ChatHistoryEntry.swift @@ -37,17 +37,20 @@ public struct ChatMessageEntryAttributes: Equatable { let rank: CachedChannelAdminRank? let isContact: Bool let contentTypeHint: ChatMessageEntryContentType + let updatingMedia: ChatUpdatingMessageMedia? - init(rank: CachedChannelAdminRank?, isContact: Bool, contentTypeHint: ChatMessageEntryContentType) { + init(rank: CachedChannelAdminRank?, isContact: Bool, contentTypeHint: ChatMessageEntryContentType, updatingMedia: ChatUpdatingMessageMedia?) { self.rank = rank self.isContact = isContact self.contentTypeHint = contentTypeHint + self.updatingMedia = updatingMedia } public init() { self.rank = nil self.isContact = false self.contentTypeHint = .generic + self.updatingMedia = nil } } diff --git a/submodules/TelegramUI/TelegramUI/ChatHistoryGridNode.swift b/submodules/TelegramUI/TelegramUI/ChatHistoryGridNode.swift index 725d90456c..f66ed95d0d 100644 --- a/submodules/TelegramUI/TelegramUI/ChatHistoryGridNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatHistoryGridNode.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramCore import SyncCore import TelegramPresentationData +import TelegramUIPreferences import AccountContext private class ChatGridLiveSelectorRecognizer: UIPanGestureRecognizer { @@ -73,11 +74,11 @@ struct ChatHistoryGridViewTransition { let stationaryItems: GridNodeStationaryItems } -private func mappedInsertEntries(context: AccountContext, peerId: PeerId, controllerInteraction: ChatControllerInteraction, entries: [ChatHistoryViewTransitionInsertEntry], theme: PresentationTheme, strings: PresentationStrings) -> [GridNodeInsertItem] { +private func mappedInsertEntries(context: AccountContext, peerId: PeerId, controllerInteraction: ChatControllerInteraction, entries: [ChatHistoryViewTransitionInsertEntry], theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) -> [GridNodeInsertItem] { return entries.map { entry -> GridNodeInsertItem in switch entry.entry { case let .MessageEntry(message, _, _, _, _, _): - return GridNodeInsertItem(index: entry.index, item: GridMessageItem(theme: theme, strings: strings, context: context, message: message, controllerInteraction: controllerInteraction), previousIndex: entry.previousIndex) + return GridNodeInsertItem(index: entry.index, item: GridMessageItem(theme: theme, strings: strings, fontSize: fontSize, context: context, message: message, controllerInteraction: controllerInteraction), previousIndex: entry.previousIndex) case .MessageGroupEntry: return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) case .UnreadEntry: @@ -90,11 +91,11 @@ private func mappedInsertEntries(context: AccountContext, peerId: PeerId, contro } } -private func mappedUpdateEntries(context: AccountContext, peerId: PeerId, controllerInteraction: ChatControllerInteraction, entries: [ChatHistoryViewTransitionUpdateEntry], theme: PresentationTheme, strings: PresentationStrings) -> [GridNodeUpdateItem] { +private func mappedUpdateEntries(context: AccountContext, peerId: PeerId, controllerInteraction: ChatControllerInteraction, entries: [ChatHistoryViewTransitionUpdateEntry], theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) -> [GridNodeUpdateItem] { return entries.map { entry -> GridNodeUpdateItem in switch entry.entry { case let .MessageEntry(message, _, _, _, _, _): - return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridMessageItem(theme: theme, strings: strings, context: context, message: message, controllerInteraction: controllerInteraction)) + return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridMessageItem(theme: theme, strings: strings, fontSize: fontSize, context: context, message: message, controllerInteraction: controllerInteraction)) case .MessageGroupEntry: return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) case .UnreadEntry: @@ -189,7 +190,7 @@ private func mappedChatHistoryViewListTransition(context: AccountContext, peerId } } - return ChatHistoryGridViewTransition(historyView: transition.historyView, topOffsetWithinMonth: topOffsetWithinMonth, deleteItems: transition.deleteItems.map { $0.index }, insertItems: mappedInsertEntries(context: context, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.insertEntries, theme: presentationData.theme.theme, strings: presentationData.strings), updateItems: mappedUpdateEntries(context: context, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.updateEntries, theme: presentationData.theme.theme, strings: presentationData.strings), scrollToItem: mappedScrollToItem, stationaryItems: stationaryItems) + return ChatHistoryGridViewTransition(historyView: transition.historyView, topOffsetWithinMonth: topOffsetWithinMonth, deleteItems: transition.deleteItems.map { $0.index }, insertItems: mappedInsertEntries(context: context, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.insertEntries, theme: presentationData.theme.theme, strings: presentationData.strings, fontSize: presentationData.fontSize), updateItems: mappedUpdateEntries(context: context, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.updateEntries, theme: presentationData.theme.theme, strings: presentationData.strings, fontSize: presentationData.fontSize), scrollToItem: mappedScrollToItem, stationaryItems: stationaryItems) } private func gridNodeLayoutForContainerLayout(size: CGSize) -> GridNodeLayoutType { @@ -250,7 +251,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { self.chatPresentationDataPromise.set(context.sharedContext.presentationData |> map { presentationData in - return ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations, largeEmoji: presentationData.largeEmoji) + return ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper), fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners) }) self.floatingSections = true @@ -304,7 +305,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { } let associatedData = ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: false) - let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(location: .peer(peerId), view: view, includeUnreadEntry: false, includeEmptyEntry: false, includeChatInfoEntry: false, includeSearchEntry: false, reverse: false, groupMessages: false, selectedMessages: nil, presentationData: chatPresentationData, historyAppearsCleared: false, associatedData: associatedData), associatedData: associatedData, id: id) + let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(location: .peer(peerId), view: view, includeUnreadEntry: false, includeEmptyEntry: false, includeChatInfoEntry: false, includeSearchEntry: false, reverse: false, groupMessages: false, selectedMessages: nil, presentationData: chatPresentationData, historyAppearsCleared: false, associatedData: associatedData, updatingMedia: [:]), associatedData: associatedData, lastHeaderId: 0, id: id) let previous = previousView.swap(processedView) let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: false, chatLocation: .peer(peerId), controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil, cachedDataMessages: nil, readStateData: nil, flashIndicators: flashIndicators, updatedMessageSelection: false) @@ -350,30 +351,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { public override func didLoad() { super.didLoad() } - - private var liveSelectingState: (selecting: Bool, currentMessageId: MessageId)? - - @objc private func panGesture(_ recognizer: UIGestureRecognizer) -> Void { - guard let selectionState = controllerInteraction.selectionState else {return} - switch recognizer.state { - case .began: - if let itemNode = self.itemNodeAtPoint(recognizer.location(in: self.view)) as? GridMessageItemNode, let messageId = itemNode.messageId { - liveSelectingState = (selecting: !selectionState.selectedIds.contains(messageId), currentMessageId: messageId) - controllerInteraction.toggleMessagesSelection([messageId], !selectionState.selectedIds.contains(messageId)) - } - case .changed: - if let liveSelectingState = liveSelectingState, let itemNode = self.itemNodeAtPoint(recognizer.location(in: self.view)) as? GridMessageItemNode, let messageId = itemNode.messageId, messageId != liveSelectingState.currentMessageId { - controllerInteraction.toggleMessagesSelection([messageId], liveSelectingState.selecting) - self.liveSelectingState?.currentMessageId = messageId - } - case .ended, .failed, .cancelled: - liveSelectingState = nil - case .possible: - break - } - } - required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -509,4 +487,27 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { public func disconnect() { self.historyDisposable.set(nil) } + + private var selectionPanState: (selecting: Bool, currentMessageId: MessageId)? + + @objc private func panGesture(_ recognizer: UIGestureRecognizer) -> Void { + guard let selectionState = self.controllerInteraction.selectionState else {return} + + switch recognizer.state { + case .began: + if let itemNode = self.itemNodeAtPoint(recognizer.location(in: self.view)) as? GridMessageItemNode, let messageId = itemNode.messageId { + self.selectionPanState = (selecting: !selectionState.selectedIds.contains(messageId), currentMessageId: messageId) + self.controllerInteraction.toggleMessagesSelection([messageId], !selectionState.selectedIds.contains(messageId)) + } + case .changed: + if let selectionPanState = self.selectionPanState, let itemNode = self.itemNodeAtPoint(recognizer.location(in: self.view)) as? GridMessageItemNode, let messageId = itemNode.messageId, messageId != selectionPanState.currentMessageId { + self.controllerInteraction.toggleMessagesSelection([messageId], selectionPanState.selecting) + self.selectionPanState?.currentMessageId = messageId + } + case .ended, .failed, .cancelled: + self.selectionPanState = nil + case .possible: + break + } + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatHistoryListNode.swift b/submodules/TelegramUI/TelegramUI/ChatHistoryListNode.swift index 7aaa8cee48..0742b7d9c5 100644 --- a/submodules/TelegramUI/TelegramUI/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatHistoryListNode.swift @@ -13,29 +13,68 @@ import AccountContext import TemporaryCachedPeerDataManager import ChatListSearchItemNode import Emoji +import AppBundle -private let historyMessageCount: Int = 100 +private class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer { + private let selectionGestureActivationThreshold: CGFloat = 5.0 + + var recognized: Bool? = nil + var initialLocation: CGPoint = CGPoint() + + var shouldBegin: (() -> Bool)? + + override init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.minimumNumberOfTouches = 2 + self.maximumNumberOfTouches = 2 + } + + override func reset() { + super.reset() + + self.recognized = nil + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if let shouldBegin = self.shouldBegin, !shouldBegin() { + self.state = .failed + } else { + let touch = touches.first! + self.initialLocation = touch.location(in: self.view) + } + } + + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + let location = touches.first!.location(in: self.view) + let translation = location.offsetBy(dx: -self.initialLocation.x, dy: -self.initialLocation.y) + + if self.recognized == nil { + if (fabs(translation.y) >= selectionGestureActivationThreshold) { + self.recognized = true + } + } + + if let recognized = self.recognized, recognized { + super.touchesMoved(touches, with: event) + } + } +} + +private let historyMessageCount: Int = 90 + +public enum ChatHistoryListDisplayHeaders { + case none + case all + case allButLast +} public enum ChatHistoryListMode: Equatable { case bubbles - case list(search: Bool, reversed: Bool) - - public static func ==(lhs: ChatHistoryListMode, rhs: ChatHistoryListMode) -> Bool { - switch lhs { - case .bubbles: - if case .bubbles = rhs { - return true - } else { - return false - } - case let .list(search, reversed): - if case .list(search, reversed) = rhs { - return true - } else { - return false - } - } - } + case list(search: Bool, reversed: Bool, displayHeaders: ChatHistoryListDisplayHeaders) } enum ChatHistoryViewScrollPosition { @@ -72,6 +111,7 @@ struct ChatHistoryView { let originalView: MessageHistoryView let filteredEntries: [ChatHistoryEntry] let associatedData: ChatMessageItemAssociatedData + let lastHeaderId: Int64 let id: Int32 } @@ -179,7 +219,7 @@ private func maxMessageIndexForEntries(_ view: ChatHistoryView, indexRange: (Int return (incoming, overall) } -private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] { +private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { case let .MessageEntry(message, presentationData, read, _, selection, attributes): @@ -187,8 +227,18 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca switch mode { case .bubbles: item = ChatMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes)) - case let .list(search, _): - item = ListMessageItem(theme: presentationData.theme.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, context: context, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: message, selection: selection, displayHeader: search) + case let .list(_, _, displayHeaders): + let displayHeader: Bool + switch displayHeaders { + case .none: + displayHeader = false + case .all: + displayHeader = true + case .allButLast: + displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != lastHeaderId + } + + item = ListMessageItem(theme: presentationData.theme.theme, strings: presentationData.strings, fontSize: presentationData.fontSize, dateTimeFormat: presentationData.dateTimeFormat, context: context, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: message, selection: selection, displayHeader: displayHeader) } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .MessageGroupEntry(_, messages, presentationData): @@ -196,9 +246,9 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca switch mode { case .bubbles: item = ChatMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages)) - case let .list(search, _): + case let .list(_, _, _): assertionFailure() - item = ListMessageItem(theme: presentationData.theme.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, context: context, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: messages[0].0, selection: .none, displayHeader: search) + item = ListMessageItem(theme: presentationData.theme.theme, strings: presentationData.strings, fontSize: presentationData.fontSize, dateTimeFormat: presentationData.dateTimeFormat, context: context, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: messages[0].0, selection: .none, displayHeader: false) } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .UnreadEntry(_, presentationData): @@ -213,7 +263,7 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca } } -private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { +private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { case let .MessageEntry(message, presentationData, read, _, selection, attributes): @@ -221,8 +271,17 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca switch mode { case .bubbles: item = ChatMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes)) - case let .list(search, _): - item = ListMessageItem(theme: presentationData.theme.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, context: context, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: message, selection: selection, displayHeader: search) + case let .list(_, _, displayHeaders): + let displayHeader: Bool + switch displayHeaders { + case .none: + displayHeader = false + case .all: + displayHeader = true + case .allButLast: + displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != lastHeaderId + } + item = ListMessageItem(theme: presentationData.theme.theme, strings: presentationData.strings, fontSize: presentationData.fontSize, dateTimeFormat: presentationData.dateTimeFormat, context: context, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: message, selection: selection, displayHeader: displayHeader) } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .MessageGroupEntry(_, messages, presentationData): @@ -230,9 +289,9 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca switch mode { case .bubbles: item = ChatMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages)) - case let .list(search, _): + case let .list(_, _, _): assertionFailure() - item = ListMessageItem(theme: presentationData.theme.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, context: context, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: messages[0].0, selection: .none, displayHeader: search) + item = ListMessageItem(theme: presentationData.theme.theme, strings: presentationData.strings, fontSize: presentationData.fontSize, dateTimeFormat: presentationData.dateTimeFormat, context: context, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: messages[0].0, selection: .none, displayHeader: false) } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .UnreadEntry(_, presentationData): @@ -247,8 +306,8 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca } } -private func mappedChatHistoryViewListTransition(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition { - return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, animateIn: transition.animateIn, reason: transition.reason, flashIndicators: transition.flashIndicators) +private func mappedChatHistoryViewListTransition(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition { + return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, animateIn: transition.animateIn, reason: transition.reason, flashIndicators: transition.flashIndicators) } private final class ChatHistoryTransactionOpaqueState { @@ -259,7 +318,7 @@ private final class ChatHistoryTransactionOpaqueState { } } -private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: StickerPackItem], isScheduledMessages: Bool) -> ChatMessageItemAssociatedData { +private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], isScheduledMessages: Bool) -> ChatMessageItemAssociatedData { var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel var contactsPeerIds: Set = Set() if case let .peer(peerId) = chatLocation { @@ -391,13 +450,14 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private var nextHistoryLocationId: Int32 = 1 private func takeNextHistoryLocationId() -> Int32 { let id = self.nextHistoryLocationId - self.nextHistoryLocationId += 1 + self.nextHistoryLocationId += 5 return id } private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() private let messageProcessingManager = ChatMessageThrottledProcessingManager() + private let seenLiveLocationProcessingManager = ChatMessageThrottledProcessingManager() private let unsupportedMessageProcessingManager = ChatMessageThrottledProcessingManager() private let messageMentionProcessingManager = ChatMessageThrottledProcessingManager(delay: 0.2) let prefetchManager: InChatPrefetchManager @@ -455,7 +515,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.mode = mode let presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.currentPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations, largeEmoji: presentationData.largeEmoji, animatedEmojiScale: 1.0) + self.currentPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper), fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners, animatedEmojiScale: 1.0) self.chatPresentationDataPromise = Promise(self.currentPresentationData) @@ -471,6 +531,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.messageProcessingManager.process = { [weak context] messageIds in context?.account.viewTracker.updateViewCountForMessageIds(messageIds: messageIds) } + self.seenLiveLocationProcessingManager.process = { [weak context] messageIds in + context?.account.viewTracker.updateSeenLiveLocationForMessageIds(messageIds: messageIds) + } self.unsupportedMessageProcessingManager.process = { [weak context] messageIds in context?.account.viewTracker.updateUnsupportedMediaForMessageIds(messageIds: messageIds) } @@ -552,15 +615,31 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { |> distinctUntilChanged let animatedEmojiStickers = loadedStickerPack(postbox: context.account.postbox, network: context.account.network, reference: .animatedEmoji, forceActualized: false) - |> map { result -> [String: StickerPackItem] in + |> map { result -> [String: [StickerPackItem]] in switch result { case let .result(_, items, _): - var animatedEmojiStickers: [String: StickerPackItem] = [:] + var animatedEmojiStickers: [String: [StickerPackItem]] = [:] for case let item as StickerPackItem in items { if let emoji = item.getStringRepresentationsOfIndexKeys().first { - animatedEmojiStickers[emoji.basicEmoji.0] = item + animatedEmojiStickers[emoji.basicEmoji.0] = [item] + let strippedEmoji = emoji.basicEmoji.0.strippedEmoji + if animatedEmojiStickers[strippedEmoji] == nil { + animatedEmojiStickers[strippedEmoji] = [item] + } } } + + if let path = getAppBundle().path(forResource: "Dice_1", ofType: "tgs") { + var dices: [StickerPackItem] = [] + for i in 1...6 { + let path = path.replacingOccurrences(of: "_1", with: "_\(i)") + let id = arc4random64() + let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: id) + dices.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(i), id: Int64(i)), file: TelegramMediaFile(fileId: MediaId(namespace: 10, id: Int64(i)), partialReference: nil, resource: resource, previewRepresentations: [], immediateThumbnailData: nil, mimeType: "application/x-tgsticker", size: nil, attributes: []), indexKeys: [])) + } + animatedEmojiStickers["🎲".strippedEmoji] = dices + } + return animatedEmojiStickers default: return [:] @@ -571,14 +650,31 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let nextTransitionVersion = Atomic(value: 0) + let updatingMedia = context.account.pendingUpdateMessageManager.updatingMessageMedia + |> map { value -> [MessageId: ChatUpdatingMessageMedia] in + var result = value + for id in value.keys { + if case let .peer(peerId) = chatLocation { + if id.peerId != peerId { + result.removeValue(forKey: id) + } + } else { + result.removeValue(forKey: id) + } + } + return result + } + |> distinctUntilChanged + let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue, historyViewUpdate, self.chatPresentationDataPromise.get(), selectedMessages, + updatingMedia, automaticDownloadNetworkType, self.historyAppearsClearedPromise.get(), animatedEmojiStickers - ).start(next: { [weak self] update, chatPresentationData, selectedMessages, networkType, historyAppearsCleared, animatedEmojiStickers in + ).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, animatedEmojiStickers in func applyHole() { Queue.mainQueue().async { if let strongSelf = self { @@ -644,7 +740,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var reverse = false var includeSearchEntry = false - if case let .list(search, reverseValue) = mode { + if case let .list(search, reverseValue, _) = mode { includeSearchEntry = search reverse = reverseValue } @@ -656,7 +752,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, isScheduledMessages: isScheduledMessages) - let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(location: chatLocation, view: view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles && tagMask == nil, includeChatInfoEntry: mode == .bubbles, includeSearchEntry: includeSearchEntry && tagMask != nil, reverse: reverse, groupMessages: mode == .bubbles, selectedMessages: selectedMessages, presentationData: chatPresentationData, historyAppearsCleared: historyAppearsCleared, associatedData: associatedData), associatedData: associatedData, id: id) + let filteredEntries = chatHistoryEntriesForView(location: chatLocation, view: view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles && tagMask == nil, includeChatInfoEntry: mode == .bubbles, includeSearchEntry: includeSearchEntry && tagMask != nil, reverse: reverse, groupMessages: mode == .bubbles, selectedMessages: selectedMessages, presentationData: chatPresentationData, historyAppearsCleared: historyAppearsCleared, associatedData: associatedData, updatingMedia: updatingMedia) + let lastHeaderId = filteredEntries.last.flatMap { listMessageDateHeaderId(timestamp: $0.index.timestamp) } ?? 0 + let processedView = ChatHistoryView(originalView: view, filteredEntries: filteredEntries, associatedData: associatedData, lastHeaderId: lastHeaderId, id: id) let previousValueAndVersion = previousView.swap((processedView, update.1, selectedMessages)) let previous = previousValueAndVersion?.0 let previousSelectedMessages = previousValueAndVersion?.2 @@ -707,7 +805,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: flashIndicators, updatedMessageSelection: previousSelectedMessages != selectedMessages) - let mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, transition: rawTransition) + let mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, transition: rawTransition) Queue.mainQueue().async { guard let strongSelf = self else { return @@ -806,7 +904,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings || previousWallpaper != presentationData.chatWallpaper || previousDisableAnimations != presentationData.disableAnimations || previousAnimatedEmojiScale != animatedEmojiConfig.scale { let themeData = ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper) - let chatPresentationData = ChatPresentationData(theme: themeData, fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations, largeEmoji: presentationData.largeEmoji, animatedEmojiScale: animatedEmojiConfig.scale) + let chatPresentationData = ChatPresentationData(theme: themeData, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners, animatedEmojiScale: animatedEmojiConfig.scale) strongSelf.currentPresentationData = chatPresentationData strongSelf.dynamicBounceEnabled = !presentationData.disableAnimations @@ -868,6 +966,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self?.isInteractivelyScrollingValue = false self?.isInteractivelyScrollingPromise.set(false) } + + let selectionRecognizer = ChatHistoryListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:))) + self.view.addGestureRecognizer(selectionRecognizer) } deinit { @@ -897,6 +998,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let toLaterRange = (historyView.filteredEntries.count - 1 - (visible.firstIndex - 1), historyView.filteredEntries.count - 1) var messageIdsWithViewCount: [MessageId] = [] + var messageIdsWithLiveLocation: [MessageId] = [] var messageIdsWithUnsupportedMedia: [MessageId] = [] var messageIdsWithUnseenPersonalMention: [MessageId] = [] var messagesWithPreloadableMediaToEarlier: [(Message, Media)] = [] @@ -930,6 +1032,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { for media in message.media { if let _ = media as? TelegramMediaUnsupported { contentRequiredValidation = true + } else if message.flags.contains(.Incoming), let media = media as? TelegramMediaMap, let liveBroadcastingTimeout = media.liveBroadcastingTimeout { + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if message.timestamp + liveBroadcastingTimeout > timestamp { + messageIdsWithLiveLocation.append(message.id) + } } } if contentRequiredValidation { @@ -1034,6 +1141,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if !messageIdsWithViewCount.isEmpty { self.messageProcessingManager.add(messageIdsWithViewCount) } + if !messageIdsWithLiveLocation.isEmpty { + self.seenLiveLocationProcessingManager.add(messageIdsWithLiveLocation) + } if !messageIdsWithUnsupportedMedia.isEmpty { self.unsupportedMessageProcessingManager.add(messageIdsWithUnsupportedMedia) } @@ -1065,11 +1175,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if let loaded = displayedRange.loadedRange, let firstEntry = historyView.filteredEntries.first, let lastEntry = historyView.filteredEntries.last { if loaded.firstIndex < 5 && historyView.originalView.laterId != nil { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .message(lastEntry.index), anchorIndex: .message(lastEntry.index), count: historyMessageCount), id: (self.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .message(lastEntry.index), anchorIndex: .message(lastEntry.index), count: historyMessageCount), id: self.takeNextHistoryLocationId()) } else if loaded.firstIndex < 5, historyView.originalView.laterId == nil, !historyView.originalView.holeLater, let chatHistoryLocationValue = self.chatHistoryLocationValue, !chatHistoryLocationValue.isAtUpperBound, historyView.originalView.anchorIndex != .upperBound { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .upperBound, anchorIndex: .upperBound, count: historyMessageCount), id: (self.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .upperBound, anchorIndex: .upperBound, count: historyMessageCount), id: self.takeNextHistoryLocationId()) } else if loaded.lastIndex >= historyView.filteredEntries.count - 5 && historyView.originalView.earlierId != nil { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .message(firstEntry.index), anchorIndex: .message(firstEntry.index), count: historyMessageCount), id: (self.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .message(firstEntry.index), anchorIndex: .message(firstEntry.index), count: historyMessageCount), id: self.takeNextHistoryLocationId()) } } @@ -1108,14 +1218,18 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if let visibleRange = self.displayedItemRange.loadedRange { var index = historyView.filteredEntries.count - 1 loop: for entry in historyView.filteredEntries { - if index >= visibleRange.firstIndex && index <= visibleRange.lastIndex { - if case let .MessageEntry(message, _, _, _, _, _) = entry { + let isVisible = index >= visibleRange.firstIndex && index <= visibleRange.lastIndex + if case let .MessageEntry(message, _, _, _, _, _) = entry { + if !isVisible || currentMessage == nil { currentMessage = message - break loop - } else if case let .MessageGroupEntry(_, messages, _) = entry { - currentMessage = messages.first?.0 - break loop } + } else if case let .MessageGroupEntry(_, messages, _) = entry { + if !isVisible || currentMessage == nil { + currentMessage = messages.first?.0 + } + } + if isVisible { + break loop } index -= 1 } @@ -1169,7 +1283,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { public func isMessageVisibleOnScreen(_ id: MessageId) -> Bool { var result = false self.forEachItemNode({ itemNode in - if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, item.content.contains(where: { $0.id == id }) { + if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, item.content.contains(where: { $0.0.id == id }) { if self.itemNodeVisibleInsideInsets(itemNode) { result = true } @@ -1434,6 +1548,28 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } + func lastVisbleMesssage() -> Message? { + var currentMessage: Message? + if let historyView = self.historyView { + if let visibleRange = self.displayedItemRange.visibleRange { + var index = 0 + loop: for entry in historyView.filteredEntries.reversed() { + if index >= visibleRange.firstIndex && index <= visibleRange.lastIndex { + if case let .MessageEntry(message, _, _, _, _, _) = entry { + currentMessage = message + break loop + } else if case let .MessageGroupEntry(_, messages, _) = entry { + currentMessage = messages.first?.0 + break loop + } + } + index += 1 + } + } + } + return currentMessage + } + func immediateScrollState() -> ChatInterfaceHistoryScrollState? { var currentMessage: Message? if let historyView = self.historyView { @@ -1524,7 +1660,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var messageItem: ChatMessageItem? self.forEachItemNode({ itemNode in if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { - for message in item.content { + for (message, _) in item.content { if message.id == id { messageItem = item break @@ -1545,8 +1681,17 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { switch self.mode { case .bubbles: item = ChatMessageItem(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes)) - case let .list(search, _): - item = ListMessageItem(theme: presentationData.theme.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, context: self.context, chatLocation: self.chatLocation, controllerInteraction: self.controllerInteraction, message: message, selection: selection, displayHeader: search) + case let .list(_, _, displayHeaders): + let displayHeader: Bool + switch displayHeaders { + case .none: + displayHeader = false + case .all: + displayHeader = true + case .allButLast: + displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != historyView.lastHeaderId + } + item = ListMessageItem(theme: presentationData.theme.theme, strings: presentationData.strings, fontSize: presentationData.fontSize, dateTimeFormat: presentationData.dateTimeFormat, context: self.context, chatLocation: self.chatLocation, controllerInteraction: self.controllerInteraction, message: message, selection: selection, displayHeader: displayHeader) } let updateItem = ListViewUpdateItem(index: index, previousIndex: index, item: item, directionHint: nil) self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [updateItem], options: [.AnimateInsertion], scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) @@ -1559,4 +1704,137 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } } + + private func messagesAtPoint(_ point: CGPoint) -> [Message]? { + var resultMessages: [Message]? + self.forEachVisibleItemNode { itemNode in + if resultMessages == nil, let itemNode = itemNode as? ListViewItemNode, itemNode.frame.contains(point) { + if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item as? ChatMessageItem { + switch item.content { + case let .message(message, _, _ , _): + resultMessages = [message] + case let .group(messages): + resultMessages = messages.map { $0.0 } + } + } + } + } + return resultMessages + } + + private var selectionPanState: (selecting: Bool, initialMessageId: MessageId, toggledMessageIds: [[MessageId]])? + private var selectionScrollActivationTimer: SwiftSignalKit.Timer? + private var selectionScrollDisplayLink: ConstantDisplayLinkAnimator? + private var selectionScrollDelta: CGFloat? + private var selectionLastLocation: CGPoint? + + @objc private func selectionPanGesture(_ recognizer: UIGestureRecognizer) -> Void { + let location = recognizer.location(in: self.view) + switch recognizer.state { + case .began: + if let messages = self.messagesAtPoint(location), let message = messages.first { + let selecting = !(self.controllerInteraction.selectionState?.selectedIds.contains(message.id) ?? false) + self.selectionPanState = (selecting, message.id, []) + self.controllerInteraction.toggleMessagesSelection(messages.map { $0.id }, selecting) + } + case .changed: + self.handlePanSelection(location: location) + self.selectionLastLocation = location + case .ended, .failed, .cancelled: + self.selectionPanState = nil + self.selectionScrollDisplayLink = nil + self.selectionScrollActivationTimer?.invalidate() + self.selectionScrollActivationTimer = nil + self.selectionScrollDelta = nil + self.selectionLastLocation = nil + self.selectionScrollSkipUpdate = false + case .possible: + break + } + } + + private func handlePanSelection(location: CGPoint) { + if let state = self.selectionPanState { + if let messages = self.messagesAtPoint(location), let message = messages.first { + if message.id == state.initialMessageId { + if !state.toggledMessageIds.isEmpty { + self.controllerInteraction.toggleMessagesSelection(state.toggledMessageIds.flatMap { $0 }, !state.selecting) + self.selectionPanState = (state.selecting, state.initialMessageId, []) + } + } else if state.toggledMessageIds.last?.first != message.id { + var updatedToggledMessageIds: [[MessageId]] = [] + var previouslyToggled = false + for i in (0 ..< state.toggledMessageIds.count) { + if let messageId = state.toggledMessageIds[i].first { + if messageId == message.id { + previouslyToggled = true + updatedToggledMessageIds = Array(state.toggledMessageIds.prefix(i + 1)) + + let messageIdsToToggle = Array(state.toggledMessageIds.suffix(state.toggledMessageIds.count - i - 1)).flatMap { $0 } + self.controllerInteraction.toggleMessagesSelection(messageIdsToToggle, !state.selecting) + break + } + } + } + + if !previouslyToggled { + updatedToggledMessageIds = state.toggledMessageIds + let isSelected = (self.controllerInteraction.selectionState?.selectedIds.contains(message.id) ?? false) + if state.selecting != isSelected { + let messageIds = messages.map { $0.id } + updatedToggledMessageIds.append(messageIds) + self.controllerInteraction.toggleMessagesSelection(messageIds, state.selecting) + } + } + + self.selectionPanState = (state.selecting, state.initialMessageId, updatedToggledMessageIds) + } + } + + let scrollingAreaHeight: CGFloat = 50.0 + if location.y < scrollingAreaHeight + self.insets.top || location.y > self.frame.height - scrollingAreaHeight - self.insets.bottom { + if location.y < self.frame.height / 2.0 { + self.selectionScrollDelta = (scrollingAreaHeight - (location.y - self.insets.top)) / scrollingAreaHeight + } else { + self.selectionScrollDelta = -(scrollingAreaHeight - min(scrollingAreaHeight, max(0.0, (self.frame.height - self.insets.bottom - location.y)))) / scrollingAreaHeight + } + if let displayLink = self.selectionScrollDisplayLink { + displayLink.isPaused = false + } else { + if let _ = self.selectionScrollActivationTimer { + } else { + let timer = SwiftSignalKit.Timer(timeout: 0.45, repeat: false, completion: { [weak self] in + self?.setupSelectionScrolling() + }, queue: .mainQueue()) + timer.start() + self.selectionScrollActivationTimer = timer + } + } + } else { + self.selectionScrollDisplayLink?.isPaused = true + self.selectionScrollActivationTimer?.invalidate() + self.selectionScrollActivationTimer = nil + } + } + } + + private var selectionScrollSkipUpdate = false + private func setupSelectionScrolling() { + self.selectionScrollDisplayLink = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.selectionScrollActivationTimer = nil + if let strongSelf = self, let delta = strongSelf.selectionScrollDelta { + let distance: CGFloat = 15.0 * min(1.0, 0.15 + abs(delta * delta)) + let direction: ListViewScrollDirection = delta > 0.0 ? .up : .down + strongSelf.scrollWithDirection(direction, distance: distance) + + if let location = strongSelf.selectionLastLocation { + if !strongSelf.selectionScrollSkipUpdate { + strongSelf.handlePanSelection(location: location) + } + strongSelf.selectionScrollSkipUpdate = !strongSelf.selectionScrollSkipUpdate + } + } + }) + self.selectionScrollDisplayLink?.isPaused = false + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatHistorySearchContainerNode.swift b/submodules/TelegramUI/TelegramUI/ChatHistorySearchContainerNode.swift index 974e57853c..04761f6546 100644 --- a/submodules/TelegramUI/TelegramUI/ChatHistorySearchContainerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatHistorySearchContainerNode.swift @@ -10,6 +10,7 @@ import TelegramPresentationData import MergeLists import AccountContext import SearchUI +import TelegramUIPreferences private enum ChatHistorySearchEntryStableId: Hashable { case messageId(MessageId) @@ -35,19 +36,19 @@ private enum ChatHistorySearchEntryStableId: Hashable { private enum ChatHistorySearchEntry: Comparable, Identifiable { - case message(Message, PresentationTheme, PresentationStrings, PresentationDateTimeFormat) + case message(Message, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationFontSize) var stableId: ChatHistorySearchEntryStableId { switch self { - case let .message(message, _, _, _): + case let .message(message, _, _, _, _): return .messageId(message.id) } } static func ==(lhs: ChatHistorySearchEntry, rhs: ChatHistorySearchEntry) -> Bool { switch lhs { - case let .message(lhsMessage, lhsTheme, lhsStrings, lhsDateTimeFormat): - if case let .message(rhsMessage, rhsTheme, rhsStrings, rhsDateTimeFormat) = rhs { + case let .message(lhsMessage, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsFontSize): + if case let .message(rhsMessage, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsFontSize) = rhs { if lhsMessage.id != rhsMessage.id { return false } @@ -63,6 +64,9 @@ private enum ChatHistorySearchEntry: Comparable, Identifiable { if lhsDateTimeFormat != rhsDateTimeFormat { return false } + if lhsFontSize != rhsFontSize { + return false + } return true } else { return false @@ -72,8 +76,8 @@ private enum ChatHistorySearchEntry: Comparable, Identifiable { static func <(lhs: ChatHistorySearchEntry, rhs: ChatHistorySearchEntry) -> Bool { switch lhs { - case let .message(lhsMessage, _, _, _): - if case let .message(rhsMessage, _, _, _) = rhs { + case let .message(lhsMessage, _, _, _, _): + if case let .message(rhsMessage, _, _, _, _) = rhs { return lhsMessage.index < rhsMessage.index } else { return false @@ -83,8 +87,8 @@ private enum ChatHistorySearchEntry: Comparable, Identifiable { func item(context: AccountContext, peerId: PeerId, interaction: ChatControllerInteraction) -> ListViewItem { switch self { - case let .message(message, theme, strings, dateTimeFormat): - return ListMessageItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, context: context, chatLocation: .peer(peerId), controllerInteraction: interaction, message: message, selection: .none, displayHeader: true) + case let .message(message, theme, strings, dateTimeFormat, fontSize): + return ListMessageItem(theme: theme, strings: strings, fontSize: fontSize, dateTimeFormat: dateTimeFormat, context: context, chatLocation: .peer(peerId), controllerInteraction: interaction, message: message, selection: .none, displayHeader: true) } } } @@ -134,7 +138,7 @@ final class ChatHistorySearchContainerNode: SearchDisplayControllerContentNode { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings, PresentationDateTimeFormat)> + private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationFontSize)> private var enqueuedTransitions: [(ChatHistorySearchContainerTransition, Bool)] = [] @@ -143,7 +147,7 @@ final class ChatHistorySearchContainerNode: SearchDisplayControllerContentNode { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings, self.presentationData.dateTimeFormat)) + self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings, self.presentationData.dateTimeFormat, self.presentationData.listsFontSize)) self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) @@ -191,7 +195,7 @@ final class ChatHistorySearchContainerNode: SearchDisplayControllerContentNode { return ([], [:]) } else { return (messages.map { message -> ChatHistorySearchEntry in - return .message(message, themeAndStrings.0, themeAndStrings.1, themeAndStrings.2) + return .message(message, themeAndStrings.0, themeAndStrings.1, themeAndStrings.2, themeAndStrings.3) }, Dictionary(messages.map { ($0.id, $0) }, uniquingKeysWith: { lhs, _ in lhs })) } } @@ -224,7 +228,7 @@ final class ChatHistorySearchContainerNode: SearchDisplayControllerContentNode { self.presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in if let strongSelf = self { - strongSelf.themeAndStringsPromise.set(.single((presentationData.theme, presentationData.strings, presentationData.dateTimeFormat))) + strongSelf.themeAndStringsPromise.set(.single((presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.listsFontSize))) strongSelf.emptyResultsTitleNode.attributedText = NSAttributedString(string: presentationData.strings.SharedMedia_SearchNoResults, font: Font.semibold(17.0), textColor: presentationData.theme.list.freeTextColor, paragraphAlignment: .center) @@ -342,10 +346,10 @@ final class ChatHistorySearchContainerNode: SearchDisplayControllerContentNode { if let currentEntries = self.currentEntries { for entry in currentEntries { switch entry { - case let .message(message, _, _, _): - if message.id == id { - return message - } + case let .message(message, _, _, _, _): + if message.id == id { + return message + } } } } @@ -364,8 +368,8 @@ final class ChatHistorySearchContainerNode: SearchDisplayControllerContentNode { } } - func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { - var transitionNode: (ASDisplayNode, () -> (UIView?, UIView?))? + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? self.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { if let result = itemNode.transitionNode(id: messageId, media: media) { diff --git a/submodules/TelegramUI/TelegramUI/ChatHistoryViewForLocation.swift b/submodules/TelegramUI/TelegramUI/ChatHistoryViewForLocation.swift index e2e0b3f3e5..e74f25cafe 100644 --- a/submodules/TelegramUI/TelegramUI/ChatHistoryViewForLocation.swift +++ b/submodules/TelegramUI/TelegramUI/ChatHistoryViewForLocation.swift @@ -206,7 +206,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, account: A let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up let chatScrollPosition = ChatHistoryViewScrollPosition.index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated) var first = true - return account.viewTracker.aroundMessageHistoryViewForLocation(chatLocation, index: index, anchorIndex: anchorIndex, count: 100, fixedCombinedReadStates: fixedCombinedReadStates, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) + return account.viewTracker.aroundMessageHistoryViewForLocation(chatLocation, index: index, anchorIndex: anchorIndex, count: 128, fixedCombinedReadStates: fixedCombinedReadStates, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in let (cachedData, cachedDataMessages, readStateData) = extractAdditionalData(view: view, chatLocation: chatLocation) diff --git a/submodules/TelegramUI/TelegramUI/ChatHoleItem.swift b/submodules/TelegramUI/TelegramUI/ChatHoleItem.swift index 59c80a38c7..b88cf37698 100644 --- a/submodules/TelegramUI/TelegramUI/ChatHoleItem.swift +++ b/submodules/TelegramUI/TelegramUI/ChatHoleItem.swift @@ -81,7 +81,7 @@ class ChatHoleItemNode: ListViewItemNode { return { item, params, dateAtBottom in var updatedBackground: UIImage? if item.presentationData.theme !== currentItem?.presentationData.theme { - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) updatedBackground = graphics.chatServiceBubbleFillImage } diff --git a/submodules/TelegramUI/TelegramUI/ChatInputContextPanelNode.swift b/submodules/TelegramUI/TelegramUI/ChatInputContextPanelNode.swift index db70caeb37..60c68d88c1 100644 --- a/submodules/TelegramUI/TelegramUI/ChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatInputContextPanelNode.swift @@ -5,6 +5,7 @@ import Display import TelegramCore import SyncCore import TelegramPresentationData +import TelegramUIPreferences import AccountContext enum ChatInputContextPanelPlacement { @@ -17,10 +18,12 @@ class ChatInputContextPanelNode: ASDisplayNode { var interfaceInteraction: ChatPanelInterfaceInteraction? var placement: ChatInputContextPanelPlacement = .overPanels var theme: PresentationTheme + var fontSize: PresentationFontSize - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { self.context = context self.theme = theme + self.fontSize = fontSize super.init() } diff --git a/submodules/TelegramUI/TelegramUI/ChatInputPanelNode.swift b/submodules/TelegramUI/TelegramUI/ChatInputPanelNode.swift index d3ea2d179f..6228bafa19 100644 --- a/submodules/TelegramUI/TelegramUI/ChatInputPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatInputPanelNode.swift @@ -11,7 +11,7 @@ class ChatInputPanelNode: ASDisplayNode { var context: AccountContext? var interfaceInteraction: ChatPanelInterfaceInteraction? - func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { return 0.0 } diff --git a/submodules/TelegramUI/TelegramUI/ChatInterfaceInputContextPanels.swift b/submodules/TelegramUI/TelegramUI/ChatInterfaceInputContextPanels.swift index 56b2bd8bca..470d350837 100644 --- a/submodules/TelegramUI/TelegramUI/ChatInterfaceInputContextPanels.swift +++ b/submodules/TelegramUI/TelegramUI/ChatInterfaceInputContextPanels.swift @@ -59,7 +59,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa if let currentPanel = currentPanel as? DisabledContextResultsChatInputContextPanelNode { return currentPanel } else { - let panel = DisabledContextResultsChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + let panel = DisabledContextResultsChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize) panel.interfaceInteraction = interfaceInteraction return panel } @@ -75,7 +75,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa currentPanel.updateResults(results.map({ $0.file })) return currentPanel } else { - let panel = HorizontalStickersChatContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + let panel = HorizontalStickersChatContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize) panel.controllerInteraction = controllerInteraction panel.interfaceInteraction = interfaceInteraction panel.updateResults(results.map({ $0.file })) @@ -88,7 +88,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa currentPanel.updateResults(results) return currentPanel } else { - let panel = HashtagChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + let panel = HashtagChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize) panel.interfaceInteraction = interfaceInteraction panel.updateResults(results) return panel @@ -100,7 +100,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa currentPanel.updateResults(results) return currentPanel } else { - let panel = EmojisChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + let panel = EmojisChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize) panel.interfaceInteraction = interfaceInteraction panel.updateResults(results) return panel @@ -112,7 +112,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa currentPanel.updateResults(peers) return currentPanel } else { - let panel = MentionChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, mode: .input) + let panel = MentionChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize, mode: .input) panel.interfaceInteraction = interfaceInteraction panel.updateResults(peers) return panel @@ -126,7 +126,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa currentPanel.updateResults(commands) return currentPanel } else { - let panel = CommandChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + let panel = CommandChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize) panel.interfaceInteraction = interfaceInteraction panel.updateResults(commands) return panel @@ -142,7 +142,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa currentPanel.updateResults(results) return currentPanel } else { - let panel = VerticalListContextResultsChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + let panel = VerticalListContextResultsChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize) panel.interfaceInteraction = interfaceInteraction panel.updateResults(results) return panel @@ -152,7 +152,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa currentPanel.updateResults(results) return currentPanel } else { - let panel = HorizontalListContextResultsChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + let panel = HorizontalListContextResultsChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize) panel.interfaceInteraction = interfaceInteraction panel.updateResults(results) return panel @@ -178,7 +178,7 @@ func chatOverlayContextPanelForChatPresentationIntefaceState(_ chatPresentationI currentPanel.updateResults(peers) return currentPanel } else { - let panel = MentionChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, mode: .search) + let panel = MentionChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize, mode: .search) panel.interfaceInteraction = interfaceInteraction panel.updateResults(peers) return panel diff --git a/submodules/TelegramUI/TelegramUI/ChatInterfaceInputNodes.swift b/submodules/TelegramUI/TelegramUI/ChatInterfaceInputNodes.swift index 5aefaec90a..0440046386 100644 --- a/submodules/TelegramUI/TelegramUI/ChatInterfaceInputNodes.swift +++ b/submodules/TelegramUI/TelegramUI/ChatInterfaceInputNodes.swift @@ -21,7 +21,7 @@ func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: if case let .peer(id) = chatPresentationInterfaceState.chatLocation { peerId = id } - let inputNode = ChatMediaInputNode(context: context, peerId: peerId, controllerInteraction: controllerInteraction, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, gifPaneIsActiveUpdated: { [weak interfaceInteraction] value in + let inputNode = ChatMediaInputNode(context: context, peerId: peerId, controllerInteraction: controllerInteraction, chatWallpaper: chatPresentationInterfaceState.chatWallpaper, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize, gifPaneIsActiveUpdated: { [weak interfaceInteraction] value in if let interfaceInteraction = interfaceInteraction { interfaceInteraction.updateInputModeAndDismissedButtonKeyboardMessageId { state in if case let .media(_, expanded) = state.inputMode { diff --git a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextMenus.swift index a0e72e0a7a..166e5e84db 100644 --- a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -253,8 +253,6 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: return .single([]) } - let dataSignal: Signal - var loadStickerSaveStatus: MediaId? var loadCopyMediaResource: MediaResource? var isAction = false @@ -346,20 +344,27 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: return transaction.getPreferencesEntry(key: PreferencesKeys.limitsConfiguration) as? LimitsConfiguration ?? LimitsConfiguration.defaultValue } - dataSignal = combineLatest(loadLimits, loadStickerSaveStatusSignal, loadResourceStatusSignal, context.sharedContext.chatAvailableMessageActions(postbox: context.account.postbox, accountPeerId: context.account.peerId, messageIds: Set(messages.map { $0.id }))) - |> map { limitsConfiguration, stickerSaveStatus, resourceStatus, messageActions -> MessageContextMenuData in + let dataSignal: Signal<(MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia]), NoError> = combineLatest( + loadLimits, + loadStickerSaveStatusSignal, + loadResourceStatusSignal, + context.sharedContext.chatAvailableMessageActions(postbox: context.account.postbox, accountPeerId: context.account.peerId, messageIds: Set(messages.map { $0.id })), + context.account.pendingUpdateMessageManager.updatingMessageMedia + |> take(1) + ) + |> map { limitsConfiguration, stickerSaveStatus, resourceStatus, messageActions, updatingMessageMedia -> (MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia]) in var canEdit = false if !isAction { let message = messages[0] canEdit = canEditMessage(context: context, limitsConfiguration: limitsConfiguration, message: message) } - return MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions) + return (MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions), updatingMessageMedia) } return dataSignal |> deliverOnMainQueue - |> map { data -> [ContextMenuItem] in + |> map { data, updatingMessageMedia -> [ContextMenuItem] in var actions: [ContextMenuItem] = [] if let starStatus = data.starStatus { @@ -418,6 +423,16 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in + let copyTextWithEntities = { + var messageEntities: [MessageTextEntity]? + for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + messageEntities = attribute.entities + break + } + } + storeMessageTextInPasteboard(message.text, entities: messageEntities) + } if resourceAvailable { for media in message.media { if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { @@ -427,32 +442,21 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { if let image = UIImage(data: imageData) { if !message.text.isEmpty { - UIPasteboard.general.string = message.text - /*UIPasteboard.general.items = [ - [kUTTypeUTF8PlainText as String: message.text], - [kUTTypePNG as String: image] - ]*/ + copyTextWithEntities() } else { UIPasteboard.general.image = image } } else { - UIPasteboard.general.string = message.text + copyTextWithEntities() } } else { - UIPasteboard.general.string = message.text + copyTextWithEntities() } }) } } } else { - var messageEntities: [MessageTextEntity]? - for attribute in message.attributes { - if let attribute = attribute as? TextEntitiesMessageAttribute { - messageEntities = attribute.entities - break - } - } - storeMessageTextInPasteboard(message.text, entities: messageEntities) + copyTextWithEntities() } f(.default) }))) @@ -507,7 +511,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: hasSelected = true } } - if hasSelected { + if hasSelected, case .poll = activePoll.kind { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_UnvotePoll, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unvote"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in @@ -535,7 +539,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } } - if let _ = activePoll, messages[0].forwardInfo == nil { + if let activePoll = activePoll, messages[0].forwardInfo == nil { var canStopPoll = false if !messages[0].flags.contains(.Incoming) { canStopPoll = true @@ -561,7 +565,14 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } if canStopPoll { - actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_StopPoll, icon: { theme in + let stopPollAction: String + switch activePoll.kind { + case .poll: + stopPollAction = chatPresentationInterfaceState.strings.Conversation_StopPoll + case .quiz: + stopPollAction = chatPresentationInterfaceState.strings.Conversation_StopQuiz + } + actions.append(.action(ContextMenuActionItem(text: stopPollAction, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/StopPoll"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in interfaceInteraction.requestStopPollInMessage(messages[0].id) @@ -689,11 +700,28 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: clearCacheAsDelete = true } if (!data.messageActions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty || clearCacheAsDelete) && !isAction { - let title = message.flags.isSending ? chatPresentationInterfaceState.strings.Conversation_ContextMenuCancelSending : chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete + let title: String + var isSending = false + var isEditing = false + if updatingMessageMedia[message.id] != nil { + isSending = true + isEditing = true + title = chatPresentationInterfaceState.strings.Conversation_ContextMenuCancelEditing + } else if message.flags.isSending { + isSending = true + title = chatPresentationInterfaceState.strings.Conversation_ContextMenuCancelSending + } else { + title = chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete + } actions.append(.action(ContextMenuActionItem(text: title, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: message.flags.isSending ? "Chat/Context Menu/Clear" : "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) + return generateTintedImage(image: UIImage(bundleImageName: isSending ? "Chat/Context Menu/Clear" : "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) }, action: { controller, f in - interfaceInteraction.deleteMessages(selectAll ? messages : [message], controller, f) + if isEditing { + context.account.pendingUpdateMessageManager.cancel(messageId: message.id) + f(.default) + } else { + interfaceInteraction.deleteMessages(selectAll ? messages : [message], controller, f) + } }))) } diff --git a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextQueries.swift index 57b4e5930a..125cf85916 100644 --- a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextQueries.swift @@ -288,7 +288,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee return signal |> then(contextBot) case let .emojiSearch(query, languageCode, range): - var signal = searchEmojiKeywords(postbox: context.account.postbox, inputLanguageCode: languageCode, query: query, completeMatch: query.count < 3) + var signal = searchEmojiKeywords(postbox: context.account.postbox, inputLanguageCode: languageCode, query: query, completeMatch: query.count < 2) if !languageCode.lowercased().hasPrefix("en") { signal = signal |> mapToSignal { keywords in diff --git a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateInputPanels.swift index 9f7c2a2f19..7518a6f482 100644 --- a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateInputPanels.swift @@ -5,56 +5,66 @@ import TelegramCore import SyncCore import AccountContext -func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputPanelNode? { - if let renderedPeer = chatPresentationInterfaceState.renderedPeer, renderedPeer.peer?.restrictionText(platform: "ios") != nil { - return nil +func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputPanelNode?, currentSecondaryPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> (primary: ChatInputPanelNode?, secondary: ChatInputPanelNode?) { + if let renderedPeer = chatPresentationInterfaceState.renderedPeer, renderedPeer.peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) != nil { + return (nil, nil) } if chatPresentationInterfaceState.isNotAccessible { - return nil + return (nil, nil) } if let _ = chatPresentationInterfaceState.search { - var hasSelection = false - if let selectionState = chatPresentationInterfaceState.interfaceState.selectionState, !selectionState.selectedIds.isEmpty { - hasSelection = true - } - if !hasSelection { - if let currentPanel = currentPanel as? ChatSearchInputPanelNode { + var selectionPanel: ChatMessageSelectionInputPanelNode? + if let selectionState = chatPresentationInterfaceState.interfaceState.selectionState { + if let currentPanel = (currentPanel as? ChatMessageSelectionInputPanelNode) ?? (currentSecondaryPanel as? ChatMessageSelectionInputPanelNode) { + currentPanel.selectedMessages = selectionState.selectedIds currentPanel.interfaceInteraction = interfaceInteraction - return currentPanel + currentPanel.updateTheme(theme: chatPresentationInterfaceState.theme) + selectionPanel = currentPanel } else { - let panel = ChatSearchInputPanelNode(theme: chatPresentationInterfaceState.theme) + let panel = ChatMessageSelectionInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.context = context + panel.selectedMessages = selectionState.selectedIds panel.interfaceInteraction = interfaceInteraction - return panel + selectionPanel = panel } } + + if let currentPanel = (currentPanel as? ChatSearchInputPanelNode) ?? (currentSecondaryPanel as? ChatSearchInputPanelNode) { + currentPanel.interfaceInteraction = interfaceInteraction + return (currentPanel, selectionPanel) + } else { + let panel = ChatSearchInputPanelNode(theme: chatPresentationInterfaceState.theme) + panel.context = context + panel.interfaceInteraction = interfaceInteraction + return (panel, selectionPanel) + } } if let selectionState = chatPresentationInterfaceState.interfaceState.selectionState { - if let currentPanel = currentPanel as? ChatMessageSelectionInputPanelNode { + if let currentPanel = (currentPanel as? ChatMessageSelectionInputPanelNode) ?? (currentSecondaryPanel as? ChatMessageSelectionInputPanelNode) { currentPanel.selectedMessages = selectionState.selectedIds currentPanel.interfaceInteraction = interfaceInteraction currentPanel.updateTheme(theme: chatPresentationInterfaceState.theme) - return currentPanel + return (currentPanel, nil) } else { let panel = ChatMessageSelectionInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.context = context panel.selectedMessages = selectionState.selectedIds panel.interfaceInteraction = interfaceInteraction - return panel + return (panel, nil) } } if chatPresentationInterfaceState.peerIsBlocked { - if let currentPanel = currentPanel as? ChatUnblockInputPanelNode { + if let currentPanel = (currentPanel as? ChatUnblockInputPanelNode) ?? (currentSecondaryPanel as? ChatUnblockInputPanelNode) { currentPanel.interfaceInteraction = interfaceInteraction currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) - return currentPanel + return (currentPanel, nil) } else { let panel = ChatUnblockInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.context = context panel.interfaceInteraction = interfaceInteraction - return panel + return (panel, nil) } } @@ -64,22 +74,22 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if let secretChat = peer as? TelegramSecretChat { switch secretChat.embeddedState { case .handshake: - if let currentPanel = currentPanel as? SecretChatHandshakeStatusInputPanelNode { - return currentPanel + if let currentPanel = (currentPanel as? SecretChatHandshakeStatusInputPanelNode) ?? (currentSecondaryPanel as? SecretChatHandshakeStatusInputPanelNode) { + return (currentPanel, nil) } else { let panel = SecretChatHandshakeStatusInputPanelNode() panel.context = context panel.interfaceInteraction = interfaceInteraction - return panel + return (panel, nil) } case .terminated: - if let currentPanel = currentPanel as? DeleteChatInputPanelNode { - return currentPanel + if let currentPanel = (currentPanel as? DeleteChatInputPanelNode) ?? (currentSecondaryPanel as? DeleteChatInputPanelNode) { + return (currentPanel, nil) } else { let panel = DeleteChatInputPanelNode() panel.context = context panel.interfaceInteraction = interfaceInteraction - return panel + return (panel, nil) } case .active: break @@ -88,13 +98,13 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState var isMember: Bool = false switch channel.participationStatus { case .kicked: - if let currentPanel = currentPanel as? DeleteChatInputPanelNode { - return currentPanel + if let currentPanel = (currentPanel as? DeleteChatInputPanelNode) ?? (currentSecondaryPanel as? DeleteChatInputPanelNode) { + return (currentPanel, nil) } else { let panel = DeleteChatInputPanelNode() panel.context = context panel.interfaceInteraction = interfaceInteraction - return panel + return (panel, nil) } case .member: isMember = true @@ -103,13 +113,13 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } if isMember && channel.hasBannedPermission(.banSendMessages) != nil { - if let currentPanel = currentPanel as? ChatRestrictedInputPanelNode { - return currentPanel + if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { + return (currentPanel, nil) } else { let panel = ChatRestrictedInputPanelNode() panel.context = context panel.interfaceInteraction = interfaceInteraction - return panel + return (panel, nil) } } @@ -118,25 +128,25 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if chatPresentationInterfaceState.interfaceState.editMessage != nil, channel.hasPermission(.editAllMessages) { displayInputTextPanel = true } else if !channel.hasPermission(.sendMessages) { - if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode { - return currentPanel + if let currentPanel = (currentPanel as? ChatChannelSubscriberInputPanelNode) ?? (currentSecondaryPanel as? ChatChannelSubscriberInputPanelNode) { + return (currentPanel, nil) } else { let panel = ChatChannelSubscriberInputPanelNode() panel.interfaceInteraction = interfaceInteraction panel.context = context - return panel + return (panel, nil) } } case .group: switch channel.participationStatus { case .kicked, .left: - if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode { - return currentPanel + if let currentPanel = (currentPanel as? ChatChannelSubscriberInputPanelNode) ?? (currentSecondaryPanel as? ChatChannelSubscriberInputPanelNode) { + return (currentPanel, nil) } else { let panel = ChatChannelSubscriberInputPanelNode() panel.interfaceInteraction = interfaceInteraction panel.context = context - return panel + return (panel, nil) } case .member: break @@ -145,26 +155,26 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } else if let group = peer as? TelegramGroup { switch group.membership { case .Removed, .Left: - if let currentPanel = currentPanel as? DeleteChatInputPanelNode { - return currentPanel + if let currentPanel = (currentPanel as? DeleteChatInputPanelNode) ?? (currentSecondaryPanel as? DeleteChatInputPanelNode) { + return (currentPanel, nil) } else { let panel = DeleteChatInputPanelNode() panel.context = context panel.interfaceInteraction = interfaceInteraction - return panel + return (panel, nil) } case .Member: break } if group.hasBannedPermission(.banSendMessages) { - if let currentPanel = currentPanel as? ChatRestrictedInputPanelNode { - return currentPanel + if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) { + return (currentPanel, nil) } else { let panel = ChatRestrictedInputPanelNode() panel.context = context panel.interfaceInteraction = interfaceInteraction - return panel + return (panel, nil) } } } @@ -183,25 +193,24 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } if displayBotStartPanel { - if let currentPanel = currentPanel as? ChatBotStartInputPanelNode { + if let currentPanel = (currentPanel as? ChatBotStartInputPanelNode) ?? (currentSecondaryPanel as? ChatBotStartInputPanelNode) { currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) - return currentPanel + return (currentPanel, nil) } else { let panel = ChatBotStartInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.context = context panel.interfaceInteraction = interfaceInteraction - return panel + return (panel, nil) } } else { if let _ = chatPresentationInterfaceState.recordedMediaPreview { - if let currentPanel = currentPanel as? ChatRecordingPreviewInputPanelNode { - //currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) - return currentPanel + if let currentPanel = (currentPanel as? ChatRecordingPreviewInputPanelNode) ?? (currentSecondaryPanel as? ChatRecordingPreviewInputPanelNode) { + return (currentPanel, nil) } else { let panel = ChatRecordingPreviewInputPanelNode(theme: chatPresentationInterfaceState.theme) panel.context = context panel.interfaceInteraction = interfaceInteraction - return panel + return (panel, nil) } } @@ -214,14 +223,14 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } if displayInputTextPanel { - if let currentPanel = currentPanel as? ChatTextInputPanelNode { + if let currentPanel = (currentPanel as? ChatTextInputPanelNode) ?? (currentSecondaryPanel as? ChatTextInputPanelNode) { currentPanel.interfaceInteraction = interfaceInteraction - return currentPanel + return (currentPanel, nil) } else { if let textInputPanelNode = textInputPanelNode { textInputPanelNode.interfaceInteraction = interfaceInteraction textInputPanelNode.context = context - return textInputPanelNode + return (textInputPanelNode, nil) } else { let panel = ChatTextInputPanelNode(presentationInterfaceState: chatPresentationInterfaceState, presentController: { [weak interfaceInteraction] controller in interfaceInteraction?.presentController(controller, nil) @@ -229,10 +238,10 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState panel.interfaceInteraction = interfaceInteraction panel.context = context - return panel + return (panel, nil) } } } else { - return nil + return (nil, nil) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift index bf3eafbf57..597862487c 100644 --- a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift @@ -6,13 +6,14 @@ import SyncCore import TelegramPresentationData import AccountContext -enum ChatNavigationButtonAction { - case openChatInfo +enum ChatNavigationButtonAction: Equatable { + case openChatInfo(expandAvatar: Bool) case clearHistory case clearCache case cancelMessageSelection case search case dismiss + case toggleInfoPanel } struct ChatNavigationButton: Equatable { @@ -71,11 +72,16 @@ func rightNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Ch } } + if presentationInterfaceState.isScheduledMessages { + return chatInfoNavigationButton + } + if case .standard(true) = presentationInterfaceState.mode { + return chatInfoNavigationButton } else if let peer = presentationInterfaceState.renderedPeer?.peer { if presentationInterfaceState.accountPeerId == peer.id { if presentationInterfaceState.isScheduledMessages { - return nil + return chatInfoNavigationButton } else { let buttonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector) buttonItem.accessibilityLabel = strings.Conversation_Search diff --git a/submodules/TelegramUI/TelegramUI/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/TelegramUI/ChatInterfaceTitlePanelNodes.swift index 27a5c40448..e9d772fa62 100644 --- a/submodules/TelegramUI/TelegramUI/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/TelegramUI/ChatInterfaceTitlePanelNodes.swift @@ -8,7 +8,7 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat if case .overlay = chatPresentationInterfaceState.mode { return nil } - if chatPresentationInterfaceState.renderedPeer?.peer?.restrictionText(platform: "ios") != nil { + if chatPresentationInterfaceState.renderedPeer?.peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) != nil { return nil } if chatPresentationInterfaceState.search != nil { diff --git a/submodules/TelegramUI/TelegramUI/ChatLoadingNode.swift b/submodules/TelegramUI/TelegramUI/ChatLoadingNode.swift index e2280941f0..6cb7638a0b 100644 --- a/submodules/TelegramUI/TelegramUI/ChatLoadingNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatLoadingNode.swift @@ -12,13 +12,13 @@ final class ChatLoadingNode: ASDisplayNode { private let activityIndicator: ActivityIndicator private let offset: CGPoint - init(theme: PresentationTheme, chatWallpaper: TelegramWallpaper) { + init(theme: PresentationTheme, chatWallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners) { self.backgroundNode = ASImageNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.displaysAsynchronously = false - let graphics = PresentationResourcesChat.additionalGraphics(theme, wallpaper: chatWallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(theme, wallpaper: chatWallpaper, bubbleCorners: bubbleCorners) self.backgroundNode.image = graphics.chatLoadingIndicatorBackgroundImage let serviceColor = serviceMessageColorComponents(theme: theme, wallpaper: chatWallpaper) diff --git a/submodules/TelegramUI/TelegramUI/ChatMediaInputGridEntries.swift b/submodules/TelegramUI/TelegramUI/ChatMediaInputGridEntries.swift index 9ca8ec1163..005f2649f5 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMediaInputGridEntries.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMediaInputGridEntries.swift @@ -11,60 +11,75 @@ enum ChatMediaInputGridEntryStableId: Equatable, Hashable { case search case peerSpecificSetup case sticker(ItemCollectionId, ItemCollectionItemIndex.Id) + case trending(ItemCollectionId) } enum ChatMediaInputGridEntryIndex: Equatable, Comparable { case search case peerSpecificSetup(dismissed: Bool) case collectionIndex(ItemCollectionViewEntryIndex) + case trending(ItemCollectionId, Int) var stableId: ChatMediaInputGridEntryStableId { switch self { - case .search: - return .search - case .peerSpecificSetup: - return .peerSpecificSetup - case let .collectionIndex(index): - return .sticker(index.collectionId, index.itemIndex.id) + case .search: + return .search + case .peerSpecificSetup: + return .peerSpecificSetup + case let .collectionIndex(index): + return .sticker(index.collectionId, index.itemIndex.id) + case let .trending(id, index): + return .trending(id) } } static func <(lhs: ChatMediaInputGridEntryIndex, rhs: ChatMediaInputGridEntryIndex) -> Bool { switch lhs { - case .search: - if case .search = rhs { + case .search: + if case .search = rhs { + return false + } else { + return true + } + case let .peerSpecificSetup(lhsDismissed): + switch rhs { + case .search, .peerSpecificSetup: + return false + case let .collectionIndex(index): + if lhsDismissed { return false } else { + if index.collectionId.id == 0 { + return false + } else { + return true + } + } + case .trending: + return true + } + case let .collectionIndex(lhsIndex): + switch rhs { + case .search: + return false + case let .peerSpecificSetup(dismissed): + if dismissed { return true + } else { + return false } - case let .peerSpecificSetup(lhsDismissed): - switch rhs { - case .search, .peerSpecificSetup: - return false - case let .collectionIndex(index): - if lhsDismissed { - return false - } else { - if index.collectionId.id == 0 { - return false - } else { - return true - } - } - } - case let .collectionIndex(lhsIndex): - switch rhs { - case .search: - return false - case let .peerSpecificSetup(dismissed): - if dismissed { - return true - } else { - return false - } - case let .collectionIndex(rhsIndex): - return lhsIndex < rhsIndex - } + case let .collectionIndex(rhsIndex): + return lhsIndex < rhsIndex + case .trending: + return true + } + case let .trending(_, lhsIndex): + switch rhs { + case .search, .peerSpecificSetup, .collectionIndex: + return false + case let .trending(_, rhsIndex): + return lhsIndex < rhsIndex + } } } } @@ -73,15 +88,18 @@ enum ChatMediaInputGridEntry: Equatable, Comparable, Identifiable { case search(theme: PresentationTheme, strings: PresentationStrings) case peerSpecificSetup(theme: PresentationTheme, strings: PresentationStrings, dismissed: Bool) case sticker(index: ItemCollectionViewEntryIndex, stickerItem: StickerPackItem, stickerPackInfo: StickerPackCollectionInfo?, canManagePeerSpecificPack: Bool?, theme: PresentationTheme) + case trending(TrendingPanePackEntry) var index: ChatMediaInputGridEntryIndex { switch self { - case .search: - return .search - case let .peerSpecificSetup(_, _, dismissed): - return .peerSpecificSetup(dismissed: dismissed) - case let .sticker(index, _, _, _, _): - return .collectionIndex(index) + case .search: + return .search + case let .peerSpecificSetup(_, _, dismissed): + return .peerSpecificSetup(dismissed: dismissed) + case let .sticker(index, _, _, _, _): + return .collectionIndex(index) + case let .trending(entry): + return .trending(entry.info.id, entry.index) } } @@ -91,45 +109,51 @@ enum ChatMediaInputGridEntry: Equatable, Comparable, Identifiable { static func ==(lhs: ChatMediaInputGridEntry, rhs: ChatMediaInputGridEntry) -> Bool { switch lhs { - case let .search(lhsTheme, lhsStrings): - if case let .search(rhsTheme, rhsStrings) = rhs { - if lhsTheme !== rhsTheme { - return false - } - if lhsStrings !== rhsStrings { - return false - } - return true - } else { + case let .search(lhsTheme, lhsStrings): + if case let .search(rhsTheme, rhsStrings) = rhs { + if lhsTheme !== rhsTheme { return false } - case let .peerSpecificSetup(lhsTheme, lhsStrings, lhsDismissed): - if case let .peerSpecificSetup(rhsTheme, rhsStrings, rhsDismissed) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDismissed == rhsDismissed { - return true - } else { + if lhsStrings !== rhsStrings { return false } - case let .sticker(lhsIndex, lhsStickerItem, lhsStickerPackInfo, lhsCanManagePeerSpecificPack, lhsTheme): - if case let .sticker(rhsIndex, rhsStickerItem, rhsStickerPackInfo, rhsCanManagePeerSpecificPack, rhsTheme) = rhs { - if lhsIndex != rhsIndex { - return false - } - if lhsStickerItem != rhsStickerItem { - return false - } - if lhsStickerPackInfo != rhsStickerPackInfo { - return false - } - if lhsCanManagePeerSpecificPack != rhsCanManagePeerSpecificPack { - return false - } - if lhsTheme !== rhsTheme { - return false - } - return true - } else { + return true + } else { + return false + } + case let .peerSpecificSetup(lhsTheme, lhsStrings, lhsDismissed): + if case let .peerSpecificSetup(rhsTheme, rhsStrings, rhsDismissed) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDismissed == rhsDismissed { + return true + } else { + return false + } + case let .sticker(lhsIndex, lhsStickerItem, lhsStickerPackInfo, lhsCanManagePeerSpecificPack, lhsTheme): + if case let .sticker(rhsIndex, rhsStickerItem, rhsStickerPackInfo, rhsCanManagePeerSpecificPack, rhsTheme) = rhs { + if lhsIndex != rhsIndex { return false } + if lhsStickerItem != rhsStickerItem { + return false + } + if lhsStickerPackInfo != rhsStickerPackInfo { + return false + } + if lhsCanManagePeerSpecificPack != rhsCanManagePeerSpecificPack { + return false + } + if lhsTheme !== rhsTheme { + return false + } + return true + } else { + return false + } + case let .trending(entry): + if case .trending(entry) = rhs { + return true + } else { + return false + } } } @@ -137,20 +161,22 @@ enum ChatMediaInputGridEntry: Equatable, Comparable, Identifiable { return lhs.index < rhs.index } - func item(account: Account, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> GridItem { + func item(account: Account, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingInteraction: TrendingPaneInteraction) -> GridItem { switch self { - case let .search(theme, strings): - return PaneSearchBarPlaceholderItem(theme: theme, strings: strings, type: .stickers, activate: { - inputNodeInteraction.toggleSearch(true, .sticker) - }) - case let .peerSpecificSetup(theme, strings, dismissed): - return StickerPanePeerSpecificSetupGridItem(theme: theme, strings: strings, setup: { - inputNodeInteraction.openPeerSpecificSettings() - }, dismiss: dismissed ? nil : { - inputNodeInteraction.dismissPeerSpecificSettings() - }) - case let .sticker(index, stickerItem, stickerPackInfo, canManagePeerSpecificPack, theme): - return ChatMediaInputStickerGridItem(account: account, collectionId: index.collectionId, stickerPackInfo: stickerPackInfo, index: index, stickerItem: stickerItem, canManagePeerSpecificPack: canManagePeerSpecificPack, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { }) + case let .search(theme, strings): + return PaneSearchBarPlaceholderItem(theme: theme, strings: strings, type: .stickers, activate: { + inputNodeInteraction.toggleSearch(true, .sticker) + }) + case let .peerSpecificSetup(theme, strings, dismissed): + return StickerPanePeerSpecificSetupGridItem(theme: theme, strings: strings, setup: { + inputNodeInteraction.openPeerSpecificSettings() + }, dismiss: dismissed ? nil : { + inputNodeInteraction.dismissPeerSpecificSettings() + }) + case let .sticker(index, stickerItem, stickerPackInfo, canManagePeerSpecificPack, theme): + return ChatMediaInputStickerGridItem(account: account, collectionId: index.collectionId, stickerPackInfo: stickerPackInfo, index: index, stickerItem: stickerItem, canManagePeerSpecificPack: canManagePeerSpecificPack, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { }) + case let .trending(entry): + return entry.item(account: account, interaction: trendingInteraction, grid: false) } } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMediaInputNode.swift b/submodules/TelegramUI/TelegramUI/ChatMediaInputNode.swift index 864816fb8b..4ceb7c00f9 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMediaInputNode.swift @@ -7,6 +7,7 @@ import TelegramCore import SyncCore import SwiftSignalKit import TelegramPresentationData +import TelegramUIPreferences import MergeLists import AccountContext import StickerPackPreviewUI @@ -14,6 +15,8 @@ import PeerInfoUI import SettingsUI import ContextUI import GalleryUI +import OverlayStatusController +import PresentationDataUtils private struct PeerSpecificPackData { let peer: Peer @@ -43,17 +46,17 @@ private struct ChatMediaInputGridTransition { let animated: Bool } -private func preparedChatMediaInputPanelEntryTransition(account: Account, from fromEntries: [ChatMediaInputPanelEntry], to toEntries: [ChatMediaInputPanelEntry], inputNodeInteraction: ChatMediaInputNodeInteraction) -> ChatMediaInputPanelTransition { +private func preparedChatMediaInputPanelEntryTransition(context: AccountContext, from fromEntries: [ChatMediaInputPanelEntry], to toEntries: [ChatMediaInputPanelEntry], inputNodeInteraction: ChatMediaInputNodeInteraction) -> ChatMediaInputPanelTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, inputNodeInteraction: inputNodeInteraction), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, inputNodeInteraction: inputNodeInteraction), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, inputNodeInteraction: inputNodeInteraction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, inputNodeInteraction: inputNodeInteraction), directionHint: nil) } return ChatMediaInputPanelTransition(deletions: deletions, insertions: insertions, updates: updates) } -private func preparedChatMediaInputGridEntryTransition(account: Account, view: ItemCollectionsView, from fromEntries: [ChatMediaInputGridEntry], to toEntries: [ChatMediaInputGridEntry], update: StickerPacksCollectionUpdate, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> ChatMediaInputGridTransition { +private func preparedChatMediaInputGridEntryTransition(account: Account, view: ItemCollectionsView, from fromEntries: [ChatMediaInputGridEntry], to toEntries: [ChatMediaInputGridEntry], update: StickerPacksCollectionUpdate, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingInteraction: TrendingPaneInteraction) -> ChatMediaInputGridTransition { var stationaryItems: GridNodeStationaryItems = .none var scrollToItem: GridNodeScrollToItem? var animated = false @@ -61,10 +64,10 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, view: I case .initial: for i in (0 ..< toEntries.count).reversed() { switch toEntries[i] { - case .search, .peerSpecificSetup: - break - case .sticker: - scrollToItem = GridNodeScrollToItem(index: i, position: .top(0.0), transition: .immediate, directionHint: .down, adjustForSection: true, adjustForTopInset: true) + case .search, .peerSpecificSetup, .trending: + break + case .sticker: + scrollToItem = GridNodeScrollToItem(index: i, position: .top(0.0), transition: .immediate, directionHint: .down, adjustForSection: true, adjustForTopInset: true) } } case .generic: @@ -130,16 +133,16 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, view: I let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices - let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction), previousIndex: $0.2) } - let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction)) } + let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction, trendingInteraction: trendingInteraction), previousIndex: $0.2) } + let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction, trendingInteraction: trendingInteraction)) } var firstIndexInSectionOffset = 0 if !toEntries.isEmpty { switch toEntries[0].index { - case .search, .peerSpecificSetup: - break - case let .collectionIndex(index): - firstIndexInSectionOffset = Int(index.itemIndex.index) + case .search, .peerSpecificSetup, .trending: + break + case let .collectionIndex(index): + firstIndexInSectionOffset = Int(index.itemIndex.index) } } @@ -160,10 +163,10 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, view: I return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, updateOpaqueState: opaqueState, animated: animated) } -private func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, hasUnreadTrending: Bool, theme: PresentationTheme) -> [ChatMediaInputPanelEntry] { +private func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, hasUnreadTrending: Bool?, theme: PresentationTheme) -> [ChatMediaInputPanelEntry] { var entries: [ChatMediaInputPanelEntry] = [] entries.append(.recentGifs(theme)) - if hasUnreadTrending { + if let hasUnreadTrending = hasUnreadTrending, hasUnreadTrending { entries.append(.trending(true, theme)) } if let savedStickers = savedStickers, !savedStickers.items.isEmpty { @@ -208,7 +211,7 @@ private func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers entries.append(.peerSpecific(theme: theme, peer: peer)) } - if !hasUnreadTrending { + if let hasUnreadTrending = hasUnreadTrending, !hasUnreadTrending { entries.append(.trending(false, theme)) } entries.append(.settings(theme)) @@ -434,6 +437,7 @@ final class ChatMediaInputNode: ChatInputNode { private var theme: PresentationTheme private var strings: PresentationStrings + private var fontSize: PresentationFontSize private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> private let _ready = Promise() @@ -442,19 +446,25 @@ final class ChatMediaInputNode: ChatInputNode { return self._ready.get() } - init(context: AccountContext, peerId: PeerId?, controllerInteraction: ChatControllerInteraction, theme: PresentationTheme, strings: PresentationStrings, gifPaneIsActiveUpdated: @escaping (Bool) -> Void) { + init(context: AccountContext, peerId: PeerId?, controllerInteraction: ChatControllerInteraction, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, gifPaneIsActiveUpdated: @escaping (Bool) -> Void) { self.context = context self.peerId = peerId self.controllerInteraction = controllerInteraction self.theme = theme self.strings = strings + self.fontSize = fontSize self.gifPaneIsActiveUpdated = gifPaneIsActiveUpdated self.themeAndStringsPromise = Promise((theme, strings)) self.collectionListPanel = ASDisplayNode() self.collectionListPanel.clipsToBounds = true - self.collectionListPanel.backgroundColor = theme.chat.inputPanel.panelBackgroundColor + + if case let .color(color) = chatWallpaper, UIColor(rgb: color).isEqual(theme.chat.inputPanel.panelBackgroundColorNoWallpaper) { + self.collectionListPanel.backgroundColor = theme.chat.inputPanel.panelBackgroundColorNoWallpaper + } else { + self.collectionListPanel.backgroundColor = theme.chat.inputPanel.panelBackgroundColor + } self.collectionListSeparator = ASDisplayNode() self.collectionListSeparator.isLayerBacked = true @@ -486,7 +496,7 @@ final class ChatMediaInputNode: ChatInputNode { var getItemIsPreviewedImpl: ((StickerPackItem) -> Bool)? self.trendingPane = ChatMediaInputTrendingPane(context: context, controllerInteraction: controllerInteraction, getItemIsPreviewed: { item in return getItemIsPreviewedImpl?(item) ?? false - }) + }, isPane: true) self.paneArrangement = ChatMediaInputPaneArrangement(panes: [.gifs, .stickers, .trending], currentIndex: 1, indexTransition: 0.0) @@ -589,7 +599,7 @@ final class ChatMediaInputNode: ChatInputNode { self?.dismissPeerSpecificPackSetup() }, clearRecentlyUsedStickers: { [weak self] in if let strongSelf = self { - let actionSheet = ActionSheetController(presentationTheme: strongSelf.theme) + let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: strongSelf.theme, fontSize: strongSelf.fontSize)) var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: strongSelf.strings.Stickers_ClearRecent, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -598,7 +608,7 @@ final class ChatMediaInputNode: ChatInputNode { }).start() })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -701,21 +711,56 @@ final class ChatMediaInputNode: ChatInputNode { peerSpecificPack = .single((nil, .none)) } - let hasUnreadTrending = context.account.viewTracker.featuredStickerPacks() - |> map { packs -> Bool in - for pack in packs { - if pack.unread { - return true - } + let trendingInteraction = TrendingPaneInteraction(installPack: { [weak self] info in + guard let strongSelf = self, let info = info as? StickerPackCollectionInfo else { + return } - return false - } - |> distinctUntilChanged + let _ = (loadedStickerPack(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false) + |> mapToSignal { result -> Signal in + switch result { + case let .result(info, items, installed): + if installed { + return .complete() + } else { + return addStickerPackInteractively(postbox: strongSelf.context.account.postbox, info: info, items: items) + } + case .fetching: + break + case .none: + break + } + return .complete() + } + |> deliverOnMainQueue).start(completed: { + guard let strongSelf = self else { + return + } + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + strongSelf.controllerInteraction.presentController(OverlayStatusController(theme: presentationData.theme, type: .success), nil) + }) + }, openPack: { [weak self] info in + guard let strongSelf = self, let info = info as? StickerPackCollectionInfo else { + return + } + strongSelf.view.window?.endEditing(true) + let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) + let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { fileReference, sourceNode, sourceRect in + if let strongSelf = self { + return strongSelf.controllerInteraction.sendSticker(fileReference, false, sourceNode, sourceRect) + } else { + return false + } + }) + strongSelf.controllerInteraction.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, getItemIsPreviewed: { item in + return getItemIsPreviewedImpl?(item) ?? false + }, openSearch: { + }) let previousView = Atomic(value: nil) let transitionQueue = Queue() - let transitions = combineLatest(queue: transitionQueue, itemCollectionsView, peerSpecificPack, hasUnreadTrending, self.themeAndStringsPromise.get()) - |> map { viewAndUpdate, peerSpecificPack, hasUnreadTrending, themeAndStrings -> (ItemCollectionsView, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in + let transitions = combineLatest(queue: transitionQueue, itemCollectionsView, peerSpecificPack, context.account.viewTracker.featuredStickerPacks(), self.themeAndStringsPromise.get()) + |> map { viewAndUpdate, peerSpecificPack, trendingPacks, themeAndStrings -> (ItemCollectionsView, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in let (view, viewUpdate) = viewAndUpdate let previous = previousView.swap(view) var update = viewUpdate @@ -733,10 +778,40 @@ final class ChatMediaInputNode: ChatInputNode { savedStickers = orderedView } } + + var installedPacks = Set() + for info in view.collectionInfos { + installedPacks.insert(info.0) + } + + var hasUnreadTrending: Bool? + for pack in trendingPacks { + if !installedPacks.contains(pack.info.id) { + if hasUnreadTrending == nil { + hasUnreadTrending = false + } + if pack.unread { + hasUnreadTrending = true + break + } + } + } + let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, hasUnreadTrending: hasUnreadTrending, theme: theme) - let gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, strings: strings, theme: theme) + var gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, strings: strings, theme: theme) + + if view.higher == nil { + var index = 0 + for item in trendingPacks { + if !installedPacks.contains(item.info.id) { + gridEntries.append(.trending(TrendingPanePackEntry(index: index, info: item.info, theme: theme, strings: strings, topItems: item.topItems, installed: installedPacks.contains(item.info.id), unread: item.unread, topSeparator: true))) + index += 1 + } + } + } + let (previousPanelEntries, previousGridEntries) = previousEntries.swap((panelEntries, gridEntries)) - return (view, preparedChatMediaInputPanelEntryTransition(account: context.account, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: context.account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction), previousGridEntries.isEmpty) + return (view, preparedChatMediaInputPanelEntryTransition(context: context, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: context.account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction, trendingInteraction: trendingInteraction), previousGridEntries.isEmpty) } self.disposable.set((transitions @@ -803,6 +878,7 @@ final class ChatMediaInputNode: ChatInputNode { self.stickerPane.inputNodeInteraction = self.inputNodeInteraction self.gifPane.inputNodeInteraction = self.inputNodeInteraction + self.trendingPane.inputNodeInteraction = self.inputNodeInteraction paneDidScrollImpl = { [weak self] pane, state, transition in self?.updatePaneDidScroll(pane: pane, state: state, transition: transition) @@ -839,7 +915,7 @@ final class ChatMediaInputNode: ChatInputNode { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let contextController = ContextController(account: strongSelf.context.account, theme: presentationData.theme, strings: presentationData.strings, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: sourceNode, sourceRect: sourceRect)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: sourceNode, sourceRect: sourceRect)), items: .single(items), reactionItems: [], gesture: gesture) strongSelf.controllerInteraction.presentGlobalOverlayController(contextController, nil) } } @@ -849,12 +925,17 @@ final class ChatMediaInputNode: ChatInputNode { self.searchContainerNodeLoadedDisposable.dispose() } - private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + private func updateThemeAndStrings(chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings) { if self.theme !== theme || self.strings !== strings { self.theme = theme self.strings = strings - self.collectionListPanel.backgroundColor = theme.chat.inputPanel.panelBackgroundColor + if case let .color(color) = chatWallpaper, UIColor(rgb: color).isEqual(theme.chat.inputPanel.panelBackgroundColorNoWallpaper) { + self.collectionListPanel.backgroundColor = theme.chat.inputPanel.panelBackgroundColorNoWallpaper + } else { + self.collectionListPanel.backgroundColor = theme.chat.inputPanel.panelBackgroundColor + } + self.collectionListSeparator.backgroundColor = theme.chat.inputMediaPanel.panelSeparatorColor self.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor @@ -911,14 +992,13 @@ final class ChatMediaInputNode: ChatInputNode { switch attribute { case let .Sticker(_, packReference, _): if let packReference = packReference { - let controller = StickerPackPreviewController(context: strongSelf.context, stickerPack: packReference, parentNavigationController: strongSelf.controllerInteraction.navigationController()) - controller.sendSticker = { file, sourceNode, sourceRect in + let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in if let strongSelf = self { return strongSelf.controllerInteraction.sendSticker(file, false, sourceNode, sourceRect) } else { return false } - } + }) strongSelf.controllerInteraction.navigationController()?.view.window?.endEditing(true) strongSelf.controllerInteraction.presentController(controller, nil) @@ -931,7 +1011,7 @@ final class ChatMediaInputNode: ChatInputNode { } return true }), - PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: { _, _ in return true }) + PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true }) ] return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: item, menu: menuItems)) } else { @@ -1026,14 +1106,13 @@ final class ChatMediaInputNode: ChatInputNode { switch attribute { case let .Sticker(_, packReference, _): if let packReference = packReference { - let controller = StickerPackPreviewController(context: strongSelf.context, stickerPack: packReference, parentNavigationController: strongSelf.controllerInteraction.navigationController()) - controller.sendSticker = { file, sourceNode, sourceRect in - if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(file, false, sourceNode, sourceRect) - } else { - return false - } - } + let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in + if let strongSelf = self { + return strongSelf.controllerInteraction.sendSticker(file, false, sourceNode, sourceRect) + } else { + return false + } + }) strongSelf.controllerInteraction.navigationController()?.view.window?.endEditing(true) strongSelf.controllerInteraction.presentController(controller, nil) @@ -1046,7 +1125,7 @@ final class ChatMediaInputNode: ChatInputNode { } return true }), - PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: { _, _ in return true }) + PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true }) ] return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) } else { @@ -1083,14 +1162,23 @@ final class ChatMediaInputNode: ChatInputNode { } private func setCurrentPane(_ pane: ChatMediaInputPaneType, transition: ContainedViewLayoutTransition, collectionIdHint: Int32? = nil) { + var transition = transition + if let index = self.paneArrangement.panes.firstIndex(of: pane), index != self.paneArrangement.currentIndex { let previousGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs + //let previousTrendingPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .trending self.paneArrangement = self.paneArrangement.withIndexTransition(0.0).withCurrentIndex(index) + let updatedGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs + //let updatedTrendingPanelIsActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .trending + + /*if updatedTrendingPanelIsActive != previousTrendingPanelWasActive { + transition = .immediate + }*/ + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) = self.validLayout { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible) + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: transition, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible) self.updateAppearanceTransition(transition: transition) } - let updatedGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs if updatedGifPanelWasActive != previousGifPanelWasActive { self.gifPaneIsActiveUpdated(updatedGifPanelWasActive) } @@ -1106,6 +1194,20 @@ final class ChatMediaInputNode: ChatInputNode { case .trending: self.setHighlightedItemCollectionId(ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue, id: 0)) } + /*if updatedTrendingPanelIsActive != previousTrendingPanelWasActive { + self.controllerInteraction.updateInputMode { current in + switch current { + case let .media(mode, _): + if updatedTrendingPanelIsActive { + return .media(mode: mode, expanded: .content) + } else { + return .media(mode: mode, expanded: nil) + } + default: + return current + } + } + }*/ } else { if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) = self.validLayout { let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible) @@ -1247,7 +1349,7 @@ final class ChatMediaInputNode: ChatInputNode { self.validLayout = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) if self.theme !== interfaceState.theme || self.strings !== interfaceState.strings { - self.updateThemeAndStrings(theme: interfaceState.theme, strings: interfaceState.strings) + self.updateThemeAndStrings(chatWallpaper: interfaceState.chatWallpaper, theme: interfaceState.theme, strings: interfaceState.strings) } var displaySearch = false @@ -1287,14 +1389,20 @@ final class ChatMediaInputNode: ChatInputNode { var placeholderNode: PaneSearchBarPlaceholderNode? if let searchMode = searchMode { switch searchMode { - case .gif: - placeholderNode = self.gifPane.searchPlaceholderNode - case .sticker: - self.stickerPane.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? PaneSearchBarPlaceholderNode { - placeholderNode = itemNode - } + case .gif: + placeholderNode = self.gifPane.searchPlaceholderNode + case .sticker: + self.stickerPane.gridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? PaneSearchBarPlaceholderNode { + placeholderNode = itemNode } + } + case .trending: + self.trendingPane.gridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? PaneSearchBarPlaceholderNode { + placeholderNode = itemNode + } + } } } @@ -1318,29 +1426,8 @@ final class ChatMediaInputNode: ChatInputNode { self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0, height: width) transition.updatePosition(node: self.listView, position: CGPoint(x: width / 2.0, y: (41.0 - collectionListPanelOffset) / 2.0)) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: 41.0, height: width), insets: UIEdgeInsets(top: 4.0 + leftInset, left: 0.0, bottom: 4.0 + rightInset, right: 0.0), duration: duration, curve: listViewCurve) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: 41.0, height: width), insets: UIEdgeInsets(top: 4.0 + leftInset, left: 0.0, bottom: 4.0 + rightInset, right: 0.0), duration: duration, curve: curve) self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) @@ -1479,15 +1566,22 @@ final class ChatMediaInputNode: ChatInputNode { var placeholderNode: PaneSearchBarPlaceholderNode? if let searchMode = searchMode { switch searchMode { - case .gif: - placeholderNode = self.gifPane.searchPlaceholderNode - paneIsEmpty = self.gifPane.isEmpty - case .sticker: - self.stickerPane.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? PaneSearchBarPlaceholderNode { - placeholderNode = itemNode - } + case .gif: + placeholderNode = self.gifPane.searchPlaceholderNode + paneIsEmpty = self.gifPane.isEmpty + case .sticker: + self.stickerPane.gridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? PaneSearchBarPlaceholderNode { + placeholderNode = itemNode } + } + case .trending: + self.trendingPane.gridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? PaneSearchBarPlaceholderNode { + placeholderNode = itemNode + } + } + paneIsEmpty = true } } if let placeholderNode = placeholderNode { diff --git a/submodules/TelegramUI/TelegramUI/ChatMediaInputPanelEntries.swift b/submodules/TelegramUI/TelegramUI/ChatMediaInputPanelEntries.swift index 7e5537aba7..da90401935 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMediaInputPanelEntries.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMediaInputPanelEntries.swift @@ -6,6 +6,7 @@ import SwiftSignalKit import Display import TelegramPresentationData import MergeLists +import AccountContext enum ChatMediaInputPanelAuxiliaryNamespace: Int32 { case savedStickers = 2 @@ -242,7 +243,7 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { } } - func item(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction) -> ListViewItem { + func item(context: AccountContext, inputNodeInteraction: ChatMediaInputNodeInteraction) -> ListViewItem { switch self { case let .recentGifs(theme): return ChatMediaInputRecentGifsItem(inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { @@ -270,11 +271,11 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { }) case let .peerSpecific(theme, peer): let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue, id: 0) - return ChatMediaInputPeerSpecificItem(account: account, inputNodeInteraction: inputNodeInteraction, collectionId: collectionId, peer: peer, theme: theme, selected: { + return ChatMediaInputPeerSpecificItem(context: context, inputNodeInteraction: inputNodeInteraction, collectionId: collectionId, peer: peer, theme: theme, selected: { inputNodeInteraction.navigateToCollectionId(collectionId) }) case let .stickerPack(index, info, topItem, theme): - return ChatMediaInputStickerPackItem(account: account, inputNodeInteraction: inputNodeInteraction, collectionId: info.id, collectionInfo: info, stickerPackItem: topItem, index: index, theme: theme, selected: { + return ChatMediaInputStickerPackItem(account: context.account, inputNodeInteraction: inputNodeInteraction, collectionId: info.id, collectionInfo: info, stickerPackItem: topItem, index: index, theme: theme, selected: { inputNodeInteraction.navigateToCollectionId(info.id) }) } diff --git a/submodules/TelegramUI/TelegramUI/ChatMediaInputPeerSpecificItem.swift b/submodules/TelegramUI/TelegramUI/ChatMediaInputPeerSpecificItem.swift index cbef9e9360..834fcb7b06 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMediaInputPeerSpecificItem.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMediaInputPeerSpecificItem.swift @@ -8,9 +8,10 @@ import SwiftSignalKit import Postbox import TelegramPresentationData import AvatarNode +import AccountContext final class ChatMediaInputPeerSpecificItem: ListViewItem { - let account: Account + let context: AccountContext let inputNodeInteraction: ChatMediaInputNodeInteraction let collectionId: ItemCollectionId let peer: Peer @@ -21,8 +22,8 @@ final class ChatMediaInputPeerSpecificItem: ListViewItem { return true } - init(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction, collectionId: ItemCollectionId, peer: Peer, theme: PresentationTheme, selected: @escaping () -> Void) { - self.account = account + init(context: AccountContext, inputNodeInteraction: ChatMediaInputNodeInteraction, collectionId: ItemCollectionId, peer: Peer, theme: PresentationTheme, selected: @escaping () -> Void) { + self.context = context self.inputNodeInteraction = inputNodeInteraction self.collectionId = collectionId self.peer = peer @@ -39,7 +40,7 @@ final class ChatMediaInputPeerSpecificItem: ListViewItem { Queue.mainQueue().async { completion(node, { return (nil, { _ in - node.updateItem(account: self.account, peer: self.peer, collectionId: self.collectionId, theme: self.theme) + node.updateItem(context: self.context, peer: self.peer, collectionId: self.collectionId, theme: self.theme) node.updateAppearanceTransition(transition: .immediate) }) }) @@ -50,7 +51,7 @@ final class ChatMediaInputPeerSpecificItem: ListViewItem { public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { completion(ListViewItemNodeLayout(contentSize: node().contentSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { _ in - (node() as? ChatMediaInputPeerSpecificItemNode)?.updateItem(account: self.account, peer: self.peer, collectionId: self.collectionId, theme: self.theme) + (node() as? ChatMediaInputPeerSpecificItemNode)?.updateItem(context: self.context, peer: self.peer, collectionId: self.collectionId, theme: self.theme) }) } } @@ -100,7 +101,7 @@ final class ChatMediaInputPeerSpecificItemNode: ListViewItemNode { self.stickerFetchedDisposable.dispose() } - func updateItem(account: Account, peer: Peer, collectionId: ItemCollectionId, theme: PresentationTheme) { + func updateItem(context: AccountContext, peer: Peer, collectionId: ItemCollectionId, theme: PresentationTheme) { self.currentCollectionId = collectionId if self.theme !== theme { @@ -109,7 +110,7 @@ final class ChatMediaInputPeerSpecificItemNode: ListViewItemNode { self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme) } - self.avatarNode.setPeer(account: account, theme: theme, peer: peer) + self.avatarNode.setPeer(context: context, theme: theme, peer: peer) } func updateIsHighlighted() { diff --git a/submodules/TelegramUI/TelegramUI/ChatMediaInputTrendingPane.swift b/submodules/TelegramUI/TelegramUI/ChatMediaInputTrendingPane.swift index 366ded3c86..f53007f746 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMediaInputTrendingPane.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMediaInputTrendingPane.swift @@ -12,20 +12,23 @@ import OverlayStatusController import AccountContext import StickerPackPreviewUI import PresentationDataUtils +import UndoUI final class TrendingPaneInteraction { let installPack: (ItemCollectionInfo) -> Void let openPack: (ItemCollectionInfo) -> Void let getItemIsPreviewed: (StickerPackItem) -> Bool + let openSearch: () -> Void - init(installPack: @escaping (ItemCollectionInfo) -> Void, openPack: @escaping (ItemCollectionInfo) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { + init(installPack: @escaping (ItemCollectionInfo) -> Void, openPack: @escaping (ItemCollectionInfo) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, openSearch: @escaping () -> Void) { self.installPack = installPack self.openPack = openPack self.getItemIsPreviewed = getItemIsPreviewed + self.openSearch = openSearch } } -private final class TrendingPaneEntry: Identifiable, Comparable { +final class TrendingPanePackEntry: Identifiable, Comparable { let index: Int let info: StickerPackCollectionInfo let theme: PresentationTheme @@ -33,8 +36,9 @@ private final class TrendingPaneEntry: Identifiable, Comparable { let topItems: [StickerPackItem] let installed: Bool let unread: Bool + let topSeparator: Bool - init(index: Int, info: StickerPackCollectionInfo, theme: PresentationTheme, strings: PresentationStrings, topItems: [StickerPackItem], installed: Bool, unread: Bool) { + init(index: Int, info: StickerPackCollectionInfo, theme: PresentationTheme, strings: PresentationStrings, topItems: [StickerPackItem], installed: Bool, unread: Bool, topSeparator: Bool) { self.index = index self.info = info self.theme = theme @@ -42,13 +46,14 @@ private final class TrendingPaneEntry: Identifiable, Comparable { self.topItems = topItems self.installed = installed self.unread = unread + self.topSeparator = topSeparator } var stableId: ItemCollectionId { return self.info.id } - static func ==(lhs: TrendingPaneEntry, rhs: TrendingPaneEntry) -> Bool { + static func ==(lhs: TrendingPanePackEntry, rhs: TrendingPanePackEntry) -> Bool { if lhs.index != rhs.index { return false } @@ -70,16 +75,19 @@ private final class TrendingPaneEntry: Identifiable, Comparable { if lhs.unread != rhs.unread { return false } + if lhs.topSeparator != rhs.topSeparator { + return false + } return true } - static func <(lhs: TrendingPaneEntry, rhs: TrendingPaneEntry) -> Bool { + static func <(lhs: TrendingPanePackEntry, rhs: TrendingPanePackEntry) -> Bool { return lhs.index < rhs.index } - func item(account: Account, interaction: TrendingPaneInteraction) -> GridItem { + func item(account: Account, interaction: TrendingPaneInteraction, grid: Bool) -> GridItem { let info = self.info - return StickerPaneSearchGlobalItem(account: account, theme: self.theme, strings: self.strings, info: self.info, topItems: self.topItems, grid: true, installed: self.installed, unread: self.unread, open: { + return StickerPaneSearchGlobalItem(account: account, theme: self.theme, strings: self.strings, info: self.info, topItems: self.topItems, grid: grid, topSeparator: self.topSeparator, installed: self.installed, unread: self.unread, open: { interaction.openPack(info) }, install: { interaction.installPack(info) @@ -89,6 +97,67 @@ private final class TrendingPaneEntry: Identifiable, Comparable { } } +private enum TrendingPaneEntryId: Hashable { + case search + case pack(ItemCollectionId) +} + +private enum TrendingPaneEntry: Identifiable, Comparable { + case search(theme: PresentationTheme, strings: PresentationStrings) + case pack(TrendingPanePackEntry) + + var stableId: TrendingPaneEntryId { + switch self { + case .search: + return .search + case let .pack(pack): + return .pack(pack.stableId) + } + } + + static func ==(lhs: TrendingPaneEntry, rhs: TrendingPaneEntry) -> Bool { + switch lhs { + case let .search(lhsTheme, lhsStrings): + if case let .search(rhsTheme, rhsStrings) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings { + return true + } else { + return false + } + case let .pack(pack): + if case .pack(pack) = rhs { + return true + } else { + return false + } + } + } + + static func <(lhs: TrendingPaneEntry, rhs: TrendingPaneEntry) -> Bool { + switch lhs { + case .search: + return false + case let .pack(lhsPack): + switch rhs { + case .search: + return false + case let .pack(rhsPack): + return lhsPack < rhsPack + } + } + } + + func item(account: Account, interaction: TrendingPaneInteraction, grid: Bool) -> GridItem { + switch self { + case let .search(theme, strings): + return PaneSearchBarPlaceholderItem(theme: theme, strings: strings, type: .stickers, activate: { + interaction.openSearch() + }) + case let .pack(pack): + return pack.item(account: account, interaction: interaction, grid: grid) + } + } +} + private struct TrendingPaneTransition { let deletions: [Int] let insertions: [GridNodeInsertItem] @@ -100,18 +169,21 @@ private func preparedTransition(from fromEntries: [TrendingPaneEntry], to toEntr let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices - let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction), previousIndex: $0.2) } - let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction)) } + let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction, grid: false), previousIndex: $0.2) } + let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction, grid: false)) } return TrendingPaneTransition(deletions: deletions, insertions: insertions, updates: updates, initial: initial) } -private func trendingPaneEntries(trendingEntries: [FeaturedStickerPackItem], installedPacks: Set, theme: PresentationTheme, strings: PresentationStrings) -> [TrendingPaneEntry] { +private func trendingPaneEntries(trendingEntries: [FeaturedStickerPackItem], installedPacks: Set, theme: PresentationTheme, strings: PresentationStrings, isPane: Bool) -> [TrendingPaneEntry] { var result: [TrendingPaneEntry] = [] var index = 0 + if isPane { + result.append(.search(theme: theme, strings: strings)) + } for item in trendingEntries { if !installedPacks.contains(item.info.id) { - result.append(TrendingPaneEntry(index: index, info: item.info, theme: theme, strings: strings, topItems: item.topItems, installed: installedPacks.contains(item.info.id), unread: item.unread)) + result.append(.pack(TrendingPanePackEntry(index: index, info: item.info, theme: theme, strings: strings, topItems: item.topItems, installed: installedPacks.contains(item.info.id), unread: item.unread, topSeparator: index != 0))) index += 1 } } @@ -122,6 +194,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { private let context: AccountContext private let controllerInteraction: ChatControllerInteraction private let getItemIsPreviewed: (StickerPackItem) -> Bool + private let isPane: Bool let gridNode: GridNode @@ -139,10 +212,13 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { var scrollingInitiated: (() -> Void)? - init(context: AccountContext, controllerInteraction: ChatControllerInteraction, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { + private let installDisposable = MetaDisposable() + + init(context: AccountContext, controllerInteraction: ChatControllerInteraction, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, isPane: Bool) { self.context = context self.controllerInteraction = controllerInteraction self.getItemIsPreviewed = getItemIsPreviewed + self.isPane = isPane self.gridNode = GridNode() @@ -157,6 +233,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { deinit { self.disposable?.dispose() + self.installDisposable.dispose() } func activate() { @@ -167,43 +244,103 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { let interaction = TrendingPaneInteraction(installPack: { [weak self] info in if let strongSelf = self, let info = info as? StickerPackCollectionInfo { - let _ = (loadedStickerPack(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false) - |> mapToSignal { result -> Signal in + let account = strongSelf.context.account + var installSignal = loadedStickerPack(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false) + |> mapToSignal { result -> Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError> in switch result { - case let .result(info, items, installed): - if installed { + case let .result(info, items, installed): + if installed { + return .complete() + } else { + return preloadedStickerPackThumbnail(account: account, info: info, items: items) + |> filter { $0 } + |> ignoreValues + |> then( + addStickerPackInteractively(postbox: strongSelf.context.account.postbox, info: info, items: items) + |> ignoreValues + ) + |> mapToSignal { _ -> Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError> in return .complete() - } else { - return addStickerPackInteractively(postbox: strongSelf.context.account.postbox, info: info, items: items) } - case .fetching: - break - case .none: - break + |> then(.single((info, items))) + } + case .fetching: + break + case .none: + break } return .complete() - } |> deliverOnMainQueue).start(completed: { - if let strongSelf = self { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - strongSelf.controllerInteraction.presentController(OverlayStatusController(theme: presentationData.theme, type: .success), nil) + } + |> deliverOnMainQueue + + let context = strongSelf.context + var cancelImpl: (() -> Void)? + let progressSignal = Signal { subscriber in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + self?.controllerInteraction.presentController(controller, nil) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } } - }) + } + |> runOn(Queue.mainQueue()) + |> delay(1.0, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + installSignal = installSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { + self?.installDisposable.set(nil) + } + + strongSelf.installDisposable.set(installSignal.start(next: { info, items in + guard let strongSelf = self else { + return + } + + var animateInAsReplacement = false + if let navigationController = strongSelf.controllerInteraction.navigationController() { + for controller in navigationController.overlayControllers { + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitActionAndReplacementAnimation() + animateInAsReplacement = true + } + } + } + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + strongSelf.controllerInteraction.navigationController()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).0, undo: false, info: info, topItem: items.first, account: strongSelf.context.account), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in + return true + })) + })) } }, openPack: { [weak self] info in if let strongSelf = self, let info = info as? StickerPackCollectionInfo { strongSelf.view.window?.endEditing(true) - let controller = StickerPackPreviewController(context: strongSelf.context, stickerPack: .id(id: info.id.id, accessHash: info.accessHash), parentNavigationController: strongSelf.controllerInteraction.navigationController()) - controller.sendSticker = { fileReference, sourceNode, sourceRect in + let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) + let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { return strongSelf.controllerInteraction.sendSticker(fileReference, false, sourceNode, sourceRect) } else { return false } - } - strongSelf.controllerInteraction.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }) + strongSelf.controllerInteraction.presentController(controller, nil) } - }, getItemIsPreviewed: self.getItemIsPreviewed) + }, getItemIsPreviewed: self.getItemIsPreviewed, + openSearch: { [weak self] in + self?.inputNodeInteraction?.toggleSearch(true, .trending) + }) + let isPane = self.isPane let previousEntries = Atomic<[TrendingPaneEntry]?>(value: nil) let context = self.context self.disposable = (combineLatest(context.account.viewTracker.featuredStickerPacks(), context.account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])]), context.sharedContext.presentationData) @@ -216,7 +353,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { } } } - let entries = trendingPaneEntries(trendingEntries: trendingEntries, installedPacks: installedPacks, theme: presentationData.theme, strings: presentationData.strings) + let entries = trendingPaneEntries(trendingEntries: trendingEntries, installedPacks: installedPacks, theme: presentationData.theme, strings: presentationData.strings, isPane: isPane) let previous = previousEntries.swap(entries) return preparedTransition(from: previous ?? [], to: entries, account: context.account, interaction: interaction, initial: previous == nil) @@ -248,25 +385,6 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { transition.updateFrame(node: self.gridNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) -// transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) -// -// var duration: Double = 0.0 -// var listViewCurve: ListViewAnimationCurve = .Default(duration: nil) -// switch transition { -// case .immediate: -// break -// case let .animated(animationDuration, animationCurve): -// duration = animationDuration -// switch animationCurve { -// case .easeInOut, .custom: -// listViewCurve = .Default(duration: duration) -// case .spring: -// listViewCurve = .Spring(duration: duration) -// } -// } -// -// self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomInset, right: 0.0), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - if !hadValidLayout { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageActionButtonsNode.swift index af96c3b8f2..ab5189f89b 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageActionButtonsNode.swift @@ -84,12 +84,12 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { } } - class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ strings: PresentationStrings, _ message: Message, _ button: ReplyMarkupButton, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) { + class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ bubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ message: Message, _ button: ReplyMarkupButton, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) { let titleLayout = TextNode.asyncLayout(maybeNode?.titleNode) - return { context, theme, strings, message, button, constrainedWidth, position in + return { context, theme, bubbleCorners, strings, message, button, constrainedWidth, position in let incoming = message.effectivelyIncoming(context.account.peerId) - let graphics = PresentationResourcesChat.additionalGraphics(theme.theme, wallpaper: theme.wallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(theme.theme, wallpaper: theme.wallpaper, bubbleCorners: bubbleCorners) let iconImage: UIImage? switch button.action { @@ -216,10 +216,10 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { } } - class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ strings: PresentationStrings, _ replyMarkup: ReplyMarkupMessageAttribute, _ message: Message, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode)) { + class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ chatBubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ replyMarkup: ReplyMarkupMessageAttribute, _ message: Message, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode)) { let currentButtonLayouts = maybeNode?.buttonNodes.map { ChatMessageActionButtonNode.asyncLayout($0) } ?? [] - return { context, theme, strings, replyMarkup, message, constrainedWidth in + return { context, theme, chatBubbleCorners, strings, replyMarkup, message, constrainedWidth in let buttonHeight: CGFloat = 42.0 let buttonSpacing: CGFloat = 4.0 @@ -252,9 +252,9 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { let prepareButtonLayout: (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) if buttonIndex < currentButtonLayouts.count { - prepareButtonLayout = currentButtonLayouts[buttonIndex](context, theme, strings, message, button, maximumButtonWidth, buttonPosition) + prepareButtonLayout = currentButtonLayouts[buttonIndex](context, theme, chatBubbleCorners, strings, message, button, maximumButtonWidth, buttonPosition) } else { - prepareButtonLayout = ChatMessageActionButtonNode.asyncLayout(nil)(context, theme, strings, message, button, maximumButtonWidth, buttonPosition) + prepareButtonLayout = ChatMessageActionButtonNode.asyncLayout(nil)(context, theme, chatBubbleCorners, strings, message, button, maximumButtonWidth, buttonPosition) } maximumRowButtonWidth = max(maximumRowButtonWidth, prepareButtonLayout.minimumWidth) diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageActionItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageActionItemNode.swift index f39d214451..71a6d84dbb 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageActionItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageActionItemNode.swift @@ -50,9 +50,9 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { super.didLoad() } - override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if let imageNode = self.imageNode, self.item?.message.id == messageId { - return (imageNode, { [weak imageNode] in + return (imageNode, imageNode.bounds, { [weak imageNode] in return (imageNode?.view.snapshotContentTree(unhide: true), nil) }) } else { @@ -244,7 +244,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { } } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction { + override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.labelNode.frame if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)), gesture == .tap { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageActionUrlAuthController.swift b/submodules/TelegramUI/TelegramUI/ChatMessageActionUrlAuthController.swift index 7d6c017dbb..a3bf69bcfb 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageActionUrlAuthController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageActionUrlAuthController.swift @@ -318,8 +318,8 @@ func chatMessageActionUrlAuthController(context: AccountContext, defaultUrl: Str open(contentNode.authorize, contentNode.allowWriteAccess) } })] - contentNode = ChatMessageActionUrlAuthAlertContentNode(theme: AlertControllerTheme(presentationTheme: theme), ptheme: theme, strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, defaultUrl: defaultUrl, domain: domain, bot: bot, requestWriteAccess: requestWriteAccess, displayName: displayName, actions: actions) - let controller = AlertController(theme: AlertControllerTheme(presentationTheme: theme), contentNode: contentNode!) + contentNode = ChatMessageActionUrlAuthAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, defaultUrl: defaultUrl, domain: domain, bot: bot, requestWriteAccess: requestWriteAccess, displayName: displayName, actions: actions) + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) dismissImpl = { [weak controller] animated in if animated { controller?.dismissAnimated() diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift index c990852b71..a6ac25c1ff 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift @@ -254,16 +254,38 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } let (emoji, fitz) = item.message.text.basicEmoji - if self.telegramFile == nil, let emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.file { - if self.emojiFile?.id != emojiFile.id { - self.emojiFile = emojiFile - let dimensions = emojiFile.dimensions ?? PixelDimensions(width: 512, height: 512) - var fitzModifier: EmojiFitzModifier? - if let fitz = fitz { - fitzModifier = EmojiFitzModifier(emoji: fitz) + if self.telegramFile == nil { + var emojiFile: TelegramMediaFile? + + if emoji == "🎲" { + var pointsValue: Int + if let value = item.controllerInteraction.seenDicePointsValue[item.message.id] { + pointsValue = value + } else { + pointsValue = Int(arc4random_uniform(6)) + item.controllerInteraction.seenDicePointsValue[item.message.id] = pointsValue + } + if let diceEmojis = item.associatedData.animatedEmojiStickers[emoji] { + emojiFile = diceEmojis[pointsValue].file + } + } else { + emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file + if emojiFile == nil { + emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file + } + } + + if self.emojiFile?.id != emojiFile?.id { + self.emojiFile = emojiFile + if let emojiFile = emojiFile { + let dimensions = emojiFile.dimensions ?? PixelDimensions(width: 512, height: 512) + var fitzModifier: EmojiFitzModifier? + if let fitz = fitz { + fitzModifier = EmojiFitzModifier(emoji: fitz) + } + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.context.account.postbox, file: emojiFile, small: false, size: dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)), fitzModifier: fitzModifier, thumbnail: false)) + self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .standalone(media: emojiFile)).start()) } - self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.context.account.postbox, file: emojiFile, small: false, size: dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)), fitzModifier: fitzModifier, thumbnail: false)) - self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .standalone(media: emojiFile)).start()) self.updateVisibility() } } @@ -286,8 +308,12 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } self.animationNode.visibility = isPlaying && !alreadySeen + if self.didSetUpAnimationNode && alreadySeen { - self.animationNode.seekToStart() + if let emojiFile = self.emojiFile, emojiFile.resource is LocalFileReferenceMediaResource { + } else { + self.animationNode.seekTo(.start) + } } if self.isPlaying && !self.didSetUpAnimationNode { @@ -306,7 +332,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } else if let emojiFile = self.emojiFile { isEmoji = true file = emojiFile - playbackMode = .once + if alreadySeen && emojiFile.resource is LocalFileReferenceMediaResource { + playbackMode = .still(.end) + } else { + playbackMode = .once + } let (_, fitz) = item.message.text.basicEmoji if let fitz = fitz { fitzModifier = EmojiFitzModifier(emoji: fitz) @@ -316,7 +346,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let file = file { let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedSize = isEmoji ? dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)) : dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) - self.animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource, fitzModifier: fitzModifier), width: Int(fittedSize.width), height: Int(fittedSize.height), playbackMode: playbackMode, mode: .cached) + let mode: AnimatedStickerMode + if file.resource is LocalFileReferenceMediaResource { + mode = .direct + } else { + mode = .cached + } + self.animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource, fitzModifier: fitzModifier), width: Int(fittedSize.width), height: Int(fittedSize.height), playbackMode: playbackMode, mode: mode) } } } @@ -342,7 +378,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let currentItem = self.item return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in - let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params) + let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData) let incoming = item.message.effectivelyIncoming(item.context.account.peerId) var imageSize: CGSize = CGSize(width: 200.0, height: 200.0) var isEmoji = false @@ -553,7 +589,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { updatedReplyBackgroundNode = ASImageNode() } - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) replyBackgroundImage = graphics.chatFreeformContentAdditionalInfoBackgroundImage } @@ -564,7 +600,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if currentShareButtonNode != nil { updatedShareButtonNode = currentShareButtonNode if item.presentationData.theme !== currentItem?.presentationData.theme { - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) if item.message.id.peerId == item.context.account.peerId { updatedShareButtonBackground = graphics.chatBubbleNavigateButtonImage } else { @@ -574,7 +610,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } else { let buttonNode = HighlightableButtonNode() let buttonIcon: UIImage? - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) if item.message.id.peerId == item.context.account.peerId { buttonIcon = graphics.chatBubbleNavigateButtonImage } else { @@ -589,7 +625,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var maxContentWidth = imageSize.width var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))? if let replyMarkup = replyMarkup { - let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.strings, replyMarkup, item.message, maxContentWidth) + let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maxContentWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout } @@ -863,34 +899,59 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if self.telegramFile != nil { let _ = item.controllerInteraction.openMessage(item.message, .default) } else if let _ = self.emojiFile { - var startTime: Signal - if self.animationNode.playIfNeeded() { - startTime = .single(0.0) + let (emoji, fitz) = item.message.text.basicEmoji + if emoji == "🎲" { + if !self.animationNode.isPlaying { + var pointsValue = Int(arc4random_uniform(6)) + item.controllerInteraction.seenDicePointsValue[item.message.id] = pointsValue + item.controllerInteraction.seenOneTimeAnimatedMedia.remove(item.message.id) + + var emojiFile: TelegramMediaFile? + if let diceEmojis = item.associatedData.animatedEmojiStickers[emoji] { + emojiFile = diceEmojis[pointsValue].file + } + + self.emojiFile = emojiFile + if let emojiFile = emojiFile { + let dimensions = emojiFile.dimensions ?? PixelDimensions(width: 512, height: 512) + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.context.account.postbox, file: emojiFile, small: false, size: dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)), fitzModifier: nil, thumbnail: false)) + self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .standalone(media: emojiFile)).start()) + } + self.isPlaying = false + self.didSetUpAnimationNode = false + self.updateVisibility() + self.animationNode.playIfNeeded() + } } else { - startTime = self.animationNode.status + var startTime: Signal + if self.animationNode.playIfNeeded() { + startTime = .single(0.0) + } else { + startTime = self.animationNode.status |> map { $0.timestamp } |> take(1) |> deliverOnMainQueue - } - - if let text = self.item?.message.text, let firstScalar = text.unicodeScalars.first, firstScalar.value == 0x2764 { - let _ = startTime.start(next: { [weak self] time in - guard let strongSelf = self else { - return - } - - let heartbeatHaptic: ChatMessageHeartbeatHaptic - if let current = strongSelf.heartbeatHaptic { - heartbeatHaptic = current - } else { - heartbeatHaptic = ChatMessageHeartbeatHaptic() - heartbeatHaptic.enabled = true - strongSelf.heartbeatHaptic = heartbeatHaptic - } - if !heartbeatHaptic.active { - heartbeatHaptic.start(time: time) - } - }) + } + + if let text = self.item?.message.text, let firstScalar = text.unicodeScalars.first, firstScalar.value == 0x2764 { + let _ = startTime.start(next: { [weak self] time in + guard let strongSelf = self else { + return + } + + let heartbeatHaptic: ChatMessageHeartbeatHaptic + if let current = strongSelf.heartbeatHaptic { + heartbeatHaptic = current + } else { + heartbeatHaptic = ChatMessageHeartbeatHaptic() + heartbeatHaptic.enabled = true + strongSelf.heartbeatHaptic = heartbeatHaptic + } + if !heartbeatHaptic.active { + heartbeatHaptic.start(time: time) + } + }) + } } } return true @@ -1010,16 +1071,20 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let selectionNode = self.selectionNode { selectionNode.updateSelected(selected, animated: false) - selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) + let selectionFrame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) + selectionNode.frame = selectionFrame + selectionNode.updateLayout(size: selectionFrame.size) self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0); } else { - let selectionNode = ChatMessageSelectionNode(theme: item.presentationData.theme.theme, toggle: { [weak self] value in + let selectionNode = ChatMessageSelectionNode(wallpaper: item.presentationData.theme.wallpaper, theme: item.presentationData.theme.theme, toggle: { [weak self] value in if let strongSelf = self, let item = strongSelf.item { item.controllerInteraction.toggleMessagesSelection([item.message.id], value) } }) - selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) + let selectionFrame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) + selectionNode.frame = selectionFrame + selectionNode.updateLayout(size: selectionFrame.size) self.addSubnode(selectionNode) self.selectionNode = selectionNode selectionNode.updateSelected(selected, animated: false) diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageAttachedContentNode.swift index 7c88fabd6c..bfb409db6b 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageAttachedContentNode.swift @@ -14,13 +14,6 @@ import AccountContext import UrlEscaping import PhotoResources -private let titleFont = Font.semibold(15.0) -private let textFont = Font.regular(15.0) -private let textBoldFont = Font.semibold(15.0) -private let textItalicFont = Font.italic(15.0) -private let textBoldItalicFont = Font.semiboldItalic(15.0) -private let textFixedFont = Font.regular(15.0) -private let textBlockQuoteFont = Font.regular(15.0) private let buttonFont = Font.semibold(13.0) enum ChatMessageAttachedContentActionIcon { @@ -275,7 +268,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { self.addSubnode(self.statusNode) } - func asyncLayout() -> (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ constrainedSize: CGSize) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { + func asyncLayout() -> (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ constrainedSize: CGSize) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { let textAsyncLayout = TextNode.asyncLayout(self.textNode) let currentImage = self.media as? TelegramMediaImage let imageLayout = self.inlineImageNode.asyncLayout() @@ -288,7 +281,17 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { let currentAdditionalImageBadgeNode = self.additionalImageBadgeNode - return { presentationData, automaticDownloadSettings, associatedData, context, controllerInteraction, message, messageRead, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, constrainedSize in + return { presentationData, automaticDownloadSettings, associatedData, attributes, context, controllerInteraction, message, messageRead, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, constrainedSize in + let fontSize: CGFloat = floor(presentationData.fontSize.baseDisplaySize * 15.0 / 17.0) + + let titleFont = Font.semibold(fontSize) + let textFont = Font.regular(fontSize) + let textBoldFont = Font.semibold(fontSize) + let textItalicFont = Font.italic(fontSize) + let textBoldItalicFont = Font.semiboldItalic(fontSize) + let textFixedFont = Font.regular(fontSize) + let textBlockQuoteFont = Font.regular(fontSize) + let incoming = message.effectivelyIncoming(context.account.peerId) var horizontalInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0) @@ -306,6 +309,9 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { var contentMode: InteractiveMediaNodeContentMode = preferMediaAspectFilled ? .aspectFill : .aspectFit var edited = false + if attributes.updatingMedia != nil { + edited = true + } var viewCount: Int? for attribute in message.attributes { if let attribute = attribute as? EditedMessageAttribute { @@ -375,6 +381,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { let updatedSubtitle = NSMutableAttributedString() updatedSubtitle.append(subtitle) updatedSubtitle.addAttribute(.foregroundColor, value: messageTheme.primaryTextColor, range: NSMakeRange(0, subtitle.length)) + updatedSubtitle.addAttribute(.font, value: titleFont, range: NSMakeRange(0, subtitle.length)) string.append(updatedSubtitle) notEmpty = true } @@ -403,12 +410,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { if let (media, flags) = mediaAndFlags { if let file = media as? TelegramMediaFile { if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 { - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, file, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, file, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if file.isInstantVideo { let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) - let (videoLayout, apply) = contentInstantVideoLayout(ChatMessageBubbleContentItem(context: context, controllerInteraction: controllerInteraction, message: message, read: messageRead, presentationData: presentationData, associatedData: associatedData), constrainedSize.width - horizontalInsets.left - horizontalInsets.right, CGSize(width: 212.0, height: 212.0), .bubble, automaticDownload) + let (videoLayout, apply) = contentInstantVideoLayout(ChatMessageBubbleContentItem(context: context, controllerInteraction: controllerInteraction, message: message, read: messageRead, presentationData: presentationData, associatedData: associatedData, attributes: attributes), constrainedSize.width - horizontalInsets.left - horizontalInsets.right, CGSize(width: 212.0, height: 212.0), .bubble, automaticDownload) initialWidth = videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight contentInstantVideoSizeAndApply = (videoLayout, apply) } else if file.isVideo { @@ -434,12 +441,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, file, automaticDownload, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, file, automaticDownload, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if file.isSticker || file.isAnimatedSticker { let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, file, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, file, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else { @@ -451,20 +458,20 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } else { if message.flags.contains(.Failed) { statusType = .BubbleOutgoing(.Failed) - } else if message.flags.isSending && !message.isSentOrAcknowledged { + } else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil { statusType = .BubbleOutgoing(.Sending) } else { statusType = .BubbleOutgoing(.Sent(read: messageRead)) } } - let (_, refineLayout) = contentFileLayout(context, presentationData, message, file, automaticDownload, message.effectivelyIncoming(context.account.peerId), false, associatedData.forcedResourceStatus, statusType, CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)) + let (_, refineLayout) = contentFileLayout(context, presentationData, message, attributes, file, automaticDownload, message.effectivelyIncoming(context.account.peerId), false, associatedData.forcedResourceStatus, statusType, CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)) refineContentFileLayout = refineLayout } } else if let image = media as? TelegramMediaImage { if !flags.contains(.preferMediaInline) { let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, image, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, image, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { @@ -476,14 +483,14 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } else if let image = media as? TelegramMediaWebFile { let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, image, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, image, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if let wallpaper = media as? WallpaperPreviewMedia { - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, wallpaper, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, attributes, wallpaper, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout - if case let .file(_, _, isTheme, _) = wallpaper.content, isTheme { + if case let .file(_, _, _, _, isTheme, _) = wallpaper.content, isTheme { skipStandardStatus = true } } @@ -542,7 +549,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } else { statusType = .BubbleOutgoing(.Failed) } - } else if message.flags.isSending && !message.isSentOrAcknowledged { + } else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil { if imageMode { statusType = .ImageOutgoing(.Sending) } else { @@ -763,7 +770,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { var hasAnimation = true var transition: ContainedViewLayoutTransition = .immediate switch animation { - case .None: + case .None, .Crossfade: hasAnimation = false case let .System(duration): hasAnimation = true @@ -956,13 +963,13 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { return false } - func transitionNode(media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if let contentImageNode = self.contentImageNode, let image = self.media as? TelegramMediaImage, image.isEqual(to: media) { - return (contentImageNode, { [weak contentImageNode] in + return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in return (contentImageNode?.view.snapshotContentTree(unhide: true), nil) }) } else if let contentImageNode = self.contentImageNode, let file = self.media as? TelegramMediaFile, file.isEqual(to: media) { - return (contentImageNode, { [weak contentImageNode] in + return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in return (contentImageNode?.view.snapshotContentTree(unhide: true), nil) }) } @@ -976,7 +983,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { return false } - func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction { + func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.textNode.frame if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { @@ -1012,7 +1019,8 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { TelegramTextAttributes.PeerMention, TelegramTextAttributes.PeerTextMention, TelegramTextAttributes.BotCommand, - TelegramTextAttributes.Hashtag + TelegramTextAttributes.Hashtag, + TelegramTextAttributes.BankCard ] for name in possibleNames { if let _ = attributes[NSAttributedString.Key(rawValue: name)] { @@ -1047,7 +1055,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { return self.contentImageNode?.playMediaWithSound() } - func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? { if !self.statusNode.isHidden { return self.statusNode.reactionNode(value: value) } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageAvatarAccessoryItem.swift b/submodules/TelegramUI/TelegramUI/ChatMessageAvatarAccessoryItem.swift index 0d1c676c29..2bc87950b6 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageAvatarAccessoryItem.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageAvatarAccessoryItem.swift @@ -47,7 +47,7 @@ final class ChatMessageAvatarAccessoryItem: ListViewAccessoryItem { let node = ChatMessageAvatarAccessoryItemNode() node.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0)) if let peer = self.peer { - node.setPeer(account: self.context.account, theme: self.context.sharedContext.currentPresentationData.with({ $0 }).theme, synchronousLoad: synchronous, peer: peer, authorOfMessage: self.messageReference, emptyColor: self.emptyColor) + node.setPeer(context: self.context, theme: self.context.sharedContext.currentPresentationData.with({ $0 }).theme, synchronousLoad: synchronous, peer: peer, authorOfMessage: self.messageReference, emptyColor: self.emptyColor) } return node } @@ -69,11 +69,11 @@ final class ChatMessageAvatarAccessoryItemNode: ListViewAccessoryItemNode { self.addSubnode(self.avatarNode) } - func setPeer(account: Account, theme: PresentationTheme, synchronousLoad:Bool, peer: Peer, authorOfMessage: MessageReference?, emptyColor: UIColor) { + func setPeer(context: AccountContext, theme: PresentationTheme, synchronousLoad:Bool, peer: Peer, authorOfMessage: MessageReference?, emptyColor: UIColor) { var overrideImage: AvatarNodeImageOverride? if peer.isDeleted { overrideImage = .deletedIcon } - self.avatarNode.setPeer(account: account, theme: theme, peer: peer, authorOfMessage: authorOfMessage, overrideImage: overrideImage, emptyColor: emptyColor, synchronousLoad: synchronousLoad) + self.avatarNode.setPeer(context: context, theme: theme, peer: peer, authorOfMessage: authorOfMessage, overrideImage: overrideImage, emptyColor: emptyColor, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: 38.0, height: 38.0)) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageBackground.swift b/submodules/TelegramUI/TelegramUI/ChatMessageBackground.swift index 1694deb2bb..3637729c02 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageBackground.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageBackground.swift @@ -57,36 +57,58 @@ enum ChatMessageBackgroundType: Equatable { } } -class ChatMessageBackground: ASImageNode { +class ChatMessageBackground: ASDisplayNode { private(set) var type: ChatMessageBackgroundType? private var currentHighlighted: Bool? + private var hasWallpaper: Bool? private var graphics: PrincipalThemeEssentialGraphics? private var maskMode: Bool? + private let imageNode: ASImageNode + private let outlineImageNode: ASImageNode + + var hasImage: Bool { + self.imageNode.image != nil + } override init() { + self.imageNode = ASImageNode() + self.imageNode.displaysAsynchronously = false + self.imageNode.displayWithoutProcessing = true + + self.outlineImageNode = ASImageNode() + self.outlineImageNode.displaysAsynchronously = false + self.outlineImageNode.displayWithoutProcessing = true + super.init() self.isUserInteractionEnabled = false - self.displaysAsynchronously = false - self.displayWithoutProcessing = true + self.addSubnode(self.outlineImageNode) + self.addSubnode(self.imageNode) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0)) + transition.updateFrame(node: self.outlineImageNode, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0)) } func setMaskMode(_ maskMode: Bool) { - if let type = self.type, let highlighted = self.currentHighlighted, let graphics = self.graphics { - self.setType(type: type, highlighted: highlighted, graphics: graphics, maskMode: maskMode, transition: .immediate) + if let type = self.type, let hasWallpaper = self.hasWallpaper, let highlighted = self.currentHighlighted, let graphics = self.graphics { + self.setType(type: type, highlighted: highlighted, graphics: graphics, maskMode: maskMode, hasWallpaper: hasWallpaper, transition: .immediate) } } - func setType(type: ChatMessageBackgroundType, highlighted: Bool, graphics: PrincipalThemeEssentialGraphics, maskMode: Bool, transition: ContainedViewLayoutTransition) { + func setType(type: ChatMessageBackgroundType, highlighted: Bool, graphics: PrincipalThemeEssentialGraphics, maskMode: Bool, hasWallpaper: Bool, transition: ContainedViewLayoutTransition) { let previousType = self.type - if let currentType = previousType, currentType == type, self.currentHighlighted == highlighted, self.graphics === graphics, self.maskMode == maskMode { + if let currentType = previousType, currentType == type, self.currentHighlighted == highlighted, self.graphics === graphics, self.maskMode == maskMode, self.hasWallpaper == hasWallpaper { return } self.type = type self.currentHighlighted = highlighted self.graphics = graphics + self.hasWallpaper = hasWallpaper let image: UIImage? + switch type { case .none: image = nil @@ -134,23 +156,146 @@ class ChatMessageBackground: ASImageNode { } } + let outlineImage: UIImage? + + if hasWallpaper { + switch type { + case .none: + outlineImage = nil + case let .incoming(mergeType): + switch mergeType { + case .None: + outlineImage = graphics.chatMessageBackgroundIncomingOutlineImage + case let .Top(side): + if side { + outlineImage = graphics.chatMessageBackgroundIncomingMergedTopSideOutlineImage + } else { + outlineImage = graphics.chatMessageBackgroundIncomingMergedTopOutlineImage + } + case .Bottom: + outlineImage = graphics.chatMessageBackgroundIncomingMergedBottomOutlineImage + case .Both: + outlineImage = graphics.chatMessageBackgroundIncomingMergedBothOutlineImage + case .Side: + outlineImage = graphics.chatMessageBackgroundIncomingMergedSideOutlineImage + } + case let .outgoing(mergeType): + switch mergeType { + case .None: + outlineImage = graphics.chatMessageBackgroundOutgoingOutlineImage + case let .Top(side): + if side { + outlineImage = graphics.chatMessageBackgroundOutgoingMergedTopSideOutlineImage + } else { + outlineImage = graphics.chatMessageBackgroundOutgoingMergedTopOutlineImage + } + case .Bottom: + outlineImage = graphics.chatMessageBackgroundOutgoingMergedBottomOutlineImage + case .Both: + outlineImage = graphics.chatMessageBackgroundOutgoingMergedBothOutlineImage + case .Side: + outlineImage = graphics.chatMessageBackgroundOutgoingMergedSideOutlineImage + } + } + } else { + outlineImage = nil + } + if let previousType = previousType, previousType != .none, type == .none { if transition.isAnimated { let tempLayer = CALayer() - tempLayer.contents = self.layer.contents - tempLayer.contentsScale = self.layer.contentsScale - tempLayer.rasterizationScale = self.layer.rasterizationScale - tempLayer.contentsGravity = self.layer.contentsGravity - tempLayer.contentsCenter = self.layer.contentsCenter + tempLayer.contents = self.imageNode.layer.contents + tempLayer.contentsScale = self.imageNode.layer.contentsScale + tempLayer.rasterizationScale = self.imageNode.layer.rasterizationScale + tempLayer.contentsGravity = self.imageNode.layer.contentsGravity + tempLayer.contentsCenter = self.imageNode.layer.contentsCenter tempLayer.frame = self.bounds - self.layer.addSublayer(tempLayer) + self.layer.insertSublayer(tempLayer, above: self.imageNode.layer) transition.updateAlpha(layer: tempLayer, alpha: 0.0, completion: { [weak tempLayer] _ in tempLayer?.removeFromSuperlayer() }) } + } else if transition.isAnimated { + if let previousContents = self.imageNode.layer.contents, let image = image { + self.imageNode.layer.animate(from: previousContents as AnyObject, to: image.cgImage! as AnyObject, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.42) + } } - self.image = image + self.imageNode.image = image + self.outlineImageNode.image = outlineImage + } +} + +final class ChatMessageShadowNode: ASDisplayNode { + private let contentNode: ASImageNode + private var graphics: PrincipalThemeEssentialGraphics? + + override init() { + self.contentNode = ASImageNode() + self.contentNode.isLayerBacked = true + self.contentNode.displaysAsynchronously = false + self.contentNode.displayWithoutProcessing = true + + super.init() + + self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + + self.isLayerBacked = true + + self.addSubnode(self.contentNode) + } + + func setType(type: ChatMessageBackgroundType, hasWallpaper: Bool, graphics: PrincipalThemeEssentialGraphics) { + let shadowImage: UIImage? + + if hasWallpaper { + switch type { + case .none: + shadowImage = nil + case let .incoming(mergeType): + switch mergeType { + case .None: + shadowImage = graphics.chatMessageBackgroundIncomingShadowImage + case let .Top(side): + if side { + shadowImage = graphics.chatMessageBackgroundIncomingMergedTopSideShadowImage + } else { + shadowImage = graphics.chatMessageBackgroundIncomingMergedTopShadowImage + } + case .Bottom: + shadowImage = graphics.chatMessageBackgroundIncomingMergedBottomShadowImage + case .Both: + shadowImage = graphics.chatMessageBackgroundIncomingMergedBothShadowImage + case .Side: + shadowImage = graphics.chatMessageBackgroundIncomingMergedSideShadowImage + } + case let .outgoing(mergeType): + switch mergeType { + case .None: + shadowImage = graphics.chatMessageBackgroundOutgoingShadowImage + case let .Top(side): + if side { + shadowImage = graphics.chatMessageBackgroundOutgoingMergedTopSideShadowImage + } else { + shadowImage = graphics.chatMessageBackgroundOutgoingMergedTopShadowImage + } + case .Bottom: + shadowImage = graphics.chatMessageBackgroundOutgoingMergedBottomShadowImage + case .Both: + shadowImage = graphics.chatMessageBackgroundOutgoingMergedBothShadowImage + case .Side: + shadowImage = graphics.chatMessageBackgroundOutgoingMergedSideShadowImage + } + } + } else { + shadowImage = nil + } + + self.contentNode.image = shadowImage + } + + func updateLayout(backgroundFrame: CGRect, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX - 10.0, y: backgroundFrame.minY - 10.0), size: CGSize(width: backgroundFrame.width + 20.0, height: backgroundFrame.height + 20.0))) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleBackdrop.swift b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleBackdrop.swift index d5c6f087f7..1f0b4a71ad 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleBackdrop.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleBackdrop.swift @@ -4,6 +4,8 @@ import Display import Postbox import TelegramPresentationData +private let maskInset: CGFloat = 1.0 + final class ChatMessageBubbleBackdrop: ASDisplayNode { private let backgroundContent: ASDisplayNode @@ -14,11 +16,16 @@ final class ChatMessageBubbleBackdrop: ASDisplayNode { private var maskView: UIImageView? + var hasImage: Bool { + return self.backgroundContent.contents != nil + } + override var frame: CGRect { didSet { if let maskView = self.maskView { - if maskView.frame != self.bounds { - maskView.frame = self.bounds + let maskFrame = self.bounds.insetBy(dx: -maskInset, dy: -maskInset) + if maskView.frame != maskFrame { + maskView.frame = maskFrame } } } @@ -84,7 +91,7 @@ final class ChatMessageBubbleBackdrop: ASDisplayNode { } func setType(type: ChatMessageBackgroundType, theme: ChatPresentationThemeData, mediaBox: MediaBox, essentialGraphics: PrincipalThemeEssentialGraphics, maskMode: Bool) { - if self.currentType != type || self.theme != theme || self.currentMaskMode != maskMode { + if self.currentType != type || self.theme != theme || self.currentMaskMode != maskMode || self.essentialGraphics !== essentialGraphics { self.currentType = type self.theme = theme self.essentialGraphics = essentialGraphics @@ -98,7 +105,7 @@ final class ChatMessageBubbleBackdrop: ASDisplayNode { maskView = current } else { maskView = UIImageView() - maskView.frame = self.bounds + maskView.frame = self.bounds.insetBy(dx: -maskInset, dy: -maskInset) self.maskView = maskView self.view.mask = maskView } @@ -140,7 +147,7 @@ final class ChatMessageBubbleBackdrop: ASDisplayNode { func updateFrame(_ value: CGRect, transition: ContainedViewLayoutTransition) { if let maskView = self.maskView { - transition.updateFrame(view: maskView, frame: CGRect(origin: CGPoint(), size: value.size)) + transition.updateFrame(view: maskView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: value.size.width + maskInset * 2.0, height: value.size.height + maskInset * 2.0))) } transition.updateFrame(node: self, frame: value) } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleContentCalclulateImageCorners.swift b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleContentCalclulateImageCorners.swift index f3c7671062..7d3fbc07f6 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleContentCalclulateImageCorners.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleContentCalclulateImageCorners.swift @@ -1,8 +1,9 @@ import Foundation import UIKit import Display +import TelegramPresentationData -func chatMessageBubbleImageContentCorners(relativeContentPosition position: ChatMessageBubbleContentPosition, normalRadius: CGFloat, mergedRadius: CGFloat, mergedWithAnotherContentRadius: CGFloat) -> ImageCorners { +func chatMessageBubbleImageContentCorners(relativeContentPosition position: ChatMessageBubbleContentPosition, normalRadius: CGFloat, mergedRadius: CGFloat, mergedWithAnotherContentRadius: CGFloat, layoutConstants: ChatMessageItemLayoutConstants, chatPresentationData: ChatPresentationData) -> ImageCorners { let topLeftCorner: ImageCorner let topRightCorner: ImageCorner @@ -12,6 +13,9 @@ func chatMessageBubbleImageContentCorners(relativeContentPosition position: Chat case .Neighbour: topLeftCorner = .Corner(mergedWithAnotherContentRadius) topRightCorner = .Corner(mergedWithAnotherContentRadius) + case .BubbleNeighbour: + topLeftCorner = .Corner(mergedRadius) + topRightCorner = .Corner(mergedRadius) case let .None(mergeStatus): switch mergeStatus { case .Left: @@ -31,12 +35,16 @@ func chatMessageBubbleImageContentCorners(relativeContentPosition position: Chat topLeftCorner = .Corner(normalRadius) case .merged: topLeftCorner = .Corner(mergedWithAnotherContentRadius) + case .mergedBubble: + topLeftCorner = .Corner(mergedRadius) } switch position.topRight { case .none: topRightCorner = .Corner(normalRadius) case .merged: topRightCorner = .Corner(mergedWithAnotherContentRadius) + case .mergedBubble: + topRightCorner = .Corner(mergedRadius) } } @@ -49,19 +57,42 @@ func chatMessageBubbleImageContentCorners(relativeContentPosition position: Chat case .Neighbour: bottomLeftCorner = .Corner(mergedWithAnotherContentRadius) bottomRightCorner = .Corner(mergedWithAnotherContentRadius) + case .BubbleNeighbour: + bottomLeftCorner = .Corner(mergedRadius) + bottomRightCorner = .Corner(mergedRadius) case let .None(mergeStatus): switch mergeStatus { case .Left: bottomLeftCorner = .Corner(mergedRadius) bottomRightCorner = .Corner(normalRadius) case let .None(status): + let bubbleInsets: UIEdgeInsets + if case .color = chatPresentationData.theme.wallpaper { + let colors: PresentationThemeBubbleColorComponents + switch status { + case .Incoming: + colors = chatPresentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper + case .Outgoing: + colors = chatPresentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper + case .None: + colors = chatPresentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper + } + if colors.fill == colors.stroke || colors.stroke.alpha.isZero { + bubbleInsets = UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) + } else { + bubbleInsets = layoutConstants.bubble.strokeInsets + } + } else { + bubbleInsets = layoutConstants.image.bubbleInsets + } + switch status { case .Incoming: - bottomLeftCorner = .Tail(normalRadius, true) + bottomLeftCorner = .Tail(normalRadius, PresentationResourcesChat.chatBubbleMediaCorner(chatPresentationData.theme.theme, incoming: true, mainRadius: normalRadius, inset: max(0.0, bubbleInsets.left - 1.0))!) bottomRightCorner = .Corner(normalRadius) case .Outgoing: bottomLeftCorner = .Corner(normalRadius) - bottomRightCorner = .Tail(normalRadius, true) + bottomRightCorner = .Tail(normalRadius, PresentationResourcesChat.chatBubbleMediaCorner(chatPresentationData.theme.theme, incoming: false, mainRadius: normalRadius, inset: max(0.0, bubbleInsets.right - 1.0))!) case .None: bottomLeftCorner = .Corner(normalRadius) bottomRightCorner = .Corner(normalRadius) @@ -75,22 +106,51 @@ func chatMessageBubbleImageContentCorners(relativeContentPosition position: Chat switch position.bottomLeft { case let .none(tail): if tail { - bottomLeftCorner = .Tail(normalRadius, true) + let bubbleInsets: UIEdgeInsets + if case .color = chatPresentationData.theme.wallpaper { + let colors: PresentationThemeBubbleColorComponents + colors = chatPresentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper + if colors.fill == colors.stroke || colors.stroke.alpha.isZero { + bubbleInsets = UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) + } else { + bubbleInsets = layoutConstants.bubble.strokeInsets + } + } else { + bubbleInsets = layoutConstants.image.bubbleInsets + } + + bottomLeftCorner = .Tail(normalRadius, PresentationResourcesChat.chatBubbleMediaCorner(chatPresentationData.theme.theme, incoming: true, mainRadius: normalRadius, inset: max(0.0, bubbleInsets.left - 1.0))!) } else { bottomLeftCorner = .Corner(normalRadius) } case .merged: bottomLeftCorner = .Corner(mergedWithAnotherContentRadius) - } + case .mergedBubble: + bottomLeftCorner = .Corner(mergedRadius) + } switch position.bottomRight { case let .none(tail): if tail { - bottomRightCorner = .Tail(normalRadius, true) + let bubbleInsets: UIEdgeInsets + if case .color = chatPresentationData.theme.wallpaper { + let colors: PresentationThemeBubbleColorComponents + colors = chatPresentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper + if colors.fill == colors.stroke || colors.stroke.alpha.isZero { + bubbleInsets = UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) + } else { + bubbleInsets = layoutConstants.bubble.strokeInsets + } + } else { + bubbleInsets = layoutConstants.image.bubbleInsets + } + bottomRightCorner = .Tail(normalRadius, PresentationResourcesChat.chatBubbleMediaCorner(chatPresentationData.theme.theme, incoming: false, mainRadius: normalRadius, inset: max(0.0, bubbleInsets.right - 1.0))!) } else { bottomRightCorner = .Corner(normalRadius) } case .merged: bottomRightCorner = .Corner(mergedWithAnotherContentRadius) + case .mergedBubble: + bottomRightCorner = .Corner(mergedRadius) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleContentNode.swift index a13e437b08..a06144dee7 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleContentNode.swift @@ -41,11 +41,13 @@ enum ChatMessageBubbleMergeStatus { enum ChatMessageBubbleRelativePosition { case None(ChatMessageBubbleMergeStatus) + case BubbleNeighbour case Neighbour } enum ChatMessageBubbleContentMosaicNeighbor { case merged + case mergedBubble case none(tail: Bool) } @@ -80,6 +82,7 @@ enum ChatMessageBubbleContentTapAction { case openMessage case timecode(Double, String) case tooltip(String, ASDisplayNode?, CGRect?) + case bankCard(String) case ignore } @@ -90,14 +93,16 @@ final class ChatMessageBubbleContentItem { let read: Bool let presentationData: ChatPresentationData let associatedData: ChatMessageItemAssociatedData + let attributes: ChatMessageEntryAttributes - init(context: AccountContext, controllerInteraction: ChatControllerInteraction, message: Message, read: Bool, presentationData: ChatPresentationData, associatedData: ChatMessageItemAssociatedData) { + init(context: AccountContext, controllerInteraction: ChatControllerInteraction, message: Message, read: Bool, presentationData: ChatPresentationData, associatedData: ChatMessageItemAssociatedData, attributes: ChatMessageEntryAttributes) { self.context = context self.controllerInteraction = controllerInteraction self.message = message self.read = read self.presentationData = presentationData self.associatedData = associatedData + self.attributes = attributes } } @@ -138,7 +143,7 @@ class ChatMessageBubbleContentNode: ASDisplayNode { }) } - func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil } @@ -160,7 +165,7 @@ class ChatMessageBubbleContentNode: ASDisplayNode { return nil } - func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction { + func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { return .none } @@ -177,7 +182,7 @@ class ChatMessageBubbleContentNode: ASDisplayNode { func updateIsExtractedToContextPreview(_ value: Bool) { } - func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? { return nil } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift index 7064160bf9..7ff5e59baa 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift @@ -23,66 +23,71 @@ import GridMessageSelectionNode import AppBundle import Markdown -private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [(Message, AnyClass)] { - var result: [(Message, AnyClass)] = [] +private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [(Message, AnyClass, ChatMessageEntryAttributes)] { + var result: [(Message, AnyClass, ChatMessageEntryAttributes)] = [] var skipText = false - var messageWithCaptionToAdd: Message? + var messageWithCaptionToAdd: (Message, ChatMessageEntryAttributes)? var isUnsupportedMedia = false - outer: for message in item.content { + outer: for (message, itemAttributes) in item.content { for attribute in message.attributes { - if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios") != nil { - result.append((message, ChatMessageRestrictedBubbleContentNode.self)) + if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil { + result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes)) break outer } } inner: for media in message.media { if let _ = media as? TelegramMediaImage { - result.append((message, ChatMessageMediaBubbleContentNode.self)) + result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes)) } else if let file = media as? TelegramMediaFile { var isVideo = file.isVideo || (file.isAnimated && file.dimensions != nil) if isVideo { - result.append((message, ChatMessageMediaBubbleContentNode.self)) + result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes)) } else { - result.append((message, ChatMessageFileBubbleContentNode.self)) + result.append((message, ChatMessageFileBubbleContentNode.self, itemAttributes)) } } else if let action = media as? TelegramMediaAction { if case .phoneCall = action.action { - result.append((message, ChatMessageCallBubbleContentNode.self)) + result.append((message, ChatMessageCallBubbleContentNode.self, itemAttributes)) } else { - result.append((message, ChatMessageActionBubbleContentNode.self)) + result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes)) } } else if let _ = media as? TelegramMediaMap { - result.append((message, ChatMessageMapBubbleContentNode.self)) + result.append((message, ChatMessageMapBubbleContentNode.self, itemAttributes)) } else if let _ = media as? TelegramMediaGame { skipText = true - result.append((message, ChatMessageGameBubbleContentNode.self)) + result.append((message, ChatMessageGameBubbleContentNode.self, itemAttributes)) break inner } else if let _ = media as? TelegramMediaInvoice { skipText = true - result.append((message, ChatMessageInvoiceBubbleContentNode.self)) + result.append((message, ChatMessageInvoiceBubbleContentNode.self, itemAttributes)) break inner } else if let _ = media as? TelegramMediaContact { - result.append((message, ChatMessageContactBubbleContentNode.self)) + result.append((message, ChatMessageContactBubbleContentNode.self, itemAttributes)) } else if let _ = media as? TelegramMediaExpiredContent { result.removeAll() - result.append((message, ChatMessageActionBubbleContentNode.self)) + result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes)) return result } else if let _ = media as? TelegramMediaPoll { - result.append((message, ChatMessagePollBubbleContentNode.self)) + result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes)) } else if let _ = media as? TelegramMediaUnsupported { isUnsupportedMedia = true } } - if !message.text.isEmpty || isUnsupportedMedia { + var messageText = message.text + if let updatingMedia = itemAttributes.updatingMedia { + messageText = updatingMedia.text + } + + if !messageText.isEmpty || isUnsupportedMedia { if !skipText { if case .group = item.content { - messageWithCaptionToAdd = message + messageWithCaptionToAdd = (message, itemAttributes) skipText = true } else { - result.append((message, ChatMessageTextBubbleContentNode.self)) + result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes)) } } else { if case .group = item.content { @@ -94,40 +99,35 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [( inner: for media in message.media { if let webpage = media as? TelegramMediaWebpage { if case .Loaded = webpage.content { - result.append((message, ChatMessageWebpageBubbleContentNode.self)) + result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes)) } break inner } } if isUnsupportedMedia { - result.append((message, ChatMessageUnsupportedBubbleContentNode.self)) + result.append((message, ChatMessageUnsupportedBubbleContentNode.self, itemAttributes)) } } - if let messageWithCaptionToAdd = messageWithCaptionToAdd { - result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self)) + if let (messageWithCaptionToAdd, itemAttributes) = messageWithCaptionToAdd { + result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes)) } if let additionalContent = item.additionalContent { switch additionalContent { case let .eventLogPreviousMessage(previousMessage): - result.append((previousMessage, ChatMessageEventLogPreviousMessageContentNode.self)) + result.append((previousMessage, ChatMessageEventLogPreviousMessageContentNode.self, ChatMessageEntryAttributes())) case let .eventLogPreviousDescription(previousMessage): - result.append((previousMessage, ChatMessageEventLogPreviousDescriptionContentNode.self)) + result.append((previousMessage, ChatMessageEventLogPreviousDescriptionContentNode.self, ChatMessageEntryAttributes())) case let .eventLogPreviousLink(previousMessage): - result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self)) + result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self, ChatMessageEntryAttributes())) } } return result } -private let nameFont = Font.medium(14.0) - -private let inlineBotPrefixFont = Font.regular(14.0) -private let inlineBotNameFont = nameFont - private let chatMessagePeerIdColors: [UIColor] = [ UIColor(rgb: 0xfc5c51), UIColor(rgb: 0xfa790f), @@ -147,8 +147,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode private let contextSourceNode: ContextExtractedContentContainingNode private let backgroundWallpaperNode: ChatMessageBubbleBackdrop private let backgroundNode: ChatMessageBackground + private let shadowNode: ChatMessageShadowNode private var transitionClippingNode: ASDisplayNode? + override var extractedBackgroundNode: ASDisplayNode? { + return self.shadowNode + } + private var selectionNode: ChatMessageSelectionNode? private var deliveryFailedNode: ChatMessageDeliveryFailedNode? private var swipeToReplyNode: ChatMessageSwipeToReplyNode? @@ -199,6 +204,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode self.backgroundWallpaperNode = ChatMessageBubbleBackdrop() self.backgroundNode = ChatMessageBackground() + self.shadowNode = ChatMessageShadowNode() self.messageAccessibilityArea = AccessibilityAreaNode() super.init(layerBacked: false) @@ -213,7 +219,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode return false } if let singleUrl = accessibilityData.singleUrl { - strongSelf.item?.controllerInteraction.openUrl(singleUrl, false, false) + strongSelf.item?.controllerInteraction.openUrl(singleUrl, false, false, strongSelf.item?.content.firstMessage) } return false } @@ -234,8 +240,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode guard let strongSelf = self, let item = strongSelf.item else { return } - strongSelf.backgroundWallpaperNode.setMaskMode(isExtractedToContextPreview, mediaBox: item.context.account.postbox.mediaBox) - strongSelf.backgroundNode.setMaskMode(isExtractedToContextPreview) + strongSelf.backgroundWallpaperNode.setMaskMode(strongSelf.backgroundMaskMode, mediaBox: item.context.account.postbox.mediaBox) + strongSelf.backgroundNode.setMaskMode(strongSelf.backgroundMaskMode) if !isExtractedToContextPreview, let (rect, size) = strongSelf.absoluteRect { strongSelf.updateAbsoluteRect(rect, within: size) } @@ -272,18 +278,32 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) + self.shadowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + if let subnodes = self.subnodes { for node in subnodes { if let contextNode = node as? ContextExtractedContentContainingNode { if let contextSubnodes = contextNode.contentNode.subnodes { - for contextSubnode in contextSubnodes { + inner: for contextSubnode in contextSubnodes { if contextSubnode !== self.accessoryItemNode { - contextSubnode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + if contextSubnode == self.backgroundNode { + if self.backgroundNode.hasImage && self.backgroundWallpaperNode.hasImage { + continue inner + } + } + contextSubnode.layer.allowsGroupOpacity = true + contextSubnode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak contextSubnode] _ in + contextSubnode?.layer.allowsGroupOpacity = false + }) } } } } else if node !== self.accessoryItemNode { + node.layer.allowsGroupOpacity = true node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak node] _ in + node?.layer.allowsGroupOpacity = false + }) } } } @@ -296,6 +316,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak self] _ in self?.allowsGroupOpacity = false }) + self.shadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) self.layer.animateScale(from: 1.0, to: 0.1, duration: 0.15, removeOnCompletion: false) self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: self.bounds.width / 2.0 - self.backgroundNode.frame.midX, y: self.backgroundNode.frame.midY), duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) } @@ -339,13 +360,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode return .waitForSingleTap } for contentNode in strongSelf.contentNodes { - let tapAction = contentNode.tapActionAtPoint(CGPoint(x: point.x - contentNode.frame.minX, y: point.y - contentNode.frame.minY), gesture: .tap) + let tapAction = contentNode.tapActionAtPoint(CGPoint(x: point.x - contentNode.frame.minX, y: point.y - contentNode.frame.minY), gesture: .tap, isEstimating: true) switch tapAction { case .none: + if let _ = strongSelf.item?.controllerInteraction.tapMessage { + return .waitForSingleTap + } break case .ignore: return .fail - case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .openMessage, .timecode, .tooltip: + case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .openMessage, .timecode, .bankCard, .tooltip: return .waitForSingleTap } } @@ -380,174 +404,132 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode self.view.addGestureRecognizer(recognizer) self.view.isExclusiveTouch = true - let replyRecognizer = ChatSwipeToReplyRecognizer(target: self, action: #selector(self.swipeToReplyGesture(_:))) - replyRecognizer.shouldBegin = { [weak self] in - if let strongSelf = self, let item = strongSelf.item { + if true { + let replyRecognizer = ChatSwipeToReplyRecognizer(target: self, action: #selector(self.swipeToReplyGesture(_:))) + replyRecognizer.shouldBegin = { [weak self] in + if let strongSelf = self, let item = strongSelf.item { + if strongSelf.selectionNode != nil { + return false + } + for media in item.content.firstMessage.media { + if let _ = media as? TelegramMediaExpiredContent { + return false + } + else if let media = media as? TelegramMediaAction { + if case .phoneCall(_, _, _) = media.action { + + } else { + return false + } + } + } + return item.controllerInteraction.canSetupReply(item.message) + } + return false + } + self.view.addGestureRecognizer(replyRecognizer) + } else { + let reactionRecognizer = ReactionSwipeGestureRecognizer(target: nil, action: nil) + self.reactionRecognizer = reactionRecognizer + reactionRecognizer.availableReactions = { [weak self] in + guard let strongSelf = self, let item = strongSelf.item, !item.presentationData.isPreview && !Namespaces.Message.allScheduled.contains(item.message.id.namespace) else { + return [] + } if strongSelf.selectionNode != nil { - return false + return [] } for media in item.content.firstMessage.media { if let _ = media as? TelegramMediaExpiredContent { - return false + return [] } else if let media = media as? TelegramMediaAction { - if case .phoneCall(_, _, _) = media.action { - + if case .phoneCall = media.action { } else { - return false + return [] } } } + + let reactions: [(String, String, String)] = [ + ("😔", "Sad", "sad"), + ("😳", "Surprised", "surprised"), + ("😂", "Fun", "lol"), + ("ðŸ‘", "Like", "thumbsup"), + ("â¤", "Love", "heart"), + ] + + var reactionItems: [ReactionGestureItem] = [] + for (value, text, name) in reactions.reversed() { + if let path = getAppBundle().path(forResource: name, ofType: "tgs") { + reactionItems.append(.reaction(value: value, text: text, path: path)) + } + } + if item.controllerInteraction.canSetupReply(item.message) { + //reactionItems.append(.reply) + } + return reactionItems + } + reactionRecognizer.getReactionContainer = { [weak self] in + return self?.item?.controllerInteraction.reactionContainerNode() + } + reactionRecognizer.getAnchorPoint = { [weak self] in + guard let strongSelf = self else { + return nil + } + return CGPoint(x: strongSelf.backgroundNode.frame.maxX, y: strongSelf.backgroundNode.frame.minY) + } + reactionRecognizer.shouldElevateAnchorPoint = { [weak self] in + guard let strongSelf = self, let item = strongSelf.item else { + return false + } return item.controllerInteraction.canSetupReply(item.message) } - return false - } - self.view.addGestureRecognizer(replyRecognizer) - - /*let reactionRecognizer = ReactionSwipeGestureRecognizer(target: nil, action: nil) - self.reactionRecognizer = reactionRecognizer - reactionRecognizer.availableReactions = { [weak self] in - guard let strongSelf = self, let item = strongSelf.item, !item.presentationData.isPreview && !Namespaces.Message.allScheduled.contains(item.message.id.namespace) else { - return [] - } - if strongSelf.selectionNode != nil { - return [] - } - for media in item.content.firstMessage.media { - if let _ = media as? TelegramMediaExpiredContent { - return [] + reactionRecognizer.began = { [weak self] in + guard let strongSelf = self, let item = strongSelf.item else { + return } - else if let media = media as? TelegramMediaAction { - if case .phoneCall = media.action { - } else { - return [] - } + item.controllerInteraction.cancelInteractiveKeyboardGestures() + } + reactionRecognizer.updateOffset = { [weak self] offset, animated in + guard let strongSelf = self else { + return + } + var bounds = strongSelf.bounds + bounds.origin.x = offset + strongSelf.bounds = bounds + + var shadowBounds = strongSelf.shadowNode.bounds + shadowBounds.origin.x = offset + strongSelf.shadowNode.bounds = shadowBounds + + if animated { + strongSelf.layer.animateBoundsOriginXAdditive(from: -offset, to: 0.0, duration: 0.1, mediaTimingFunction: CAMediaTimingFunction(name: .easeOut)) + strongSelf.shadowNode.layer.animateBoundsOriginXAdditive(from: -offset, to: 0.0, duration: 0.1, mediaTimingFunction: CAMediaTimingFunction(name: .easeOut)) + } + if let swipeToReplyNode = strongSelf.swipeToReplyNode { + swipeToReplyNode.alpha = max(0.0, min(1.0, abs(offset / 40.0))) } } - - /*let reactions: [(String, String, String)] = [ - ("😔", "Sad", "sad"), - ("😳", "Surprised", "surprised"), - ("😂", "Fun", "lol"), - ("ðŸ‘", "Like", "thumbsup"), - ("â¤", "Love", "heart"), - ] - - var reactionItems: [ReactionGestureItem] = [] - for (value, text, name) in reactions.reversed() { - if let path = getAppBundle().path(forResource: name, ofType: "tgs") { - reactionItems.append(.reaction(value: value, text: text, path: path)) + reactionRecognizer.activateReply = { [weak self] in + guard let strongSelf = self, let item = strongSelf.item else { + return } - } - if item.controllerInteraction.canSetupReply(item.message) { - //reactionItems.append(.reply) - } - return reactionItems*/ - - return [] - } - reactionRecognizer.getReactionContainer = { [weak self] in - return self?.item?.controllerInteraction.reactionContainerNode() - } - reactionRecognizer.getAnchorPoint = { [weak self] in - guard let strongSelf = self else { - return nil - } - return CGPoint(x: strongSelf.backgroundNode.frame.maxX, y: strongSelf.backgroundNode.frame.minY) - } - reactionRecognizer.began = { [weak self] in - guard let strongSelf = self, let item = strongSelf.item else { - return - } - item.controllerInteraction.cancelInteractiveKeyboardGestures() - } - reactionRecognizer.updateOffset = { [weak self] offset, animated in - guard let strongSelf = self else { - return - } - var bounds = strongSelf.bounds - bounds.origin.x = offset - strongSelf.bounds = bounds - if animated { - strongSelf.layer.animateBoundsOriginXAdditive(from: -offset, to: 0.0, duration: 0.1, mediaTimingFunction: CAMediaTimingFunction(name: .easeOut)) - } - if let swipeToReplyNode = strongSelf.swipeToReplyNode { - swipeToReplyNode.alpha = max(0.0, min(1.0, abs(offset / 40.0))) - } - } - reactionRecognizer.activateReply = { [weak self] in - guard let strongSelf = self, let item = strongSelf.item else { - return - } - var bounds = strongSelf.bounds - let offset = bounds.origin.x - bounds.origin.x = 0.0 - strongSelf.bounds = bounds - if !offset.isZero { - strongSelf.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) - } - if let swipeToReplyNode = strongSelf.swipeToReplyNode { - strongSelf.swipeToReplyNode = nil - swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in - swipeToReplyNode?.removeFromSupernode() - }) - swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - } - item.controllerInteraction.setupReply(item.message.id) - } - reactionRecognizer.displayReply = { [weak self] offset in - guard let strongSelf = self, let item = strongSelf.item else { - return - } - if strongSelf.swipeToReplyFeedback == nil { - strongSelf.swipeToReplyFeedback = HapticFeedback() - } - strongSelf.swipeToReplyFeedback?.tap() - if strongSelf.swipeToReplyNode == nil { - let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonFillColor, wallpaper: item.presentationData.theme.wallpaper), strokeColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonStrokeColor, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper)) - strongSelf.swipeToReplyNode = swipeToReplyNode - strongSelf.insertSubnode(swipeToReplyNode, belowSubnode: strongSelf.messageAccessibilityArea) - swipeToReplyNode.frame = CGRect(origin: CGPoint(x: strongSelf.bounds.size.width, y: floor((strongSelf.contentSize.height - 33.0) / 2.0)), size: CGSize(width: 33.0, height: 33.0)) - swipeToReplyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) - swipeToReplyNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) - } - } - reactionRecognizer.completed = { [weak self] reaction in - guard let strongSelf = self else { - return - } - if let item = strongSelf.item, let reaction = reaction { - switch reaction { - case let .reaction(value, _, _): - strongSelf.awaitingAppliedReaction = (value, {}) - item.controllerInteraction.updateMessageReaction(item.message.id, value) - case .reply: - strongSelf.reactionRecognizer?.complete(into: nil, hideTarget: false) - var bounds = strongSelf.bounds - let offset = bounds.origin.x - bounds.origin.x = 0.0 - strongSelf.bounds = bounds - if !offset.isZero { - strongSelf.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) - } - if let swipeToReplyNode = strongSelf.swipeToReplyNode { - strongSelf.swipeToReplyNode = nil - swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in - swipeToReplyNode?.removeFromSupernode() - }) - swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - } - item.controllerInteraction.setupReply(item.message.id) - } - } else { - strongSelf.reactionRecognizer?.complete(into: nil, hideTarget: false) var bounds = strongSelf.bounds let offset = bounds.origin.x bounds.origin.x = 0.0 strongSelf.bounds = bounds + + var shadowBounds = strongSelf.shadowNode.bounds + let shadowOffset = shadowBounds.origin.x + shadowBounds.origin.x = 0.0 + strongSelf.shadowNode.bounds = shadowBounds + if !offset.isZero { strongSelf.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) } + if !shadowOffset.isZero { + strongSelf.shadowNode.layer.animateBoundsOriginXAdditive(from: shadowOffset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) + } if let swipeToReplyNode = strongSelf.swipeToReplyNode { strongSelf.swipeToReplyNode = nil swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in @@ -555,9 +537,95 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode }) swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) } + item.controllerInteraction.setupReply(item.message.id) } + reactionRecognizer.displayReply = { [weak self] offset in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + if !item.controllerInteraction.canSetupReply(item.message) { + return + } + if strongSelf.swipeToReplyFeedback == nil { + strongSelf.swipeToReplyFeedback = HapticFeedback() + } + strongSelf.swipeToReplyFeedback?.tap() + if strongSelf.swipeToReplyNode == nil { + let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonFillColor, wallpaper: item.presentationData.theme.wallpaper), strokeColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonStrokeColor, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper)) + strongSelf.swipeToReplyNode = swipeToReplyNode + strongSelf.insertSubnode(swipeToReplyNode, belowSubnode: strongSelf.messageAccessibilityArea) + swipeToReplyNode.frame = CGRect(origin: CGPoint(x: strongSelf.bounds.size.width, y: floor((strongSelf.contentSize.height - 33.0) / 2.0)), size: CGSize(width: 33.0, height: 33.0)) + swipeToReplyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) + swipeToReplyNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) + } + } + reactionRecognizer.completed = { [weak self] reaction in + guard let strongSelf = self else { + return + } + if let item = strongSelf.item, let reaction = reaction { + switch reaction { + case let .reaction(value, _, _): + var resolvedValue: String? + if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), reactionsAttribute.reactions.contains(where: { $0.value == value }) { + resolvedValue = nil + } else { + resolvedValue = value + } + strongSelf.awaitingAppliedReaction = (resolvedValue, {}) + item.controllerInteraction.updateMessageReaction(item.message.id, resolvedValue) + case .reply: + strongSelf.reactionRecognizer?.complete(into: nil, hideTarget: false) + var bounds = strongSelf.bounds + let offset = bounds.origin.x + bounds.origin.x = 0.0 + strongSelf.bounds = bounds + var shadowBounds = strongSelf.shadowNode.bounds + let shadowOffset = shadowBounds.origin.x + shadowBounds.origin.x = 0.0 + strongSelf.shadowNode.bounds = shadowBounds + if !offset.isZero { + strongSelf.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) + } + if !shadowOffset.isZero { + strongSelf.shadowNode.layer.animateBoundsOriginXAdditive(from: shadowOffset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) + } + if let swipeToReplyNode = strongSelf.swipeToReplyNode { + strongSelf.swipeToReplyNode = nil + swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in + swipeToReplyNode?.removeFromSupernode() + }) + swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } + item.controllerInteraction.setupReply(item.message.id) + } + } else { + strongSelf.reactionRecognizer?.complete(into: nil, hideTarget: false) + var bounds = strongSelf.bounds + let offset = bounds.origin.x + bounds.origin.x = 0.0 + strongSelf.bounds = bounds + var shadowBounds = strongSelf.shadowNode.bounds + let shadowOffset = shadowBounds.origin.x + shadowBounds.origin.x = 0.0 + strongSelf.shadowNode.bounds = shadowBounds + if !offset.isZero { + strongSelf.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) + } + if !shadowOffset.isZero { + strongSelf.shadowNode.layer.animateBoundsOriginXAdditive(from: shadowOffset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) + } + if let swipeToReplyNode = strongSelf.swipeToReplyNode { + strongSelf.swipeToReplyNode = nil + swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in + swipeToReplyNode?.removeFromSupernode() + }) + swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } + } + } + self.view.addGestureRecognizer(reactionRecognizer) } - self.view.addGestureRecognizer(reactionRecognizer)*/ } override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, Bool) -> Void) { @@ -590,7 +658,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode let weakSelf = Weak(self) return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in - let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params) + let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData) return ChatMessageBubbleItemNode.beginLayout(selfReference: weakSelf, item, params, mergedTop, mergedBottom, dateHeaderAtBottom, currentContentClassesPropertiesAndLayouts: currentContentClassesPropertiesAndLayouts, authorNameLayout: authorNameLayout, @@ -614,7 +682,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, CGSize) -> (CGSize, () -> ChatMessageForwardInfoNode), replyInfoLayout: (ChatPresentationData, PresentationStrings, AccountContext, ChatMessageReplyInfoType, Message, CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode), - actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (Bool) -> ChatMessageActionButtonsNode)), + actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (Bool) -> ChatMessageActionButtonsNode)), mosaicStatusLayout: (AccountContext, ChatPresentationData, Bool, Int?, String, ChatMessageDateAndStatusType, CGSize, [MessageReaction]) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode), currentShareButtonNode: HighlightableButtonNode?, layoutConstants: ChatMessageItemLayoutConstants, @@ -624,6 +692,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode ) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, Bool) -> Void) { let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: isSelected) + let fontSize = floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0) + let nameFont = Font.medium(fontSize) + + let inlineBotPrefixFont = Font.regular(fontSize) + let inlineBotNameFont = nameFont + let baseWidth = params.width - params.leftInset - params.rightInset let content = item.content @@ -787,22 +861,22 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode let maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset) - var contentPropertiesAndPrepareLayouts: [(Message, Bool, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))))] = [] + var contentPropertiesAndPrepareLayouts: [(Message, Bool, ChatMessageEntryAttributes, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))))] = [] var addedContentNodes: [(Message, ChatMessageBubbleContentNode)]? let contentNodeMessagesAndClasses = contentNodeMessagesAndClassesForItem(item) - for (contentNodeMessage, contentNodeClass) in contentNodeMessagesAndClasses { + for (contentNodeMessage, contentNodeClass, attributes) in contentNodeMessagesAndClasses { var found = false for (currentMessage, currentClass, supportsMosaic, currentLayout) in currentContentClassesPropertiesAndLayouts { if currentClass == contentNodeClass && currentMessage.stableId == contentNodeMessage.stableId { - contentPropertiesAndPrepareLayouts.append((contentNodeMessage, supportsMosaic, currentLayout)) + contentPropertiesAndPrepareLayouts.append((contentNodeMessage, supportsMosaic, attributes, currentLayout)) found = true break } } if !found { let contentNode = (contentNodeClass as! ChatMessageBubbleContentNode.Type).init() - contentPropertiesAndPrepareLayouts.append((contentNodeMessage, contentNode.supportsMosaic, contentNode.asyncLayoutContent())) + contentPropertiesAndPrepareLayouts.append((contentNodeMessage, contentNode.supportsMosaic, attributes, contentNode.asyncLayoutContent())) if addedContentNodes == nil { addedContentNodes = [] } @@ -858,8 +932,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode var backgroundHiding: ChatMessageBubbleContentBackgroundHiding? var hasSolidWallpaper = false - if case .color = item.presentationData.theme.wallpaper { + switch item.presentationData.theme.wallpaper { + case .color, .gradient: hasSolidWallpaper = true + default: + break } var alignment: ChatMessageBubbleContentAlignment = .none @@ -896,7 +973,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode } var index = 0 - for (message, _, prepareLayout) in contentPropertiesAndPrepareLayouts { + for (message, _, attributes, prepareLayout) in contentPropertiesAndPrepareLayouts { let topPosition: ChatMessageBubbleRelativePosition let bottomPosition: ChatMessageBubbleRelativePosition @@ -916,7 +993,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode prepareContentPosition = .linear(top: topPosition, bottom: refinedBottomPosition) } - let contentItem = ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: message, read: read, presentationData: item.presentationData, associatedData: item.associatedData) + let contentItem = ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: message, read: read, presentationData: item.presentationData, associatedData: item.associatedData, attributes: attributes) var itemSelection: Bool? if case .mosaic = prepareContentPosition { @@ -1052,6 +1129,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode let message = item.content.firstMessage var edited = false + if item.content.firstMessageAttributes.updatingMedia != nil { + edited = true + } var viewCount: Int? for attribute in message.attributes { if let attribute = attribute as? EditedMessageAttribute { @@ -1082,7 +1162,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode } else { if isFailed { statusType = .ImageOutgoing(.Failed) - } else if message.flags.isSending && !message.isSentOrAcknowledged { + } else if (message.flags.isSending && !message.isSentOrAcknowledged) || item.content.firstMessageAttributes.updatingMedia != nil { statusType = .ImageOutgoing(.Sending) } else { statusType = .ImageOutgoing(.Sent(read: item.read)) @@ -1236,7 +1316,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode findRemoved: for i in 0 ..< currentContentClassesPropertiesAndLayouts.count { let currentMessage = currentContentClassesPropertiesAndLayouts[i].0 let currentClass: AnyClass = currentContentClassesPropertiesAndLayouts[i].1 - for (contentNodeMessage, contentNodeClass) in contentNodeMessagesAndClasses { + for (contentNodeMessage, contentNodeClass, _) in contentNodeMessagesAndClasses { if currentClass == contentNodeClass && currentMessage.stableId == contentNodeMessage.stableId { continue findRemoved } @@ -1254,7 +1334,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))? if let replyMarkup = replyMarkup { - let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.strings, replyMarkup, item.message, maximumNodeWidth) + let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maximumNodeWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout } @@ -1276,11 +1356,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode case .Neighbour: topLeft = .merged topRight = .merged + case .BubbleNeighbour: + topLeft = .mergedBubble + topRight = .mergedBubble case let .None(status): if position.contains(.top) && position.contains(.left) { switch status { case .Left: - topLeft = .merged + topLeft = .mergedBubble case .Right: topLeft = .none(tail: false) case .None: @@ -1295,7 +1378,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode case .Left: topRight = .none(tail: false) case .Right: - topRight = .merged + topRight = .mergedBubble case .None: topRight = .none(tail: false) } @@ -1319,11 +1402,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode case .Neighbour: bottomLeft = .merged bottomRight = .merged + case .BubbleNeighbour: + bottomLeft = .mergedBubble + bottomRight = .mergedBubble case let .None(status): if position.contains(.bottom) && position.contains(.left) { switch status { case .Left: - bottomLeft = .merged + bottomLeft = .mergedBubble case .Right: bottomLeft = .none(tail: false) case let .None(tailStatus): @@ -1342,7 +1428,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode case .Left: bottomRight = .none(tail: false) case .Right: - bottomRight = .merged + bottomRight = .mergedBubble case let .None(tailStatus): if case .Outgoing = tailStatus { bottomRight = .none(tail: true) @@ -1486,7 +1572,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode if currentShareButtonNode != nil { updatedShareButtonNode = currentShareButtonNode if item.presentationData.theme !== currentItem?.presentationData.theme { - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) if item.message.id.peerId == item.context.account.peerId { updatedShareButtonBackground = graphics.chatBubbleNavigateButtonImage } else { @@ -1496,7 +1582,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode } else { let buttonNode = HighlightableButtonNode() let buttonIcon: UIImage? - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) if item.message.id.peerId == item.context.account.peerId { buttonIcon = graphics.chatBubbleNavigateButtonImage } else { @@ -1509,7 +1595,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode let layout = ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets) - let graphics = PresentationResourcesChat.principalGraphics(mediaBox: item.context.account.postbox.mediaBox, knockoutWallpaper: item.context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, gradientBubbles: item.context.sharedContext.immediateExperimentalUISettings.gradientBubbles) + let graphics = PresentationResourcesChat.principalGraphics(mediaBox: item.context.account.postbox.mediaBox, knockoutWallpaper: item.context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) var updatedMergedTop = mergedBottom var updatedMergedBottom = mergedTop @@ -1592,7 +1678,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode replyInfoOriginY: CGFloat, removedContentNodeIndices: [Int]?, addedContentNodes: [(Message, ChatMessageBubbleContentNode)]?, - contentNodeMessagesAndClasses: [(Message, AnyClass)], + contentNodeMessagesAndClasses: [(Message, AnyClass, ChatMessageEntryAttributes)], contentNodeFramesPropertiesAndApply: [(CGRect, ChatMessageBubbleContentProperties, (ListViewItemUpdateAnimation, Bool) -> Void)], mosaicStatusOrigin: CGPoint?, mosaicStatusSizeAndApply: (CGSize, (Bool) -> ChatMessageDateAndStatusNode)?, @@ -1630,8 +1716,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode } else { backgroundType = .incoming(mergeType) } - strongSelf.backgroundNode.setType(type: backgroundType, highlighted: strongSelf.highlightedState, graphics: graphics, maskMode: strongSelf.contextSourceNode.isExtractedToContextPreview, transition: transition) - strongSelf.backgroundWallpaperNode.setType(type: backgroundType, theme: item.presentationData.theme, mediaBox: item.context.account.postbox.mediaBox, essentialGraphics: graphics, maskMode: strongSelf.contextSourceNode.isExtractedToContextPreview) + let hasWallpaper = item.presentationData.theme.wallpaper.hasWallpaper + strongSelf.backgroundNode.setType(type: backgroundType, highlighted: strongSelf.highlightedState, graphics: graphics, maskMode: strongSelf.backgroundMaskMode, hasWallpaper: hasWallpaper, transition: transition) + strongSelf.backgroundWallpaperNode.setType(type: backgroundType, theme: item.presentationData.theme, mediaBox: item.context.account.postbox.mediaBox, essentialGraphics: graphics, maskMode: strongSelf.backgroundMaskMode) + strongSelf.shadowNode.setType(type: backgroundType, hasWallpaper: hasWallpaper, graphics: graphics) strongSelf.backgroundType = backgroundType @@ -1789,7 +1877,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode } var sortedContentNodes: [ChatMessageBubbleContentNode] = [] - outer: for (message, nodeClass) in contentNodeMessagesAndClasses { + outer: for (message, nodeClass, _) in contentNodeMessagesAndClasses { if let addedContentNodes = addedContentNodes { for (contentNodeMessage, contentNode) in addedContentNodes { if type(of: contentNode) == nodeClass && contentNodeMessage.stableId == message.stableId { @@ -1902,18 +1990,23 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode if case .System = animation, strongSelf.contextSourceNode.isExtractedToContextPreview { transition.updateFrame(node: strongSelf.backgroundNode, frame: backgroundFrame) - //let backgroundWallpaperDelta = CGPoint(x: backgroundFrame.minX - strongSelf.backgroundWallpaperNode.frame.minX, y: backgroundFrame.minY - strongSelf.backgroundWallpaperNode.frame.minY) + strongSelf.backgroundNode.updateLayout(size: backgroundFrame.size, transition: transition) strongSelf.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: transition) + strongSelf.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: transition) } else { strongSelf.backgroundNode.frame = backgroundFrame - strongSelf.backgroundWallpaperNode.frame = backgroundFrame//.insetBy(dx: 1.0, dy: 1.0) + strongSelf.backgroundNode.updateLayout(size: backgroundFrame.size, transition: .immediate) + strongSelf.backgroundWallpaperNode.frame = backgroundFrame + strongSelf.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: .immediate) } if let (rect, size) = strongSelf.absoluteRect { strongSelf.updateAbsoluteRect(rect, within: size) } } let offset: CGFloat = params.leftInset + (incoming ? 42.0 : 0.0) - strongSelf.selectionNode?.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: params.width, height: layout.size.height)) + let selectionFrame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: params.width, height: layout.contentSize.height)) + strongSelf.selectionNode?.frame = selectionFrame + strongSelf.selectionNode?.updateLayout(size: selectionFrame.size) if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { var animated = false @@ -1971,9 +2064,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode let offset = bounds.origin.x bounds.origin.x = 0.0 strongSelf.bounds = bounds + var shadowBounds = strongSelf.shadowNode.bounds + let shadowOffset = shadowBounds.origin.x + shadowBounds.origin.x = 0.0 + strongSelf.shadowNode.bounds = shadowBounds if !offset.isZero { strongSelf.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) } + if !shadowOffset.isZero { + strongSelf.shadowNode.layer.animateBoundsOriginXAdditive(from: shadowOffset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) + } if let swipeToReplyNode = strongSelf.swipeToReplyNode { strongSelf.swipeToReplyNode = nil swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in @@ -1983,13 +2083,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode } strongSelf.awaitingAppliedReaction = nil - var targetNode: ASImageNode? + var targetNode: ASDisplayNode? var hideTarget = false - for contentNode in strongSelf.contentNodes { - if let (reactionNode, count) = contentNode.reactionTargetNode(value: awaitingAppliedReaction) { - targetNode = reactionNode - hideTarget = count == 1 - break + if let awaitingAppliedReaction = awaitingAppliedReaction { + for contentNode in strongSelf.contentNodes { + if let (reactionNode, count) = contentNode.reactionTargetNode(value: awaitingAppliedReaction) { + targetNode = reactionNode + hideTarget = count == 1 + break + } } } strongSelf.reactionRecognizer?.complete(into: targetNode, hideTarget: hideTarget) @@ -2097,7 +2199,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode if let backgroundFrameTransition = self.backgroundFrameTransition { let backgroundFrame = CGRect.interpolator()(backgroundFrameTransition.0, backgroundFrameTransition.1, progress) as! CGRect self.backgroundNode.frame = backgroundFrame - self.backgroundWallpaperNode.frame = backgroundFrame//.insetBy(dx: 1.0, dy: 1.0) + self.backgroundNode.updateLayout(size: backgroundFrame.size, transition: .immediate) + self.backgroundWallpaperNode.frame = backgroundFrame + self.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: .immediate) if let type = self.backgroundNode.type { var incomingOffset: CGFloat = 0.0 @@ -2207,7 +2311,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode item.controllerInteraction.displayMessageTooltip(item.content.firstMessage.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, self, avatarNode.frame) } else { if item.message.id.peerId == item.context.account.peerId, let channel = item.content.firstMessage.forwardInfo?.author as? TelegramChannel, channel.username == nil { - if case .member = channel.participationStatus { + if case let .broadcast(info) = channel.info, info.flags.contains(.hasDiscussionGroup) { + } else if case .member = channel.participationStatus { } else { item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_PrivateChannelTooltip, self, avatarNode.frame) return true @@ -2242,7 +2347,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode } } } - } else if let replyInfoNode = self.replyInfoNode, replyInfoNode.frame.contains(location) { + } else if let replyInfoNode = self.replyInfoNode, self.item?.controllerInteraction.tapMessage == nil, replyInfoNode.frame.contains(location) { if let item = self.item { for attribute in item.message.attributes { if let attribute = attribute as? ReplyMessageAttribute { @@ -2256,7 +2361,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode if let item = self.item, let forwardInfo = item.message.forwardInfo { if let sourceMessageId = forwardInfo.sourceMessageId { if let channel = forwardInfo.author as? TelegramChannel, channel.username == nil { - if case .member = channel.participationStatus { + if case let .broadcast(info) = channel.info, info.flags.contains(.hasDiscussionGroup) { + } else if case .member = channel.participationStatus { } else { item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_PrivateChannelTooltip, forwardInfoNode, nil) return true @@ -2273,13 +2379,17 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode } var foundTapAction = false loop: for contentNode in self.contentNodes { - let tapAction = contentNode.tapActionAtPoint(CGPoint(x: location.x - contentNode.frame.minX, y: location.y - contentNode.frame.minY), gesture: gesture) + let tapAction = contentNode.tapActionAtPoint(CGPoint(x: location.x - contentNode.frame.minX, y: location.y - contentNode.frame.minY), gesture: gesture, isEstimating: false) switch tapAction { case .none, .ignore: + if let item = self.item, self.backgroundNode.frame.contains(CGPoint(x: self.frame.width - location.x, y: location.y)), let tapMessage = self.item?.controllerInteraction.tapMessage { + foundTapAction = true + tapMessage(item.message) + } break case let .url(url, concealed): foundTapAction = true - self.item?.controllerInteraction.openUrl(url, concealed, nil) + self.item?.controllerInteraction.openUrl(url, concealed, nil, self.item?.content.firstMessage) break loop case let .peerMention(peerId, _): foundTapAction = true @@ -2333,6 +2443,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode item.controllerInteraction.seekToTimecode(mediaMessage, timecode, forceOpen) } break loop + case let .bankCard(number): + foundTapAction = true + if let item = self.item { + item.controllerInteraction.longTap(.bankCard(number), item.message) + } case let .tooltip(text, node, rect): foundTapAction = true if let item = self.item { @@ -2358,7 +2473,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode selectAll = false } tapMessage = contentNode.item?.message - let tapAction = contentNode.tapActionAtPoint(CGPoint(x: location.x - contentNode.frame.minX, y: location.y - contentNode.frame.minY), gesture: gesture) + let tapAction = contentNode.tapActionAtPoint(CGPoint(x: location.x - contentNode.frame.minX, y: location.y - contentNode.frame.minY), gesture: gesture, isEstimating: false) switch tapAction { case .none, .ignore: break @@ -2399,6 +2514,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode item.controllerInteraction.longTap(.timecode(timecode, text), mediaMessage) } break loop + case let .bankCard(number): + foundTapAction = true + item.controllerInteraction.longTap(.bankCard(number), message) case .tooltip: break } @@ -2482,23 +2600,38 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode return super.hitTest(point, with: event) } - override func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + override func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { for contentNode in self.contentNodes { if let result = contentNode.transitionNode(messageId: id, media: media) { if self.contentNodes.count == 1 && self.contentNodes.first is ChatMessageMediaBubbleContentNode && self.nameNode == nil && self.adminBadgeNode == nil && self.forwardInfoNode == nil && self.replyInfoNode == nil { - return (result.0, { [weak self] in - guard let strongSelf = self, let resultView = result.1().0 else { + return (result.0, result.1, { [weak self] in + guard let strongSelf = self, let resultView = result.2().0 else { return (nil, nil) } if strongSelf.backgroundNode.supernode != nil, let backgroundView = strongSelf.backgroundNode.view.snapshotContentTree(unhide: true) { + let backgroundContainer = UIView() + + let backdropView = strongSelf.backgroundWallpaperNode.view.snapshotContentTree(unhide: true) + if let backdropView = backdropView { + let backdropFrame = strongSelf.backgroundWallpaperNode.layer.convert(strongSelf.backgroundWallpaperNode.bounds, to: strongSelf.backgroundNode.layer) + backdropView.frame = backdropFrame + } + + if let backdropView = backdropView { + backgroundContainer.addSubview(backdropView) + } + + backgroundContainer.addSubview(backgroundView) + let backgroundFrame = strongSelf.backgroundNode.layer.convert(strongSelf.backgroundNode.bounds, to: result.0.layer) - backgroundView.frame = backgroundFrame + backgroundView.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size) + backgroundContainer.frame = backgroundFrame let viewWithBackground = UIView() - viewWithBackground.addSubview(backgroundView) + viewWithBackground.addSubview(backgroundContainer) viewWithBackground.frame = resultView.frame resultView.frame = CGRect(origin: CGPoint(), size: resultView.frame.size) viewWithBackground.addSubview(resultView) - return (viewWithBackground, backgroundView) + return (viewWithBackground, backgroundContainer) } return (resultView, nil) }) @@ -2615,10 +2748,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode if let selectionNode = self.selectionNode { selectionNode.updateSelected(selected, animated: animated) - selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) + let selectionFrame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentSize.width, height: self.contentSize.height)) + selectionNode.frame = selectionFrame + selectionNode.updateLayout(size: selectionFrame.size) self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0); } else { - let selectionNode = ChatMessageSelectionNode(theme: item.presentationData.theme.theme, toggle: { [weak self] value in + let selectionNode = ChatMessageSelectionNode(wallpaper: item.presentationData.theme.wallpaper, theme: item.presentationData.theme.theme, toggle: { [weak self] value in if let strongSelf = self, let item = strongSelf.item { switch item.content { case let .message(message, _, _, _): @@ -2629,7 +2764,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode } }) - selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) + let selectionFrame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentSize.width, height: self.contentSize.height)) + selectionNode.frame = selectionFrame + selectionNode.updateLayout(size: selectionFrame.size) self.insertSubnode(selectionNode, belowSubnode: self.messageAccessibilityArea) self.selectionNode = selectionNode selectionNode.updateSelected(selected, animated: false) @@ -2691,7 +2828,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode } if let highlightedState = item.controllerInteraction.highlightedState { - for message in item.content { + for (message, _) in item.content { if highlightedState.messageStableId == message.stableId { highlighted = true break @@ -2702,21 +2839,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode if self.highlightedState != highlighted { self.highlightedState = highlighted if let backgroundType = self.backgroundType { - let graphics = PresentationResourcesChat.principalGraphics(mediaBox: item.context.account.postbox.mediaBox, knockoutWallpaper: item.context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, gradientBubbles: item.context.sharedContext.immediateExperimentalUISettings.gradientBubbles) + let graphics = PresentationResourcesChat.principalGraphics(mediaBox: item.context.account.postbox.mediaBox, knockoutWallpaper: item.context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) - if highlighted { - self.backgroundNode.setType(type: backgroundType, highlighted: true, graphics: graphics, maskMode: self.contextSourceNode.isExtractedToContextPreview, transition: .immediate) - } else { - if let previousContents = self.backgroundNode.layer.contents, animated { - self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics, maskMode: self.contextSourceNode.isExtractedToContextPreview, transition: .immediate) - - if let updatedContents = self.backgroundNode.layer.contents { - self.backgroundNode.layer.animate(from: previousContents as AnyObject, to: updatedContents as AnyObject, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.42) - } - } else { - self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics, maskMode: self.contextSourceNode.isExtractedToContextPreview, transition: .immediate) - } - } + let hasWallpaper = item.presentationData.theme.wallpaper.hasWallpaper + self.backgroundNode.setType(type: backgroundType, highlighted: highlighted, graphics: graphics, maskMode: self.contextSourceNode.isExtractedToContextPreview, hasWallpaper: hasWallpaper, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) } } } @@ -2763,6 +2889,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode var bounds = self.bounds bounds.origin.x = -translation.x self.bounds = bounds + var shadowBounds = self.shadowNode.bounds + shadowBounds.origin.x = -translation.x + self.shadowNode.bounds = shadowBounds if let swipeToReplyNode = self.swipeToReplyNode { swipeToReplyNode.frame = CGRect(origin: CGPoint(x: bounds.size.width, y: floor((self.contentSize.height - 33.0) / 2.0)), size: CGSize(width: 33.0, height: 33.0)) @@ -2786,7 +2915,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode let previousBounds = bounds bounds.origin.x = 0.0 self.bounds = bounds + var shadowBounds = self.shadowNode.bounds + let previousShadowBounds = shadowBounds + shadowBounds.origin.x = 0.0 + self.shadowNode.bounds = shadowBounds self.layer.animateBounds(from: previousBounds, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.shadowNode.layer.animateBounds(from: previousShadowBounds, to: shadowBounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) if let swipeToReplyNode = self.swipeToReplyNode { self.swipeToReplyNode = nil swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in @@ -2837,7 +2971,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode self.contextSourceNode.contentNode.addSubnode(accessoryItemNode) } - override func targetReactionNode(value: String) -> (ASImageNode, Int)? { + override func targetReactionNode(value: String) -> (ASDisplayNode, Int)? { for contentNode in self.contentNodes { if let (reactionNode, count) = contentNode.reactionTargetNode(value: value) { return (reactionNode, count) @@ -2845,4 +2979,67 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode } return nil } + + private var backgroundMaskMode: Bool { + let hasWallpaper = self.item?.presentationData.theme.wallpaper.hasWallpaper ?? false + let isPreview = self.item?.presentationData.isPreview ?? false + return self.contextSourceNode.isExtractedToContextPreview || hasWallpaper || isPreview + } + + func animateQuizInvalidOptionSelected() { + if let supernode = self.supernode, let subnodes = supernode.subnodes { + for i in 0 ..< subnodes.count { + if subnodes[i] === self { + break + } + } + } + + let duration: Double = 0.5 + let minScale: CGFloat = -0.03 + let scaleAnimation0 = self.layer.makeAnimation(from: 0.0 as NSNumber, to: minScale as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: duration / 2.0, removeOnCompletion: false, additive: true, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + let scaleAnimation1 = strongSelf.layer.makeAnimation(from: minScale as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: duration / 2.0, additive: true) + strongSelf.layer.add(scaleAnimation1, forKey: "quizInvalidScale") + }) + self.layer.add(scaleAnimation0, forKey: "quizInvalidScale") + + let k = Float(UIView.animationDurationFactor()) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + + let count = 4 + + let animation = CAKeyframeAnimation(keyPath: "transform.rotation.z") + var values: [CGFloat] = [] + values.append(0.0) + let rotationAmplitude: CGFloat = CGFloat.pi / 180.0 * 3.0 + for i in 0 ..< count { + let sign: CGFloat = (i % 2 == 0) ? 1.0 : -1.0 + let amplitude: CGFloat = rotationAmplitude + values.append(amplitude * sign) + } + values.append(0.0) + animation.values = values.map { ($0 as NSNumber) as AnyObject } + var keyTimes: [NSNumber] = [] + for i in 0 ..< values.count { + if i == 0 { + keyTimes.append(0.0) + } else if i == values.count - 1 { + keyTimes.append(1.0) + } else { + keyTimes.append((Double(i) / Double(values.count - 1)) as NSNumber) + } + } + animation.keyTimes = keyTimes + animation.speed = speed + animation.duration = duration + animation.isAdditive = true + + self.layer.add(animation, forKey: "quizInvalidRotation") + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageCallBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageCallBubbleContentNode.swift index 1a667442e5..b2c5038812 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageCallBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageCallBubbleContentNode.swift @@ -207,7 +207,7 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { } } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction { + override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.buttonNode.frame.contains(point) { return .ignore } else if self.bounds.contains(point), let item = self.item { diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageContactBubbleContentNode.swift index 5523ff8921..c351ad160c 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageContactBubbleContentNode.swift @@ -142,6 +142,9 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var edited = false + if item.attributes.updatingMedia != nil { + edited = true + } var viewCount: Int? for attribute in item.message.attributes { if let attribute = attribute as? EditedMessageAttribute { @@ -174,7 +177,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } else { if item.message.flags.contains(.Failed) { statusType = .BubbleOutgoing(.Failed) - } else if item.message.flags.isSending && !item.message.isSentOrAcknowledged { + } else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil { statusType = .BubbleOutgoing(.Sending) } else { statusType = .BubbleOutgoing(.Sent(read: item.read)) @@ -302,7 +305,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } if let peerId = selectedContact?.peerId, let peer = item.message.peers[peerId] { - strongSelf.avatarNode.setPeer(account: item.context.account, theme: item.presentationData.theme.theme, peer: peer, emptyColor: avatarPlaceholderColor, synchronousLoad: synchronousLoads) + strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme.theme, peer: peer, emptyColor: avatarPlaceholderColor, synchronousLoad: synchronousLoads) } else { strongSelf.avatarNode.setCustomLetters(customLetters) } @@ -325,7 +328,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction { + override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.buttonNode.frame.contains(point) { return .openMessage } @@ -346,7 +349,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } } - override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? { if !self.dateAndStatusNode.isHidden { return self.dateAndStatusNode.reactionNode(value: value) } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageContextControllerContentSource.swift b/submodules/TelegramUI/TelegramUI/ChatMessageContextControllerContentSource.swift index 93050d570f..ac481544af 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageContextControllerContentSource.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageContextControllerContentSource.swift @@ -26,7 +26,7 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou guard let item = itemNode.item else { return } - if item.content.contains(where: { $0.stableId == self.message.stableId }), let contentNode = itemNode.getMessageContextSourceNode() { + if item.content.contains(where: { $0.0.stableId == self.message.stableId }), let contentNode = itemNode.getMessageContextSourceNode() { result = ContextControllerTakeViewInfo(contentContainingNode: contentNode, contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil)) } } @@ -46,7 +46,7 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou guard let item = itemNode.item else { return } - if item.content.contains(where: { $0.stableId == self.message.stableId }) { + if item.content.contains(where: { $0.0.stableId == self.message.stableId }) { result = ContextControllerPutBackViewInfo(contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil)) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageDateAndStatusNode.swift index 38cf1fb2e6..114cebebbd 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageDateAndStatusNode.swift @@ -8,9 +8,9 @@ import Display import SwiftSignalKit import TelegramPresentationData import AccountContext +import AppBundle -private let dateFont = UIFont.italicSystemFont(ofSize: 11.0) -private let reactionCountFont = Font.semiboldItalic(11.0) +private let reactionCountFont = Font.semibold(11.0) private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) { if let _ = layer.animation(forKey: "clockFrameAnimation") { @@ -32,29 +32,6 @@ enum ChatMessageDateAndStatusOutgoingType: Equatable { case Sent(read: Bool) case Sending case Failed - - static func ==(lhs: ChatMessageDateAndStatusOutgoingType, rhs: ChatMessageDateAndStatusOutgoingType) -> Bool { - switch lhs { - case let .Sent(read): - if case .Sent(read) = rhs { - return true - } else { - return false - } - case .Sending: - if case .Sending = rhs { - return true - } else { - return false - } - case .Failed: - if case .Failed = rhs { - return true - } else { - return false - } - } - } } enum ChatMessageDateAndStatusType: Equatable { @@ -66,26 +43,90 @@ enum ChatMessageDateAndStatusType: Equatable { case FreeOutgoing(ChatMessageDateAndStatusOutgoingType) } -private let reactionSize: CGFloat = 19.0 +private let reactionSize: CGFloat = 20.0 private let reactionFont = Font.regular(12.0) -private final class StatusReactionNode: ASImageNode { +private final class StatusReactionNodeParameters: NSObject { + let value: String + let previousValue: String? + + init(value: String, previousValue: String?) { + self.value = value + self.previousValue = previousValue + } +} + +private func drawReaction(context: CGContext, value: String, in rect: CGRect) { + var fileId: Int? + switch value { + case "😔": + fileId = 8 + case "😳": + fileId = 19 + case "😂": + fileId = 17 + case "ðŸ‘": + fileId = 6 + case "â¤": + fileId = 13 + default: + break + } + if let fileId = fileId, let path = getAppBundle().path(forResource: "simplereaction_\(fileId)@2x", ofType: "png"), let image = UIImage(contentsOfFile: path) { + context.saveGState() + context.translateBy(x: rect.midX, y: rect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -rect.midX, y: -rect.midY) + context.draw(image.cgImage!, in: rect) + context.restoreGState() + } else { + let string = NSAttributedString(string: value, font: reactionFont, textColor: .black) + string.draw(at: CGPoint(x: rect.minX + 1.0, y: rect.minY + 3.0)) + } +} + +private final class StatusReactionNode: ASDisplayNode { let value: String var count: Int + var previousValue: String? { + didSet { + self.setNeedsDisplay() + } + } - init(value: String, count: Int) { + init(value: String, count: Int, previousValue: String?) { self.value = value self.count = count + self.previousValue = previousValue super.init() - self.image = generateImage(CGSize(width: reactionSize, height: reactionSize), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - UIGraphicsPushContext(context) - let string = NSAttributedString(string: value, font: reactionFont, textColor: .black) - string.draw(at: CGPoint(x: 1.0, y: 3.0)) - UIGraphicsPopContext() - }) + self.isOpaque = false + self.backgroundColor = nil + } + + override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + return StatusReactionNodeParameters(value: self.value, previousValue: self.previousValue) + } + + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + guard let parameters = parameters as? StatusReactionNodeParameters else { + return + } + drawReaction(context: context, value: parameters.value, in: bounds) + if let previousValue = parameters.previousValue { + let previousRect = bounds.offsetBy(dx: -14.0, dy: 0) + context.setBlendMode(.destinationOut) + drawReaction(context: context, value: previousValue, in: previousRect) + } } } @@ -146,15 +187,18 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { let themeUpdated = presentationData.theme != currentTheme || type != currentType - let graphics = PresentationResourcesChat.principalGraphics(mediaBox: context.account.postbox.mediaBox, knockoutWallpaper: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, gradientBubbles: context.sharedContext.immediateExperimentalUISettings.gradientBubbles) + let graphics = PresentationResourcesChat.principalGraphics(mediaBox: context.account.postbox.mediaBox, knockoutWallpaper: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) + let isDefaultWallpaper = serviceMessageColorHasDefaultWallpaper(presentationData.theme.wallpaper) let offset: CGFloat = -UIScreenPixel + let checkSize: CGFloat = floor(floor(presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)) + switch type { case .BubbleIncoming: dateColor = presentationData.theme.theme.chat.message.incoming.secondaryTextColor leftInset = 10.0 - loadedCheckFullImage = graphics.checkBubbleFullImage - loadedCheckPartialImage = graphics.checkBubblePartialImage + loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(presentationData.theme.theme, size: checkSize) + loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(presentationData.theme.theme, size: checkSize) clockFrameImage = graphics.clockBubbleIncomingFrameImage clockMinImage = graphics.clockBubbleIncomingMinImage if impressionCount != nil { @@ -164,8 +208,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { dateColor = presentationData.theme.theme.chat.message.outgoing.secondaryTextColor outgoingStatus = status leftInset = 10.0 - loadedCheckFullImage = graphics.checkBubbleFullImage - loadedCheckPartialImage = graphics.checkBubblePartialImage + loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(presentationData.theme.theme, size: checkSize) + loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(presentationData.theme.theme, size: checkSize) clockFrameImage = graphics.clockBubbleOutgoingFrameImage clockMinImage = graphics.clockBubbleOutgoingMinImage if impressionCount != nil { @@ -175,8 +219,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { dateColor = presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor backgroundImage = graphics.dateAndStatusMediaBackground leftInset = 0.0 - loadedCheckFullImage = graphics.checkMediaFullImage - loadedCheckPartialImage = graphics.checkMediaPartialImage + loadedCheckFullImage = PresentationResourcesChat.chatMediaFullCheck(presentationData.theme.theme, size: checkSize) + loadedCheckPartialImage = PresentationResourcesChat.chatMediaPartialCheck(presentationData.theme.theme, size: checkSize) clockFrameImage = graphics.clockMediaFrameImage clockMinImage = graphics.clockMediaMinImage if impressionCount != nil { @@ -187,8 +231,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { outgoingStatus = status backgroundImage = graphics.dateAndStatusMediaBackground leftInset = 0.0 - loadedCheckFullImage = graphics.checkMediaFullImage - loadedCheckPartialImage = graphics.checkMediaPartialImage + loadedCheckFullImage = PresentationResourcesChat.chatMediaFullCheck(presentationData.theme.theme, size: checkSize) + loadedCheckPartialImage = PresentationResourcesChat.chatMediaPartialCheck(presentationData.theme.theme, size: checkSize) clockFrameImage = graphics.clockMediaFrameImage clockMinImage = graphics.clockMediaMinImage if impressionCount != nil { @@ -199,8 +243,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { dateColor = serviceColor.primaryText backgroundImage = graphics.dateAndStatusFreeBackground leftInset = 0.0 - loadedCheckFullImage = graphics.checkFreeFullImage - loadedCheckPartialImage = graphics.checkFreePartialImage + loadedCheckFullImage = PresentationResourcesChat.chatFreeFullCheck(presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) + loadedCheckPartialImage = PresentationResourcesChat.chatFreePartialCheck(presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) clockFrameImage = graphics.clockFreeFrameImage clockMinImage = graphics.clockFreeMinImage if impressionCount != nil { @@ -212,8 +256,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { outgoingStatus = status backgroundImage = graphics.dateAndStatusFreeBackground leftInset = 0.0 - loadedCheckFullImage = graphics.checkFreeFullImage - loadedCheckPartialImage = graphics.checkFreePartialImage + loadedCheckFullImage = PresentationResourcesChat.chatFreeFullCheck(presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) + loadedCheckPartialImage = PresentationResourcesChat.chatFreePartialCheck(presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) clockFrameImage = graphics.clockFreeFrameImage clockMinImage = graphics.clockFreeMinImage if impressionCount != nil { @@ -229,8 +273,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { updatedDateText = compactNumericCountString(impressionCount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) + " " + updatedDateText } + let dateFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)) let (date, dateApply) = dateLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: updatedDateText, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let checkOffset = floor(presentationData.fontSize.baseDisplaySize * 6.0 / 17.0) + let statusWidth: CGFloat var checkSentFrame: CGRect? @@ -306,7 +353,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { clockFrameNode = nil clockMinNode = nil } else { - statusWidth = 13.0 + statusWidth = floor(floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0)) if checkReadNode == nil { checkReadNode = ASImageNode() @@ -330,7 +377,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { if read { checkReadFrame = CGRect(origin: CGPoint(x: leftInset + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width, y: 3.0 + offset), size: checkSize) } - checkSentFrame = CGRect(origin: CGPoint(x: leftInset + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width - 6.0, y: 3.0 + offset), size: checkSize) + checkSentFrame = CGRect(origin: CGPoint(x: leftInset + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width - checkOffset, y: 3.0 + offset), size: checkSize) } case .Failed: statusWidth = 0.0 @@ -364,9 +411,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { var reactionCountLayoutAndApply: (TextNodeLayout, () -> TextNode)? + let reactionSpacing: CGFloat = -4.0 + let reactionTrailingSpacing: CGFloat = 4.0 var reactionInset: CGFloat = 0.0 if !reactions.isEmpty { - reactionInset = 5.0 + CGFloat(reactions.count) * reactionSize + reactionInset = 5.0 + CGFloat(reactions.count) * reactionSize + CGFloat(reactions.count - 1) * reactionSpacing + reactionTrailingSpacing var count = 0 for reaction in reactions { @@ -415,7 +464,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { strongSelf.impressionIcon = currentImpressionIcon strongSelf.addSubnode(currentImpressionIcon) } - currentImpressionIcon.frame = CGRect(origin: CGPoint(x: leftInset + backgroundInsets.left, y: backgroundInsets.top + 3.0 + offset), size: impressionSize) + currentImpressionIcon.frame = CGRect(origin: CGPoint(x: leftInset + backgroundInsets.left, y: backgroundInsets.top + 1.0 + offset + floor((date.size.height - impressionSize.height) / 2.0)), size: impressionSize) } else if let impressionIcon = strongSelf.impressionIcon { impressionIcon.removeFromSupernode() strongSelf.impressionIcon = nil @@ -517,8 +566,9 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { if strongSelf.reactionNodes.count > i, strongSelf.reactionNodes[i].value == reactions[i].value { node = strongSelf.reactionNodes[i] node.count = Int(reactions[i].count) + node.previousValue = i == 0 ? nil : reactions[i - 1].value } else { - node = StatusReactionNode(value: reactions[i].value, count: Int(reactions[i].count)) + node = StatusReactionNode(value: reactions[i].value, count: Int(reactions[i].count), previousValue: i == 0 ? nil : reactions[i - 1].value) if strongSelf.reactionNodes.count > i { let previousNode = strongSelf.reactionNodes[i] if animated { @@ -540,11 +590,15 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } node.frame = CGRect(origin: CGPoint(x: reactionOffset, y: backgroundInsets.top + offset - 3.0), size: CGSize(width: reactionSize, height: reactionSize)) - reactionOffset += reactionSize + reactionOffset += reactionSize + reactionSpacing + } + if !reactions.isEmpty { + reactionOffset += reactionTrailingSpacing } for _ in reactions.count ..< strongSelf.reactionNodes.count { let node = strongSelf.reactionNodes.removeLast() if animated { + node.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in node?.removeFromSupernode() }) @@ -576,7 +630,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } - if !strongSelf.reactionNodes.isEmpty { + if false, !strongSelf.reactionNodes.isEmpty { if strongSelf.reactionButtonNode == nil { let reactionButtonNode = HighlightTrackingButtonNode() strongSelf.reactionButtonNode = reactionButtonNode @@ -628,7 +682,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } - func reactionNode(value: String) -> (ASImageNode, Int)? { + func reactionNode(value: String) -> (ASDisplayNode, Int)? { for node in self.reactionNodes { if node.value == value { return (node, node.count) diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageDateHeader.swift b/submodules/TelegramUI/TelegramUI/ChatMessageDateHeader.swift index 3459fd1564..97640a4d64 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageDateHeader.swift @@ -50,10 +50,15 @@ final class ChatMessageDateHeader: ListViewItemHeader { func node() -> ListViewItemHeaderNode { return ChatMessageDateHeaderNode(localTimestamp: self.roundedTimestamp, scheduled: self.scheduled, presentationData: self.presentationData, context: self.context, action: self.action) } + + func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) { + guard let node = node as? ChatMessageDateHeaderNode, let next = next as? ChatMessageDateHeader else { + return + } + node.updatePresentationData(next.presentationData, context: next.context) + } } -private let titleFont = Font.medium(13.0) - private func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String { switch index { case 0: @@ -93,6 +98,7 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { private let localTimestamp: Int32 private var presentationData: ChatPresentationData private let context: AccountContext + private let text: String private var flashingOnScrolling = false private var stickDistanceFactor: CGFloat = 0.0 @@ -107,7 +113,7 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { self.labelNode = TextNode() self.labelNode.isUserInteractionEnabled = false - self.labelNode.displaysAsynchronously = true + self.labelNode.displaysAsynchronously = !presentationData.isPreview self.backgroundNode = ASImageNode() self.backgroundNode.isLayerBacked = true @@ -119,19 +125,6 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { self.stickBackgroundNode.displayWithoutProcessing = true self.stickBackgroundNode.displaysAsynchronously = false - super.init(layerBacked: false, dynamicBounce: true, isRotated: true, seeThrough: false) - - self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) - - let graphics = PresentationResourcesChat.principalGraphics(mediaBox: context.account.postbox.mediaBox, knockoutWallpaper: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, gradientBubbles: context.sharedContext.immediateExperimentalUISettings.gradientBubbles) - - self.backgroundNode.image = graphics.dateStaticBackground - self.stickBackgroundNode.image = graphics.dateFloatingBackground - self.stickBackgroundNode.alpha = 0.0 - self.backgroundNode.addSubnode(self.stickBackgroundNode) - self.addSubnode(self.backgroundNode) - self.addSubnode(self.labelNode) - let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) var t: time_t = time_t(localTimestamp) @@ -162,6 +155,22 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { text = presentationData.strings.ScheduledMessages_ScheduledDate(text).0 } } + self.text = text + + super.init(layerBacked: false, dynamicBounce: true, isRotated: true, seeThrough: false) + + self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + + let graphics = PresentationResourcesChat.principalGraphics(mediaBox: context.account.postbox.mediaBox, knockoutWallpaper: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) + + self.backgroundNode.image = graphics.dateStaticBackground + self.stickBackgroundNode.image = graphics.dateFloatingBackground + self.stickBackgroundNode.alpha = 0.0 + self.backgroundNode.addSubnode(self.stickBackgroundNode) + self.addSubnode(self.backgroundNode) + self.addSubnode(self.labelNode) + + let titleFont = Font.medium(min(18.0, floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0))) let attributedString = NSAttributedString(string: text, font: titleFont, textColor: bubbleVariableColor(variableColor: presentationData.theme.theme.chat.serviceMessage.dateTextColor, wallpaper: presentationData.theme.wallpaper)) let labelLayout = TextNode.asyncLayout(self.labelNode) @@ -178,14 +187,38 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { } func updatePresentationData(_ presentationData: ChatPresentationData, context: AccountContext) { - let graphics = PresentationResourcesChat.principalGraphics(mediaBox: context.account.postbox.mediaBox, knockoutWallpaper: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, gradientBubbles: context.sharedContext.immediateExperimentalUISettings.gradientBubbles) + let previousPresentationData = self.presentationData + self.presentationData = presentationData + + let graphics = PresentationResourcesChat.principalGraphics(mediaBox: context.account.postbox.mediaBox, knockoutWallpaper: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) self.backgroundNode.image = graphics.dateStaticBackground self.stickBackgroundNode.image = graphics.dateFloatingBackground + let titleFont = Font.medium(min(18.0, floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0))) + + let attributedString = NSAttributedString(string: self.text, font: titleFont, textColor: bubbleVariableColor(variableColor: presentationData.theme.theme.chat.serviceMessage.dateTextColor, wallpaper: presentationData.theme.wallpaper)) + let labelLayout = TextNode.asyncLayout(self.labelNode) + + let (size, apply) = labelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let _ = apply() + + if presentationData.fontSize != previousPresentationData.fontSize { + self.labelNode.bounds = CGRect(origin: CGPoint(), size: size.size) + } + self.setNeedsLayout() } + func updateBackgroundColor(_ color: UIColor) { + let chatDateSize: CGFloat = 20.0 + self.backgroundNode.image = generateImage(CGSize(width: chatDateSize, height: chatDateSize), contextGenerator: { size, context -> Void in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + })!.stretchableImage(withLeftCapWidth: Int(chatDateSize) / 2, topCapHeight: Int(chatDateSize) / 2) + } + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { let chatDateSize: CGFloat = 20.0 let chatDateInset: CGFloat = 6.0 diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift index 2a2a51fff0..9512d2b705 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift @@ -44,7 +44,7 @@ final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubble } let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.context, item.controllerInteraction, item.message, true, title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) @@ -84,7 +84,7 @@ final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubble self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction { + override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.bounds.contains(point) { /*if let webPage = self.webPage, case let .Loaded(content) = webPage.content { if content.instantPage != nil { @@ -99,7 +99,7 @@ final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubble return self.contentNode.updateHiddenMedia(media) } - override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id != messageId { return nil } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift index 975026d312..cdefb5f241 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift @@ -39,7 +39,7 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent let text: String = item.message.text let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.context, item.controllerInteraction, item.message, true, title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) @@ -79,7 +79,7 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction { + override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.bounds.contains(point) { /*if let webPage = self.webPage, case let .Loaded(content) = webPage.content { if content.instantPage != nil { @@ -94,7 +94,7 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent return self.contentNode.updateHiddenMedia(media) } - override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id != messageId { return nil } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift index bb9ea516dd..406fb699b0 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift @@ -44,7 +44,7 @@ final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleCont } let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.context, item.controllerInteraction, item.message, true, title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) @@ -84,10 +84,10 @@ final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleCont self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction { + override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.bounds.contains(point) { let contentNodeFrame = self.contentNode.frame - return self.contentNode.tapActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY), gesture: gesture) + return self.contentNode.tapActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY), gesture: gesture, isEstimating: isEstimating) } return .none } @@ -101,7 +101,7 @@ final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleCont return self.contentNode.updateHiddenMedia(media) } - override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id != messageId { return nil } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageFileBubbleContentNode.swift index e36825afab..701b5ce552 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageFileBubbleContentNode.swift @@ -59,7 +59,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } else { if item.message.flags.contains(.Failed) { statusType = .BubbleOutgoing(.Failed) - } else if item.message.flags.isSending && !item.message.isSentOrAcknowledged { + } else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil { statusType = .BubbleOutgoing(.Sending) } else { statusType = .BubbleOutgoing(.Sent(read: item.read)) @@ -71,7 +71,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!) - let (initialWidth, refineLayout) = interactiveFileLayout(item.context, item.presentationData, item.message, selectedFile!, automaticDownload, item.message.effectivelyIncoming(item.context.account.peerId), item.associatedData.isRecentActions, item.associatedData.forcedResourceStatus, statusType, CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height)) + let (initialWidth, refineLayout) = interactiveFileLayout(item.context, item.presentationData, item.message, item.attributes, selectedFile!, automaticDownload, item.message.effectivelyIncoming(item.context.account.peerId), item.associatedData.isRecentActions, item.associatedData.forcedResourceStatus, statusType, CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height)) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) @@ -95,7 +95,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } } - override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id == messageId { return self.interactiveFileNode.transitionNode(media: media) } else { @@ -119,7 +119,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { self.interactiveFileNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } - override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? { return self.interactiveFileNode.reactionTargetNode(value: value) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageForwardInfoNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageForwardInfoNode.swift index ad44989da9..852f4ddb66 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageForwardInfoNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageForwardInfoNode.swift @@ -8,9 +8,6 @@ import SyncCore import TelegramPresentationData import LocalizedPeerData -private let prefixFont = Font.regular(13.0) -private let peerFont = Font.medium(13.0) - enum ChatMessageForwardInfoType { case bubble(incoming: Bool) case standalone @@ -28,6 +25,10 @@ class ChatMessageForwardInfoNode: ASDisplayNode { let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode) return { presentationData, strings, type, peer, authorName, constrainedSize in + let fontSize = floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0) + let prefixFont = Font.regular(fontSize) + let peerFont = Font.medium(fontSize) + let peerString: String if let peer = peer { if let authorName = authorName { @@ -58,7 +59,8 @@ class ChatMessageForwardInfoNode: ASDisplayNode { var highlight = true if let peer = peer { if let channel = peer as? TelegramChannel, channel.username == nil { - if case .member = channel.participationStatus { + if case let .broadcast(info) = channel.info, info.flags.contains(.hasDiscussionGroup) { + } else if case .member = channel.participationStatus { } else { highlight = false } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageGameBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageGameBubbleContentNode.swift index 2c178e673d..75fa0bed5c 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageGameBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageGameBubbleContentNode.swift @@ -71,7 +71,7 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.context, item.controllerInteraction, item.message, item.read, title, nil, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, title, nil, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) @@ -112,7 +112,7 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction { + override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.bounds.contains(point) { /*if let webPage = self.webPage, case let .Loaded(content) = webPage.content { if content.instantPage != nil { @@ -127,14 +127,14 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { return self.contentNode.updateHiddenMedia(media) } - override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id != messageId { return nil } return self.contentNode.transitionNode(media: media) } - override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? { return self.contentNode.reactionTargetNode(value: value) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageInstantVideoItemNode.swift index 6bff72df76..7ef8e6201b 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageInstantVideoItemNode.swift @@ -115,7 +115,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { let currentForwardInfo = self.appliedForwardInfo return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in - let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params) + let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData) let incoming = item.message.effectivelyIncoming(item.context.account.peerId) let avatarInset: CGFloat @@ -210,7 +210,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } } - let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, read: item.read, presentationData: item.presentationData, associatedData: item.associatedData), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, .free, automaticDownload) + let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, read: item.read, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, .free, automaticDownload) let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize) @@ -295,7 +295,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { updatedReplyBackgroundNode = ASImageNode() } - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) replyBackgroundImage = graphics.chatFreeformContentAdditionalInfoBackgroundImage } @@ -306,7 +306,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { if currentShareButtonNode != nil { updatedShareButtonNode = currentShareButtonNode if item.presentationData.theme !== currentItem?.presentationData.theme { - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) if item.message.id.peerId == item.context.account.peerId { updatedShareButtonBackground = graphics.chatBubbleNavigateButtonImage } else { @@ -316,7 +316,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } else { let buttonNode = HighlightableButtonNode() let buttonIcon: UIImage? - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) if item.message.id.peerId == item.context.account.peerId { buttonIcon = graphics.chatBubbleNavigateButtonImage } else { @@ -364,14 +364,14 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { updatedForwardBackgroundNode = ASImageNode() } - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) forwardBackgroundImage = graphics.chatServiceBubbleFillImage } var maxContentWidth = videoLayout.contentSize.width var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))? if let replyMarkup = replyMarkup { - let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.strings, replyMarkup, item.message, maxContentWidth) + let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maxContentWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout } @@ -764,17 +764,20 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { let offset: CGFloat = incoming ? 42.0 : 0.0 if let selectionNode = self.selectionNode { + let selectionFrame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) + selectionNode.frame = selectionFrame + selectionNode.updateLayout(size: selectionFrame.size) selectionNode.updateSelected(selected, animated: animated) - selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0); } else { - let selectionNode = ChatMessageSelectionNode(theme: item.presentationData.theme.theme, toggle: { [weak self] value in + let selectionNode = ChatMessageSelectionNode(wallpaper: item.presentationData.theme.wallpaper, theme: item.presentationData.theme.theme, toggle: { [weak self] value in if let strongSelf = self, let item = strongSelf.item { item.controllerInteraction.toggleMessagesSelection([item.message.id], value) } }) - - selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) + let selectionFrame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) + selectionNode.frame = selectionFrame + selectionNode.updateLayout(size: selectionFrame.size) self.addSubnode(selectionNode) self.selectionNode = selectionNode selectionNode.updateSelected(selected, animated: false) diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveFileNode.swift index 28db418c60..664a47f472 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -9,19 +9,16 @@ import SyncCore import UniversalMediaPlayer import TelegramPresentationData import AccountContext -import RadialStatusNode import PhotoResources import TelegramStringFormatting +import RadialStatusNode +import SemanticStatusNode private struct FetchControls { let fetch: () -> Void let cancel: () -> Void } -private let titleFont = Font.regular(16.0) -private let descriptionFont = Font.regular(13.0) -private let durationFont = Font.regular(11.0) - final class ChatMessageInteractiveFileNode: ASDisplayNode { private let titleNode: TextNode private let descriptionNode: TextNode @@ -35,7 +32,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { private let consumableContentNode: ASImageNode private var iconNode: TransformImageNode? - private var statusNode: RadialStatusNode? + private var statusNode: SemanticStatusNode? private var streamingStatusNode: RadialStatusNode? private var tapRecognizer: UITapGestureRecognizer? @@ -67,7 +64,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { private var context: AccountContext? private var message: Message? - private var themeAndStrings: (ChatPresentationThemeData, PresentationStrings, String)? + private var presentationData: ChatPresentationData? private var file: TelegramMediaFile? private var progressFrame: CGRect? private var streamingCacheStatusFrame: CGRect? @@ -189,7 +186,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))) { + func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))) { let currentFile = self.file let titleAsyncLayout = TextNode.asyncLayout(self.titleNode) @@ -199,8 +196,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let currentMessage = self.message - return { context, presentationData, message, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, constrainedSize in + return { context, presentationData, message, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, constrainedSize in return (CGFloat.greatestFiniteMagnitude, { constrainedSize in + let titleFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 16.0 / 17.0)) + let descriptionFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0)) + let durationFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)) + var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? var updatedStatusSignal: Signal<(FileMediaResourceStatus, MediaResourceStatus?), NoError>? var updatedPlaybackStatusSignal: Signal? @@ -268,6 +269,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { if let statusType = dateAndStatusType { var edited = false + if attributes.updatingMedia != nil { + edited = true + } var viewCount: Int? for attribute in message.attributes { if let attribute = attribute as? EditedMessageAttribute { @@ -435,7 +439,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { if hasThumbnail { fileIconImage = nil } else { - let principalGraphics = PresentationResourcesChat.principalGraphics(mediaBox: context.account.postbox.mediaBox, knockoutWallpaper: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, gradientBubbles: context.sharedContext.immediateExperimentalUISettings.gradientBubbles) + let principalGraphics = PresentationResourcesChat.principalGraphics(mediaBox: context.account.postbox.mediaBox, knockoutWallpaper: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) fileIconImage = incoming ? principalGraphics.radialIndicatorFileIconIncoming : principalGraphics.radialIndicatorFileIconOutgoing } @@ -493,8 +497,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } if let statusFrameValue = statusFrame, descriptionFrame.intersects(statusFrameValue) { - fittedLayoutSize.height += 10.0 - statusFrame = statusFrameValue.offsetBy(dx: 0.0, dy: 10.0) + fittedLayoutSize.height += statusFrameValue.height + statusFrame = statusFrameValue.offsetBy(dx: 0.0, dy: statusFrameValue.height) } if let statusFrameValue = statusFrame, let iconFrame = iconFrame, iconFrame.intersects(statusFrameValue) { fittedLayoutSize.height += 15.0 @@ -514,7 +518,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { return (fittedLayoutSize, { [weak self] synchronousLoads in if let strongSelf = self { strongSelf.context = context - strongSelf.themeAndStrings = (presentationData.theme, presentationData.strings, presentationData.dateTimeFormat.decimalSeparator) + strongSelf.presentationData = presentationData strongSelf.message = message strongSelf.file = file @@ -609,9 +613,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status, actualFetchStatus in displayLinkDispatcher.dispatch { if let strongSelf = strongSelf { + let firstTime = strongSelf.resourceStatus == nil strongSelf.resourceStatus = status strongSelf.actualFetchStatus = actualFetchStatus - strongSelf.updateStatus(animated: !synchronousLoads) + strongSelf.updateStatus(animated: !synchronousLoads || !firstTime) } } })) @@ -629,6 +634,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } strongSelf.waveformNode.displaysAsynchronously = !presentationData.isPreview + strongSelf.statusNode?.displaysAsynchronously = !presentationData.isPreview strongSelf.statusNode?.frame = progressFrame strongSelf.progressFrame = progressFrame strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame @@ -661,7 +667,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { guard let context = self.context else { return } - guard let (presentationData, _, decimalSeparator) = self.themeAndStrings else { + guard let presentationData = self.presentationData else { return } guard let progressFrame = self.progressFrame, let streamingCacheStatusFrame = self.streamingCacheStatusFrame else { @@ -671,7 +677,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { return } let incoming = message.effectivelyIncoming(context.account.peerId) - let messageTheme = incoming ? presentationData.theme.chat.message.incoming : presentationData.theme.chat.message.outgoing + let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing var isAudio = false var isVoice = false @@ -687,7 +693,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - let state: RadialStatusNodeState + let state: SemanticStatusNodeState var streamingState: RadialStatusNodeState = .none let isSending = message.flags.isSending @@ -706,8 +712,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { switch fetchStatus { case let .Fetching(_, progress): if let size = file.size { - let compactString = dataSizeString(Int(Float(size) * progress), forceDecimal: true, decimalSeparator: decimalSeparator) - downloadingStrings = ("\(compactString) / \(dataSizeString(size, forceDecimal: true, decimalSeparator: decimalSeparator))", compactString, descriptionFont) + let compactString = dataSizeString(Int(Float(size) * progress), forceDecimal: true, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) + let descriptionFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0)) + downloadingStrings = ("\(compactString) / \(dataSizeString(size, forceDecimal: true, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", compactString, descriptionFont) } default: break @@ -725,6 +732,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { playerDuration = Int32(playerStatus.duration) let durationString = stringForDuration(playerDuration > 0 ? playerDuration : (audioDuration ?? 0), position: playerPosition) + let durationFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)) downloadingStrings = (durationString, durationString, durationFont) } } @@ -755,9 +763,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let statusForegroundColor: UIColor if self.iconNode != nil { - statusForegroundColor = presentationData.theme.chat.message.mediaOverlayControlColors.foregroundColor + statusForegroundColor = presentationData.theme.theme.chat.message.mediaOverlayControlColors.foregroundColor } else { - statusForegroundColor = incoming ? presentationData.theme.chat.message.incoming.mediaControlInnerBackgroundColor : presentationData.theme.chat.message.outgoing.mediaControlInnerBackgroundColor + statusForegroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.mediaControlInnerBackgroundColor : presentationData.theme.theme.chat.message.outgoing.mediaControlInnerBackgroundColor } switch resourceStatus.mediaStatus { case var .fetchStatus(fetchStatus): @@ -768,10 +776,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { switch fetchStatus { case let .Fetching(_, progress): let adjustedProgress = max(progress, 0.027) - state = .progress(color: statusForegroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true) + state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true) case .Local: if isAudio { - state = .play(statusForegroundColor) + state = .play } else if let fileIconImage = self.fileIconImage { state = .customIcon(fileIconImage) } else { @@ -779,30 +787,33 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } case .Remote: if isAudio && !isVoice { - state = .play(statusForegroundColor) + state = .play } else { - state = .download(statusForegroundColor) + state = .download } } case let .playbackStatus(playbackStatus): self.waveformScrubbingNode?.enableScrubbing = true switch playbackStatus { case .playing: - state = .pause(statusForegroundColor) + state = .pause case .paused: - state = .play(statusForegroundColor) + state = .play } } let backgroundNodeColor: UIColor + let foregroundNodeColor: UIColor if self.iconNode != nil { - backgroundNodeColor = presentationData.theme.chat.message.mediaOverlayControlColors.fillColor + backgroundNodeColor = presentationData.theme.theme.chat.message.mediaOverlayControlColors.fillColor + foregroundNodeColor = .white } else { backgroundNodeColor = messageTheme.mediaActiveControlColor + foregroundNodeColor = .clear } if state != .none && self.statusNode == nil { - let statusNode = RadialStatusNode(backgroundNodeColor: backgroundNodeColor) + let statusNode = SemanticStatusNode(backgroundNodeColor: backgroundNodeColor, foregroundNodeColor: foregroundNodeColor) self.statusNode = statusNode statusNode.frame = progressFrame self.addSubnode(statusNode) @@ -820,8 +831,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { if let statusNode = self.statusNode { if state == .none { self.statusNode = nil + if animated { + statusNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) + statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } } - statusNode.transitionToState(state, animated: animated, synchronous: presentationData.theme.preview, completion: { [weak statusNode] in + statusNode.transitionToState(state, animated: animated, synchronous: presentationData.theme.theme.preview, completion: { [weak statusNode] in if state == .none { statusNode?.removeFromSupernode() } @@ -872,12 +887,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { self.fetchingCompactTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingCompactSize) } - static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageInteractiveFileNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageInteractiveFileNode))) { let currentAsyncLayout = node?.asyncLayout() - return { context, presentationData, message, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, constrainedSize in + return { context, presentationData, message, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, constrainedSize in var fileNode: ChatMessageInteractiveFileNode - var fileLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))) + var fileLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { fileNode = node @@ -887,7 +902,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { fileLayout = fileNode.asyncLayout() } - let (initialWidth, continueLayout) = fileLayout(context, presentationData, message, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, constrainedSize) + let (initialWidth, continueLayout) = fileLayout(context, presentationData, message, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, constrainedSize) return (initialWidth, { constrainedSize in let (finalWidth, finalLayout) = continueLayout(constrainedSize) @@ -904,9 +919,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - func transitionNode(media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if let iconNode = self.iconNode, let file = self.file, file.isEqual(to: media) { - return (iconNode, { [weak iconNode] in + return (iconNode, iconNode.bounds, { [weak iconNode] in return (iconNode?.view.snapshotContentTree(unhide: true), nil) }) } else { @@ -943,7 +958,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { self.playerUpdateTimer = nil } - func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? { if !self.dateAndStatusNode.isHidden { return self.dateAndStatusNode.reactionNode(value: value) } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift index 8791948699..8a35e620dc 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift @@ -239,7 +239,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { case .bubble: if item.message.flags.contains(.Failed) { statusType = .BubbleOutgoing(.Failed) - } else if item.message.flags.isSending && !item.message.isSentOrAcknowledged { + } else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil { statusType = .BubbleOutgoing(.Sending) } else { statusType = .BubbleOutgoing(.Sent(read: item.read)) @@ -248,6 +248,9 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } var edited = false + if item.attributes.updatingMedia != nil { + edited = true + } let sentViaBot = false var viewCount: Int? = nil for attribute in item.message.attributes { diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveMediaBadge.swift b/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveMediaBadge.swift index d71b6f4a6f..b713b25614 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveMediaBadge.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveMediaBadge.swift @@ -87,7 +87,7 @@ final class ChatMessageInteractiveMediaBadge: ASDisplayNode { return self.measureNode.measure(CGSize(width: 240.0, height: 160.0)).width } - func update(theme: PresentationTheme, content: ChatMessageInteractiveMediaBadgeContent?, mediaDownloadState: ChatMessageInteractiveMediaDownloadState?, alignment: NSTextAlignment = .left, animated: Bool, badgeAnimated: Bool = true) { + func update(theme: PresentationTheme?, content: ChatMessageInteractiveMediaBadgeContent?, mediaDownloadState: ChatMessageInteractiveMediaDownloadState?, alignment: NSTextAlignment = .left, animated: Bool, badgeAnimated: Bool = true) { var transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate let previousContentSize = self.previousContentSize @@ -263,7 +263,7 @@ final class ChatMessageInteractiveMediaBadge: ASDisplayNode { var originY: CGFloat = 5.0 switch mediaDownloadState { case .remote: - if let image = PresentationResourcesChat.chatBubbleFileCloudFetchMediaIcon(theme) { + if let theme = theme, let image = PresentationResourcesChat.chatBubbleFileCloudFetchMediaIcon(theme) { state = .customIcon(image) } else { state = .none diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveMediaNode.swift index fe019dd5d6..42c76d580f 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -79,6 +79,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio private var context: AccountContext? private var message: Message? + private var attributes: ChatMessageEntryAttributes? private var media: Media? private var themeAndStrings: (PresentationTheme, PresentationStrings, String)? private var sizeCalculation: InteractiveMediaNodeSizeCalculation? @@ -168,7 +169,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } private func progressPressed(canActivate: Bool) { - if let fetchStatus = self.fetchStatus { + if let _ = self.attributes?.updatingMedia { + if let message = self.message { + self.context?.account.pendingUpdateMessageManager.cancel(messageId: message.id) + } + } else if let fetchStatus = self.fetchStatus { var activateContent = false if let state = self.statusNode?.state, case .play = state { activateContent = true @@ -214,7 +219,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio @objc func imageTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { let point = recognizer.location(in: self.imageNode.view) - if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { + if let _ = self.attributes?.updatingMedia { + if let statusNode = self.statusNode, statusNode.frame.contains(point) { + self.progressPressed(canActivate: true) + } + } else if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { var videoContentMatch = true if let content = self.videoContent, case let .message(stableId, mediaId) = content.nativeId { videoContentMatch = self.message?.stableId == stableId && self.media?.id == mediaId @@ -232,7 +241,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - func asyncLayout() -> (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) { + func asyncLayout() -> (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) { let currentMessage = self.message let currentMedia = self.media let imageLayout = self.imageNode.asyncLayout() @@ -245,7 +254,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio let currentAutomaticDownload = self.automaticDownload let currentAutomaticPlayback = self.automaticPlayback - return { [weak self] context, theme, strings, dateTimeFormat, message, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in + return { [weak self] context, theme, strings, dateTimeFormat, message, attributes, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in var nativeSize: CGSize let isSecretMedia = message.containsSecretMedia @@ -303,7 +312,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio unboundSize = CGSize(width: floor(dimensions.cgSize.width * 0.5), height: floor(dimensions.cgSize.height * 0.5)) } else if let wallpaper = media as? WallpaperPreviewMedia { switch wallpaper.content { - case let .file(file, _, isTheme, isSupported): + case let .file(file, _, _, _, isTheme, isSupported): if let thumbnail = file.previewRepresentations.first, var dimensions = file.dimensions { let dimensionsVertical = dimensions.width < dimensions.height let thumbnailVertical = thumbnail.dimensions.width < thumbnail.dimensions.height @@ -311,6 +320,9 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio dimensions = PixelDimensions(CGSize(width: dimensions.cgSize.height, height: dimensions.cgSize.width)) } unboundSize = CGSize(width: floor(dimensions.cgSize.width * 0.5), height: floor(dimensions.cgSize.height * 0.5)).fitted(CGSize(width: 240.0, height: 240.0)) + } else if file.mimeType == "image/svg+xml" || file.mimeType == "application/x-tgwallpattern" { + let dimensions = CGSize(width: 1440.0, height: 2960.0) + unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)).fitted(CGSize(width: 240.0, height: 240.0)) } else if isTheme { if isSupported { unboundSize = CGSize(width: 160.0, height: 240.0).fitted(CGSize(width: 240.0, height: 240.0)) @@ -322,7 +334,9 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } else { unboundSize = CGSize(width: 54.0, height: 54.0) } - case .color: + case .themeSettings: + unboundSize = CGSize(width: 160.0, height: 240.0).fitted(CGSize(width: 240.0, height: 240.0)) + case .color, .gradient: unboundSize = CGSize(width: 128.0, height: 128.0) } } else { @@ -433,13 +447,21 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio var onlyFullSizeVideoThumbnail: Bool? var emptyColor: UIColor + var patternArguments: PatternWallpaperArguments? if isSticker { emptyColor = .clear } else { emptyColor = message.effectivelyIncoming(context.account.peerId) ? theme.chat.message.incoming.mediaPlaceholderColor : theme.chat.message.outgoing.mediaPlaceholderColor } - if let wallpaper = media as? WallpaperPreviewMedia, case let .file(_, patternColor, _, _) = wallpaper.content { - emptyColor = patternColor ?? UIColor(rgb: 0xd6e2ee, alpha: 0.5) + if let wallpaper = media as? WallpaperPreviewMedia { + if case let .file(_, patternColor, patternBottomColor, rotation, _, _) = wallpaper.content { + var colors: [UIColor] = [] + colors.append(patternColor ?? UIColor(rgb: 0xd6e2ee, alpha: 0.5)) + if let patternBottomColor = patternBottomColor { + colors.append(patternBottomColor) + } + patternArguments = PatternWallpaperArguments(colors: colors, rotation: rotation) + } } if mediaUpdated || isSendingUpdated || automaticPlaybackUpdated { @@ -523,8 +545,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if file.isVideo && !isSecretMedia && automaticPlayback && !uploading { updateVideoFile = file if hasCurrentVideoNode { - if let currentFile = currentMedia as? TelegramMediaFile, currentFile.resource is EmptyMediaResource { - replaceVideoNode = true + if let currentFile = currentMedia as? TelegramMediaFile { + if currentFile.resource is EmptyMediaResource { + replaceVideoNode = true + } else if currentFile.fileId.namespace == Namespaces.Media.CloudFile && file.fileId.namespace == Namespaces.Media.CloudFile && currentFile.fileId != file.fileId { + replaceVideoNode = true + } } } else if !(file.resource is LocalFileVideoMediaResource) { replaceVideoNode = true @@ -568,23 +594,30 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } else if let wallpaper = media as? WallpaperPreviewMedia { updateImageSignal = { synchronousLoad in switch wallpaper.content { - case let .file(file, _, isTheme, _): + case let .file(file, _, _, _, isTheme, _): if isTheme { - return themeImage(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: FileMediaReference.message(message: MessageReference(message), media: file)) + return themeImage(account: context.account, accountManager: context.sharedContext.accountManager, source: .file(FileMediaReference.message(message: MessageReference(message), media: file))) } else { - let representations: [ImageRepresentationWithReference] = file.previewRepresentations.map({ ImageRepresentationWithReference(representation: $0, reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference($0.resource)) }) - if file.mimeType == "image/png" { + var representations: [ImageRepresentationWithReference] = file.previewRepresentations.map({ ImageRepresentationWithReference(representation: $0, reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference($0.resource)) }) + if file.mimeType == "image/svg+xml" || file.mimeType == "application/x-tgwallpattern" { + representations.append(ImageRepresentationWithReference(representation: .init(dimensions: PixelDimensions(width: 1440, height: 2960), resource: file.resource), reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource))) + } + if ["image/png", "image/svg+xml", "application/x-tgwallpattern"].contains(file.mimeType) { return patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: representations, mode: .thumbnail) } else { return wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: FileMediaReference.message(message: MessageReference(message), media: file), representations: representations, alwaysShowThumbnailFirst: false, thumbnail: true, autoFetchFullSize: true) } } + case let .themeSettings(settings): + return themeImage(account: context.account, accountManager: context.sharedContext.accountManager, source: .settings(settings)) case let .color(color): - return solidColor(color) + return solidColorImage(color) + case let .gradient(topColor, bottomColor, rotation): + return gradientImage([topColor, bottomColor], rotation: rotation ?? 0) } } - if case let .file(file, _, _, _) = wallpaper.content { + if case let .file(file, _, _, _, _, _) = wallpaper.content { updatedFetchControls = FetchControls(fetch: { manual in if let strongSelf = self { strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: manual).start()) @@ -592,6 +625,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio }, cancel: { messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: file) }) + } else if case .themeSettings = wallpaper.content { } else { boundingSize = CGSize(width: boundingSize.width, height: boundingSize.width) } @@ -628,18 +662,18 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } else if let wallpaper = media as? WallpaperPreviewMedia { switch wallpaper.content { - case let .file(file, _, _, _): + case let .file(file, _, _, _, _, _): updatedStatusSignal = messageMediaFileStatus(context: context, messageId: message.id, file: file) |> map { resourceStatus -> (MediaResourceStatus, MediaResourceStatus?) in return (resourceStatus, nil) } - case .color: + case .themeSettings, .color, .gradient: updatedStatusSignal = .single((.Local, nil)) } } } - let arguments = TransformImageArguments(corners: corners, imageSize: drawingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: isInlinePlayableVideo ? .fill(.black) : .blurBackground, emptyColor: emptyColor) + let arguments = TransformImageArguments(corners: corners, imageSize: drawingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: isInlinePlayableVideo ? .fill(.black) : .blurBackground, emptyColor: emptyColor, custom: patternArguments) let imageFrame = CGRect(origin: CGPoint(x: -arguments.insets.left, y: -arguments.insets.top), size: arguments.drawingSize) @@ -649,6 +683,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if let strongSelf = self { strongSelf.context = context strongSelf.message = message + strongSelf.attributes = attributes strongSelf.media = media strongSelf.wideLayout = wideLayout strongSelf.themeAndStrings = (theme, strings, dateTimeFormat.decimalSeparator) @@ -879,7 +914,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } private func updateStatus(animated: Bool) { - guard let (theme, strings, decimalSeparator) = self.themeAndStrings, let sizeCalculation = self.sizeCalculation, let message = self.message, var automaticPlayback = self.automaticPlayback, let wideLayout = self.wideLayout else { + guard let (theme, strings, decimalSeparator) = self.themeAndStrings, let sizeCalculation = self.sizeCalculation, let message = self.message, let attributes = self.attributes, var automaticPlayback = self.automaticPlayback, let wideLayout = self.wideLayout else { return } @@ -919,7 +954,9 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } var progressRequired = false - if secretBeginTimeAndTimeout?.0 != nil { + if let updatingMedia = attributes.updatingMedia, case .update = updatingMedia.media { + progressRequired = true + } else if secretBeginTimeAndTimeout?.0 != nil { progressRequired = true } else if let fetchStatus = self.fetchStatus { switch fetchStatus { @@ -992,7 +1029,9 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: string) } var animated: Bool = animated - if var fetchStatus = self.fetchStatus { + if let updatingMedia = attributes.updatingMedia { + state = .progress(color: messageTheme.mediaOverlayControlColors.foregroundColor, lineWidth: nil, value: CGFloat(updatingMedia.progress), cancelEnabled: true) + } else if var fetchStatus = self.fetchStatus { var playerPosition: Int32? var playerDuration: Int32 = 0 var active = false @@ -1016,6 +1055,10 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if let file = self.media as? TelegramMediaFile, file.isAnimated { muted = false + + if case .Fetching = fetchStatus, message.flags.isSending, file.resource is CloudDocumentMediaResource { + fetchStatus = .Local + } } if message.flags.contains(.Unsent) { @@ -1245,12 +1288,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode))) { let currentAsyncLayout = node?.asyncLayout() - return { context, theme, strings, dateTimeFormat, message, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in + return { context, theme, strings, dateTimeFormat, message, attributes, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in var imageNode: ChatMessageInteractiveMediaNode - var imageLayout: (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) + var imageLayout: (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { imageNode = node @@ -1260,7 +1303,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio imageLayout = imageNode.asyncLayout() } - let (unboundSize, initialWidth, continueLayout) = imageLayout(context, theme, strings, dateTimeFormat, message, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode) + let (unboundSize, initialWidth, continueLayout) = imageLayout(context, theme, strings, dateTimeFormat, message, attributes, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode) return (unboundSize, initialWidth, { constrainedSize, automaticPlayback, wideLayout, corners in let (finalWidth, finalLayout) = continueLayout(constrainedSize, automaticPlayback, wideLayout, corners) @@ -1309,8 +1352,14 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - func transitionNode() -> (ASDisplayNode, () -> (UIView?, UIView?))? { - return (self, { [weak self] in + func transitionNode() -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + let bounds: CGRect + if let currentImageArguments = self.currentImageArguments { + bounds = currentImageArguments.imageRect + } else { + bounds = self.bounds + } + return (self, bounds, { [weak self] in var badgeNodeHidden: Bool? if let badgeNode = self?.badgeNode { badgeNodeHidden = badgeNode.isHidden diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift index 467e78d1b3..1b7f36074c 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift @@ -74,7 +74,7 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, automaticDownloadSettings, item.associatedData, item.context, item.controllerInteraction, item.message, item.read, title, subtitle, text, nil, mediaAndFlags, nil, nil, nil, false, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, automaticDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, title, subtitle, text, nil, mediaAndFlags, nil, nil, nil, false, layoutConstants, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) @@ -115,7 +115,7 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction { + override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.bounds.contains(point) { /*if let webPage = self.webPage, case let .Loaded(content) = webPage.content { if content.instantPage != nil { @@ -130,14 +130,14 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { return self.contentNode.updateHiddenMedia(media) } - override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id != messageId { return nil } return self.contentNode.transitionNode(media: media) } - override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? { return self.contentNode.reactionTargetNode(value: value) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageItem.swift b/submodules/TelegramUI/TelegramUI/ChatMessageItem.swift index 437411d05e..fe50b15654 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageItem.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageItem.swift @@ -43,14 +43,23 @@ public enum ChatMessageItemContent: Sequence { } } - public func makeIterator() -> AnyIterator { + var firstMessageAttributes: ChatMessageEntryAttributes { + switch self { + case let .message(message): + return message.attributes + case let .group(messages): + return messages[0].3 + } + } + + public func makeIterator() -> AnyIterator<(Message, ChatMessageEntryAttributes)> { var index = 0 - return AnyIterator { () -> Message? in + return AnyIterator { () -> (Message, ChatMessageEntryAttributes)? in switch self { - case let .message(message, _, _, _): + case let .message(message): if index == 0 { index += 1 - return message + return (message.message, message.attributes) } else { index += 1 return nil @@ -59,7 +68,7 @@ public enum ChatMessageItemContent: Sequence { if index < messages.count { let currentIndex = index index += 1 - return messages[currentIndex].0 + return (messages[currentIndex].0, messages[currentIndex].3) } else { return nil } @@ -217,10 +226,10 @@ public final class ChatMessageItemAssociatedData: Equatable { let isRecentActions: Bool let isScheduledMessages: Bool let contactsPeerIds: Set - let animatedEmojiStickers: [String: StickerPackItem] + let animatedEmojiStickers: [String: [StickerPackItem]] let forcedResourceStatus: FileMediaResourceStatus? - init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, isScheduledMessages: Bool = false, contactsPeerIds: Set = Set(), animatedEmojiStickers: [String: StickerPackItem] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil) { + init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, isScheduledMessages: Bool = false, contactsPeerIds: Set = Set(), animatedEmojiStickers: [String: [StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil) { self.automaticDownloadPeerType = automaticDownloadPeerType self.automaticDownloadNetworkType = automaticDownloadNetworkType self.isRecentActions = isRecentActions diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageItemView.swift b/submodules/TelegramUI/TelegramUI/ChatMessageItemView.swift index 1f3972d0f3..96066bb31d 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageItemView.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageItemView.swift @@ -11,9 +11,9 @@ import ContextUI import ChatListUI struct ChatMessageItemWidthFill { - let compactInset: CGFloat - let compactWidthBoundary: CGFloat - let freeMaximumFillFactor: CGFloat + var compactInset: CGFloat + var compactWidthBoundary: CGFloat + var freeMaximumFillFactor: CGFloat func widthFor(_ width: CGFloat) -> CGFloat { if width <= self.compactWidthBoundary { @@ -25,68 +25,68 @@ struct ChatMessageItemWidthFill { } struct ChatMessageItemBubbleLayoutConstants { - let edgeInset: CGFloat - let defaultSpacing: CGFloat - let mergedSpacing: CGFloat - let maximumWidthFill: ChatMessageItemWidthFill - let minimumSize: CGSize - let contentInsets: UIEdgeInsets - let borderInset: CGFloat - let strokeInsets: UIEdgeInsets + var edgeInset: CGFloat + var defaultSpacing: CGFloat + var mergedSpacing: CGFloat + var maximumWidthFill: ChatMessageItemWidthFill + var minimumSize: CGSize + var contentInsets: UIEdgeInsets + var borderInset: CGFloat + var strokeInsets: UIEdgeInsets } struct ChatMessageItemTextLayoutConstants { - let bubbleInsets: UIEdgeInsets + var bubbleInsets: UIEdgeInsets } struct ChatMessageItemImageLayoutConstants { - let bubbleInsets: UIEdgeInsets - let statusInsets: UIEdgeInsets - let defaultCornerRadius: CGFloat - let mergedCornerRadius: CGFloat - let contentMergedCornerRadius: CGFloat - let maxDimensions: CGSize - let minDimensions: CGSize + var bubbleInsets: UIEdgeInsets + var statusInsets: UIEdgeInsets + var defaultCornerRadius: CGFloat + var mergedCornerRadius: CGFloat + var contentMergedCornerRadius: CGFloat + var maxDimensions: CGSize + var minDimensions: CGSize } struct ChatMessageItemVideoLayoutConstants { - let maxHorizontalHeight: CGFloat - let maxVerticalHeight: CGFloat + var maxHorizontalHeight: CGFloat + var maxVerticalHeight: CGFloat } struct ChatMessageItemInstantVideoConstants { - let insets: UIEdgeInsets - let dimensions: CGSize + var insets: UIEdgeInsets + var dimensions: CGSize } struct ChatMessageItemFileLayoutConstants { - let bubbleInsets: UIEdgeInsets + var bubbleInsets: UIEdgeInsets } struct ChatMessageItemWallpaperLayoutConstants { - let maxTextWidth: CGFloat + var maxTextWidth: CGFloat } struct ChatMessageItemLayoutConstants { - let avatarDiameter: CGFloat - let timestampHeaderHeight: CGFloat + var avatarDiameter: CGFloat + var timestampHeaderHeight: CGFloat - let bubble: ChatMessageItemBubbleLayoutConstants - let image: ChatMessageItemImageLayoutConstants - let video: ChatMessageItemVideoLayoutConstants - let text: ChatMessageItemTextLayoutConstants - let file: ChatMessageItemFileLayoutConstants - let instantVideo: ChatMessageItemInstantVideoConstants - let wallpapers: ChatMessageItemWallpaperLayoutConstants + var bubble: ChatMessageItemBubbleLayoutConstants + var image: ChatMessageItemImageLayoutConstants + var video: ChatMessageItemVideoLayoutConstants + var text: ChatMessageItemTextLayoutConstants + var file: ChatMessageItemFileLayoutConstants + var instantVideo: ChatMessageItemInstantVideoConstants + var wallpapers: ChatMessageItemWallpaperLayoutConstants static var `default`: ChatMessageItemLayoutConstants { return self.compact } fileprivate static var compact: ChatMessageItemLayoutConstants { - let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 1.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.85), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0 - UIScreenPixel, left: 1.0 - UIScreenPixel, bottom: 1.0 - UIScreenPixel, right: 1.0 - UIScreenPixel)) + let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 1.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.85), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)) let text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 12.0, bottom: 6.0 - UIScreenPixel, right: 12.0)) - let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 1.0 + UIScreenPixel, left: 1.0 + UIScreenPixel, bottom: 1.0 + UIScreenPixel, right: 1.0 + UIScreenPixel), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 17.0, mergedCornerRadius: 5.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 300.0, height: 300.0), minDimensions: CGSize(width: 170.0, height: 74.0)) + let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 0.0, maxDimensions: CGSize(width: 300.0, height: 300.0), minDimensions: CGSize(width: 170.0, height: 74.0)) let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0) let file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0)) let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 212.0, height: 212.0)) @@ -96,9 +96,9 @@ struct ChatMessageItemLayoutConstants { } fileprivate static var regular: ChatMessageItemLayoutConstants { - let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 1.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.65), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0 - UIScreenPixel, left: 1.0 - UIScreenPixel, bottom: 1.0 - UIScreenPixel, right: 1.0 - UIScreenPixel)) + let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 1.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.65), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)) let text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 12.0, bottom: 6.0 - UIScreenPixel, right: 12.0)) - let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 1.0 + UIScreenPixel, left: 1.0 + UIScreenPixel, bottom: 1.0 + UIScreenPixel, right: 1.0 + UIScreenPixel), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 17.0, mergedCornerRadius: 5.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 440.0, height: 440.0), minDimensions: CGSize(width: 170.0, height: 74.0)) + let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 440.0, height: 440.0), minDimensions: CGSize(width: 170.0, height: 74.0)) let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0) let file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0)) let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 212.0, height: 212.0)) @@ -108,12 +108,24 @@ struct ChatMessageItemLayoutConstants { } } -func chatMessageItemLayoutConstants(_ constants: (ChatMessageItemLayoutConstants, ChatMessageItemLayoutConstants), params: ListViewItemLayoutParams) -> ChatMessageItemLayoutConstants { +func chatMessageItemLayoutConstants(_ constants: (ChatMessageItemLayoutConstants, ChatMessageItemLayoutConstants), params: ListViewItemLayoutParams, presentationData: ChatPresentationData) -> ChatMessageItemLayoutConstants { + var result: ChatMessageItemLayoutConstants if params.width > 680.0 { - return constants.1 + result = constants.1 } else { - return constants.0 + result = constants.0 } + result.image.defaultCornerRadius = presentationData.chatBubbleCorners.mainRadius + result.image.mergedCornerRadius = (presentationData.chatBubbleCorners.mergeBubbleCorners && result.image.defaultCornerRadius >= 10.0) ? presentationData.chatBubbleCorners.auxiliaryRadius : presentationData.chatBubbleCorners.mainRadius + let minRadius: CGFloat = 4.0 + let maxRadius: CGFloat = 16.0 + let radiusTransition = (presentationData.chatBubbleCorners.mainRadius - minRadius) / (maxRadius - minRadius) + let minInset: CGFloat = 9.0 + let maxInset: CGFloat = 12.0 + let textInset: CGFloat = min(maxInset, ceil(maxInset * radiusTransition + minInset * (1.0 - radiusTransition))) + result.text.bubbleInsets.left = textInset + result.text.bubbleInsets.right = textInset + return result } enum ChatMessageItemBottomNeighbor { @@ -634,7 +646,7 @@ public class ChatMessageItemView: ListViewItemNode { var item: ChatMessageItem? var accessibilityData: ChatMessageAccessibilityData? - var awaitingAppliedReaction: (String, () -> Void)? + var awaitingAppliedReaction: (String?, () -> Void)? public required convenience init() { self.init(layerBacked: false) @@ -698,7 +710,7 @@ public class ChatMessageItemView: ListViewItemNode { } } - func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil } @@ -763,7 +775,7 @@ public class ChatMessageItemView: ListViewItemNode { case .text: item.controllerInteraction.sendMessage(button.title) case let .url(url): - item.controllerInteraction.openUrl(url, true, nil) + item.controllerInteraction.openUrl(url, true, nil, nil) case .requestMap: item.controllerInteraction.shareCurrentLocation() case .requestPhone: @@ -799,6 +811,8 @@ public class ChatMessageItemView: ListViewItemNode { item.controllerInteraction.openCheckoutOrReceipt(item.message.id) case let .urlAuth(url, buttonId): item.controllerInteraction.requestMessageActionUrlAuth(url, item.message.id, buttonId) + case let .setupPoll(isQuiz): + break } } } @@ -814,7 +828,7 @@ public class ChatMessageItemView: ListViewItemNode { } } - func targetReactionNode(value: String) -> (ASImageNode, Int)? { + func targetReactionNode(value: String) -> (ASDisplayNode, Int)? { return nil } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageMapBubbleContentNode.swift index d9a6074e55..178685165d 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageMapBubbleContentNode.swift @@ -9,6 +9,7 @@ import SyncCore import LiveLocationTimerNode import PhotoResources import MediaResources +import LocationResources import LiveLocationPositionNode private let titleFont = Font.medium(14.0) @@ -141,15 +142,13 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 5.0, hidesBackground: (activeLiveBroadcastingTimeout == nil && selectedMedia?.venue == nil) ? .emptyWallpaper : .never, forceFullCorners: false, forceAlignment: .none) - var pinPeer: Peer? - var pinLiveLocationActive: Bool? - if let selectedMedia = selectedMedia { + var mode: ChatMessageLiveLocationPositionNode.Mode = .location(selectedMedia) + if let selectedMedia = selectedMedia, let peer = item.message.author { if selectedMedia.liveBroadcastingTimeout != nil { - pinPeer = item.message.author - pinLiveLocationActive = activeLiveBroadcastingTimeout != nil + mode = .liveLocation(peer, activeLiveBroadcastingTimeout != nil) } } - let (pinSize, pinApply) = makePinLayout(item.context.account, item.presentationData.theme.theme, pinPeer, pinLiveLocationActive) + let (pinSize, pinApply) = makePinLayout(item.context, item.presentationData.theme.theme, mode) return (contentProperties, nil, maximumWidth, { constrainedSize, position in let imageCorners: ImageCorners @@ -160,19 +159,22 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { if case let .linear(top, _) = position { relativePosition = .linear(top: top, bottom: ChatMessageBubbleRelativePosition.Neighbour) } - imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: relativePosition, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius) + imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: relativePosition, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius, layoutConstants: layoutConstants, chatPresentationData: item.presentationData) maxTextWidth = constrainedSize.width - bubbleInsets.left + bubbleInsets.right - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - 40.0 } else { maxTextWidth = constrainedSize.width - imageSize.width - bubbleInsets.left + bubbleInsets.right - layoutConstants.text.bubbleInsets.right - imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: position, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius) + imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: position, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius, layoutConstants: layoutConstants, chatPresentationData: item.presentationData) } let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, maxTextWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: max(1.0, maxTextWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var edited = false + if item.attributes.updatingMedia != nil { + edited = true + } var viewCount: Int? for attribute in item.message.attributes { if let attribute = attribute as? EditedMessageAttribute { @@ -212,7 +214,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } else { if item.message.flags.contains(.Failed) { statusType = .BubbleOutgoing(.Failed) - } else if item.message.flags.isSending && !item.message.isSentOrAcknowledged { + } else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil { statusType = .BubbleOutgoing(.Sending) } else { statusType = .BubbleOutgoing(.Sent(read: item.read)) @@ -224,7 +226,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } else { if item.message.flags.contains(.Failed) { statusType = .ImageOutgoing(.Failed) - } else if item.message.flags.isSending && !item.message.isSentOrAcknowledged { + } else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil { statusType = .ImageOutgoing(.Sending) } else { statusType = .ImageOutgoing(.Sent(read: item.read)) @@ -311,14 +313,14 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { if let statusApply = statusApply { if strongSelf.dateAndStatusNode.supernode == nil { - strongSelf.imageNode.addSubnode(strongSelf.dateAndStatusNode) + strongSelf.addSubnode(strongSelf.dateAndStatusNode) } var hasAnimation = true if case .None = animation { hasAnimation = false } statusApply(hasAnimation) - strongSelf.dateAndStatusNode.frame = statusFrame + strongSelf.dateAndStatusNode.frame = statusFrame.offsetBy(dx: imageFrame.minX, dy: imageFrame.minY) } else if strongSelf.dateAndStatusNode.supernode != nil { strongSelf.dateAndStatusNode.removeFromSupernode() } @@ -433,10 +435,10 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } - override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id == messageId, let currentMedia = self.media, currentMedia.isEqual(to: media) { let imageNode = self.imageNode - return (self.imageNode, { [weak imageNode] in + return (self.imageNode, self.imageNode.bounds, { [weak imageNode] in return (imageNode?.view.snapshotContentTree(unhide: true), nil) }) } @@ -458,7 +460,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { return mediaHidden } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction { + override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { return .none } @@ -470,7 +472,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } } - override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? { if !self.dateAndStatusNode.isHidden { return self.dateAndStatusNode.reactionNode(value: value) } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageMediaBubbleContentNode.swift index 8fda443f1f..fec48009c9 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -70,36 +70,41 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { var automaticPlayback: Bool = false var contentMode: InteractiveMediaNodeContentMode = .aspectFit - for media in item.message.media { - if let telegramImage = media as? TelegramMediaImage { - selectedMedia = telegramImage - if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramImage) { - automaticDownload = .full - } - } else if let telegramFile = media as? TelegramMediaFile { - selectedMedia = telegramFile - if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramFile) { - automaticDownload = .full - } else if shouldPredownloadMedia(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, media: telegramFile) { - automaticDownload = .prefetch - } - - if !item.message.containsSecretMedia { - if telegramFile.isAnimated && item.controllerInteraction.automaticMediaDownloadSettings.autoplayGifs { - if case .full = automaticDownload { - automaticPlayback = true - } else { - automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil - } - } else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.controllerInteraction.automaticMediaDownloadSettings.autoplayVideos { - if case .full = automaticDownload { - automaticPlayback = true - } else { - automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil + if let updatingMedia = item.attributes.updatingMedia, case let .update(mediaReference) = updatingMedia.media { + selectedMedia = mediaReference.media + } + if selectedMedia == nil { + for media in item.message.media { + if let telegramImage = media as? TelegramMediaImage { + selectedMedia = telegramImage + if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramImage) { + automaticDownload = .full + } + } else if let telegramFile = media as? TelegramMediaFile { + selectedMedia = telegramFile + if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramFile) { + automaticDownload = .full + } else if shouldPredownloadMedia(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, media: telegramFile) { + automaticDownload = .prefetch + } + + if !item.message.containsSecretMedia { + if telegramFile.isAnimated && item.controllerInteraction.automaticMediaDownloadSettings.autoplayGifs { + if case .full = automaticDownload { + automaticPlayback = true + } else { + automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil + } + } else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.controllerInteraction.automaticMediaDownloadSettings.autoplayVideos { + if case .full = automaticDownload { + automaticPlayback = true + } else { + automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil + } } } + contentMode = .aspectFill } - contentMode = .aspectFill } } @@ -123,8 +128,8 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } else { colors = item.presentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper } - if colors.fill == colors.stroke { - bubbleInsets = UIEdgeInsets() + if colors.fill == colors.stroke || colors.stroke.alpha.isZero { + bubbleInsets = UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) } else { bubbleInsets = layoutConstants.bubble.strokeInsets } @@ -138,7 +143,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { sizeCalculation = .unconstrained } - let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData.theme.theme, item.presentationData.strings, item.presentationData.dateTimeFormat, item.message, selectedMedia!, automaticDownload, item.associatedData.automaticDownloadPeerType, sizeCalculation, layoutConstants, contentMode) + let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData.theme.theme, item.presentationData.strings, item.presentationData.dateTimeFormat, item.message, item.attributes, selectedMedia!, automaticDownload, item.associatedData.automaticDownloadPeerType, sizeCalculation, layoutConstants, contentMode) let forceFullCorners = false let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 7.0, hidesBackground: .emptyWallpaper, forceFullCorners: forceFullCorners, forceAlignment: .none) @@ -154,10 +159,10 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { if forceFullCorners, case .linear = updatedPosition { updatedPosition = .linear(top: .None(.None(.None)), bottom: .None(.None(.None))) } else if hasReplyMarkup, case let .linear(top, _) = updatedPosition { - updatedPosition = .linear(top: top, bottom: .Neighbour) + updatedPosition = .linear(top: top, bottom: .BubbleNeighbour) } - let imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: updatedPosition, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius) + let imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: updatedPosition, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius, layoutConstants: layoutConstants, chatPresentationData: item.presentationData) let (refinedWidth, finishLayout) = refineLayout(CGSize(width: constrainedSize.width - bubbleInsets.left - bubbleInsets.right, height: constrainedSize.height), automaticPlayback, wideLayout, imageCorners) @@ -165,6 +170,9 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { let (imageSize, imageApply) = finishLayout(boundingWidth - bubbleInsets.left - bubbleInsets.right) var edited = false + if item.attributes.updatingMedia != nil { + edited = true + } var viewCount: Int? for attribute in item.message.attributes { if let attribute = attribute as? EditedMessageAttribute { @@ -200,7 +208,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } else { if item.message.flags.contains(.Failed) { statusType = .ImageOutgoing(.Failed) - } else if item.message.flags.isSending && !item.message.isSentOrAcknowledged { + } else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil { statusType = .ImageOutgoing(.Sending) } else { statusType = .ImageOutgoing(.Sent(read: item.read)) @@ -297,7 +305,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } } - override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id == messageId, let currentMedia = self.media, currentMedia.isSemanticallyEqual(to: media) { return self.interactiveImageNode.transitionNode() } @@ -347,7 +355,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { return self.interactiveImageNode.playMediaWithSound() } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction { + override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { return .none } @@ -386,7 +394,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { return false } - override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? { if !self.dateAndStatusNode.isHidden { return self.dateAndStatusNode.reactionNode(value: value) } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageNotificationItem.swift b/submodules/TelegramUI/TelegramUI/ChatMessageNotificationItem.swift index 11b4ca4b7b..882ebf425a 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageNotificationItem.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageNotificationItem.swift @@ -108,9 +108,10 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { } let presentationData = item.context.sharedContext.currentPresentationData.with { $0 } + var isReminder = false + var isScheduled = false var title: String? if let firstMessage = item.messages.first, let peer = messageMainPeer(firstMessage) { - var overrideImage: AvatarNodeImageOverride? if let channel = peer as? TelegramChannel, case .broadcast = channel.info { title = peer.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder) } else if let author = firstMessage.author { @@ -137,14 +138,12 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { if let text = title, firstMessage.flags.contains(.WasScheduled) { if let author = firstMessage.author, author.id == peer.id, author.id == item.context.account.peerId { - title = presentationData.strings.ScheduledMessages_ReminderNotification - overrideImage = .savedMessagesIcon + isReminder = true } else { - title = "📅 \(text)" + isScheduled = true } } - - self.avatarNode.setPeer(account: item.context.account, theme: presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: presentationData.theme.list.mediaPlaceholderColor) + self.avatarNode.setPeer(context: item.context, theme: presentationData.theme, peer: peer, overrideImage: peer.id == item.context.account.peerId ? .savedMessagesIcon : nil, emptyColor: presentationData.theme.list.mediaPlaceholderColor) } var titleIcon: UIImage? @@ -176,7 +175,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { if message.containsSecretMedia { imageDimensions = nil } - messageText = descriptionStringForMessage(message, strings: item.strings, nameDisplayOrder: item.nameDisplayOrder, accountPeerId: item.context.account.peerId).0 + messageText = descriptionStringForMessage(contentSettings: item.context.currentContentSettings.with { $0 }, message: message, strings: item.strings, nameDisplayOrder: item.nameDisplayOrder, accountPeerId: item.context.account.peerId).0 } else if item.messages.count > 1, let peer = item.messages[0].peers[item.messages[0].id.peerId] { var displayAuthor = true if let channel = peer as? TelegramChannel { @@ -194,7 +193,9 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { if let author = item.messages[0].author, displayAuthor { let rawText = presentationData.strings.PUSH_CHAT_MESSAGE_FWDS(Int32(item.messages.count), peer.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder), author.compactDisplayTitle, Int32(item.messages.count)) if let index = rawText.firstIndex(of: "|") { - title = String(rawText[rawText.startIndex ..< index]) + if !isReminder { + title = String(rawText[rawText.startIndex ..< index]) + } messageText = String(rawText[rawText.index(after: index)...]) } else { title = nil @@ -211,9 +212,9 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { } } } else if item.messages[0].groupingKey != nil { - var kind = messageContentKind(item.messages[0], strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId).key + var kind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.messages[0], strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId).key for i in 1 ..< item.messages.count { - let nextKind = messageContentKind(item.messages[i], strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId) + let nextKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.messages[i], strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId) if kind != nextKind.key { kind = .text break @@ -304,6 +305,12 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { messageText = "" } + if isReminder { + title = presentationData.strings.ScheduledMessages_ReminderNotification + } else if isScheduled, let currentTitle = title { + title = "📅 \(currentTitle)" + } + messageText = messageText.replacingOccurrences(of: "\n\n", with: " ") self.titleAttributedText = NSAttributedString(string: title ?? "", font: compact ? Font.semibold(15.0) : Font.semibold(16.0), textColor: presentationData.theme.inAppNotification.primaryTextColor) diff --git a/submodules/TelegramUI/TelegramUI/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessagePollBubbleContentNode.swift index dcd5621f02..8a812fe3e0 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessagePollBubbleContentNode.swift @@ -7,8 +7,12 @@ import SyncCore import Postbox import TextFormat import UrlEscaping +import SwiftSignalKit +import AccountContext +import AvatarNode +import TelegramPresentationData -struct PercentCounterItem: Comparable { +private struct PercentCounterItem: Comparable { var index: Int = 0 var percent: Int = 0 var remainder: Int = 0 @@ -24,7 +28,7 @@ struct PercentCounterItem: Comparable { } -func adjustPercentCount(_ items: [PercentCounterItem], left: Int) -> [PercentCounterItem] { +private func adjustPercentCount(_ items: [PercentCounterItem], left: Int) -> [PercentCounterItem] { var left = left var items = items.sorted(by: <) var i:Int = 0 @@ -84,39 +88,80 @@ func countNicePercent(votes: [Int], total: Int) -> [Int] { } private final class ChatMessagePollOptionRadioNodeParameters: NSObject { + let timestamp: Double let staticColor: UIColor let animatedColor: UIColor + let fillColor: UIColor + let foregroundColor: UIColor let offset: Double? + let isChecked: Bool? + let checkTransition: ChatMessagePollOptionRadioNodeCheckTransition? - init(staticColor: UIColor, animatedColor: UIColor, offset: Double?) { + init(timestamp: Double, staticColor: UIColor, animatedColor: UIColor, fillColor: UIColor, foregroundColor: UIColor, offset: Double?, isChecked: Bool?, checkTransition: ChatMessagePollOptionRadioNodeCheckTransition?) { + self.timestamp = timestamp self.staticColor = staticColor self.animatedColor = animatedColor + self.fillColor = fillColor + self.foregroundColor = foregroundColor self.offset = offset + self.isChecked = isChecked + self.checkTransition = checkTransition super.init() } } +private final class ChatMessagePollOptionRadioNodeCheckTransition { + let startTime: Double + let duration: Double + let previousValue: Bool + let updatedValue: Bool + + init(startTime: Double, duration: Double, previousValue: Bool, updatedValue: Bool) { + self.startTime = startTime + self.duration = duration + self.previousValue = previousValue + self.updatedValue = updatedValue + } +} + private final class ChatMessagePollOptionRadioNode: ASDisplayNode { private(set) var staticColor: UIColor? private(set) var animatedColor: UIColor? + private(set) var fillColor: UIColor? + private(set) var foregroundColor: UIColor? private var isInHierarchyValue: Bool = false private(set) var isAnimating: Bool = false private var startTime: Double? + private var checkTransition: ChatMessagePollOptionRadioNodeCheckTransition? + private(set) var isChecked: Bool? - private var displayLink: CADisplayLink? + private var displayLink: ConstantDisplayLinkAnimator? private var shouldBeAnimating: Bool { - return self.isInHierarchyValue && self.isAnimating + return self.isInHierarchyValue && (self.isAnimating || self.checkTransition != nil) + } + + func updateIsChecked(_ value: Bool, animated: Bool) { + if let previousValue = self.isChecked, previousValue != value { + self.checkTransition = ChatMessagePollOptionRadioNodeCheckTransition(startTime: CACurrentMediaTime(), duration: 0.15, previousValue: previousValue, updatedValue: value) + self.isChecked = value + self.updateAnimating() + self.setNeedsDisplay() + } } override init() { super.init() - self.isLayerBacked = true + self.isUserInteractionEnabled = false self.isOpaque = false } + deinit { + self.displayLink?.isPaused = true + } + override func willEnterHierarchy() { super.willEnterHierarchy() @@ -139,8 +184,10 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { } } - func update(staticColor: UIColor, animatedColor: UIColor, isAnimating: Bool) { + func update(staticColor: UIColor, animatedColor: UIColor, fillColor: UIColor, foregroundColor: UIColor, isSelectable: Bool, isAnimating: Bool) { var updated = false + let shouldHaveBeenAnimating = self.shouldBeAnimating + let wasAnimating = self.isAnimating if !staticColor.isEqual(self.staticColor) { self.staticColor = staticColor updated = true @@ -149,11 +196,27 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { self.animatedColor = animatedColor updated = true } + if !fillColor.isEqual(self.fillColor) { + self.fillColor = fillColor + updated = true + } + if !foregroundColor.isEqual(self.foregroundColor) { + self.foregroundColor = foregroundColor + updated = true + } + if isSelectable != (self.isChecked != nil) { + if isSelectable { + self.isChecked = false + } else { + self.isChecked = nil + self.checkTransition = nil + } + updated = true + } if isAnimating != self.isAnimating { - let previous = self.shouldBeAnimating self.isAnimating = isAnimating let updated = self.shouldBeAnimating - if previous != updated { + if shouldHaveBeenAnimating != updated { self.updateAnimating() } } @@ -163,26 +226,25 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { } private func updateAnimating() { - if self.shouldBeAnimating { - self.startTime = CACurrentMediaTime() - if self.displayLink == nil { - class DisplayLinkProxy: NSObject { - var f: () -> Void - init(_ f: @escaping () -> Void) { - self.f = f - } - - @objc func displayLinkEvent() { - self.f() - } - } - let displayLink = CADisplayLink(target: DisplayLinkProxy({ [weak self] in - self?.setNeedsDisplay() - }), selector: #selector(DisplayLinkProxy.displayLinkEvent)) - displayLink.add(to: .main, forMode: .common) - self.displayLink = displayLink + let timestamp = CACurrentMediaTime() + if let checkTransition = self.checkTransition { + if checkTransition.startTime + checkTransition.duration <= timestamp { + self.checkTransition = nil + } + } + + if self.shouldBeAnimating { + if self.isAnimating && self.startTime == nil { + self.startTime = timestamp + } + if self.displayLink == nil { + self.displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.updateAnimating() + self?.setNeedsDisplay() + }) + self.displayLink?.isPaused = false + self.setNeedsDisplay() } - self.setNeedsDisplay() } else if let displayLink = self.displayLink { self.startTime = nil displayLink.invalidate() @@ -192,12 +254,13 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { } override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - if let staticColor = self.staticColor, let animatedColor = self.animatedColor { + if let staticColor = self.staticColor, let animatedColor = self.animatedColor, let fillColor = self.fillColor, let foregroundColor = self.foregroundColor { + let timestamp = CACurrentMediaTime() var offset: Double? if let startTime = self.startTime { offset = CACurrentMediaTime() - startTime } - return ChatMessagePollOptionRadioNodeParameters(staticColor: staticColor, animatedColor: animatedColor, offset: offset) + return ChatMessagePollOptionRadioNodeParameters(timestamp: timestamp, staticColor: staticColor, animatedColor: animatedColor, fillColor: fillColor, foregroundColor: foregroundColor, offset: offset, isChecked: self.isChecked, checkTransition: self.checkTransition) } else { return nil } @@ -276,20 +339,93 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { } } } else { - context.setStrokeColor(parameters.staticColor.cgColor) - context.strokeEllipse(in: CGRect(origin: CGPoint(x: 0.5, y: 0.5), size: CGSize(width: bounds.width - 1.0, height: bounds.height - 1.0))) + if let isChecked = parameters.isChecked { + let checkedT: CGFloat + let fromValue: CGFloat + let toValue: CGFloat + let fromAlpha: CGFloat + let toAlpha: CGFloat + if let checkTransition = parameters.checkTransition { + checkedT = CGFloat(max(0.0, min(1.0, (parameters.timestamp - checkTransition.startTime) / checkTransition.duration))) + fromValue = checkTransition.previousValue ? bounds.width : 0.0 + fromAlpha = checkTransition.previousValue ? 1.0 : 0.0 + toValue = checkTransition.updatedValue ? bounds.width : 0.0 + toAlpha = checkTransition.updatedValue ? 1.0 : 0.0 + } else { + checkedT = 1.0 + fromValue = isChecked ? bounds.width : 0.0 + fromAlpha = isChecked ? 1.0 : 0.0 + toValue = isChecked ? bounds.width : 0.0 + toAlpha = isChecked ? 1.0 : 0.0 + } + + let diameter = fromValue * (1.0 - checkedT) + toValue * checkedT + let alpha = fromAlpha * (1.0 - checkedT) + toAlpha * checkedT + + if abs(diameter - 1.0) > CGFloat.ulpOfOne { + context.setStrokeColor(parameters.staticColor.cgColor) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: 0.5, y: 0.5), size: CGSize(width: bounds.width - 1.0, height: bounds.height - 1.0))) + } + + if !diameter.isZero { + context.setFillColor(parameters.fillColor.withAlphaComponent(alpha).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: (bounds.width - diameter) / 2.0, y: (bounds.width - diameter) / 2.0), size: CGSize(width: diameter, height: diameter))) + + context.setLineWidth(1.5) + context.setLineJoin(.round) + context.setLineCap(.round) + + context.setStrokeColor(parameters.foregroundColor.withAlphaComponent(alpha).cgColor) + if parameters.foregroundColor.alpha.isZero { + context.setBlendMode(.clear) + } + let startPoint = CGPoint(x: 6.0, y: 12.13) + let centerPoint = CGPoint(x: 9.28, y: 15.37) + let endPoint = CGPoint(x: 16.0, y: 8.0) + + let pathStartT: CGFloat = 0.15 + let pathT = max(0.0, (alpha - pathStartT) / (1.0 - pathStartT)) + let pathMiddleT: CGFloat = 0.4 + + context.move(to: startPoint) + if pathT >= pathMiddleT { + context.addLine(to: centerPoint) + + let pathEndT = (pathT - pathMiddleT) / (1.0 - pathMiddleT) + if pathEndT >= 1.0 { + context.addLine(to: endPoint) + } else { + context.addLine(to: CGPoint(x: (1.0 - pathEndT) * centerPoint.x + pathEndT * endPoint.x, y: (1.0 - pathEndT) * centerPoint.y + pathEndT * endPoint.y)) + } + } else { + context.addLine(to: CGPoint(x: (1.0 - pathT) * startPoint.x + pathT * centerPoint.x, y: (1.0 - pathT) * startPoint.y + pathT * centerPoint.y)) + } + context.strokePath() + context.setBlendMode(.normal) + } + } else { + context.setStrokeColor(parameters.staticColor.cgColor) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: 0.5, y: 0.5), size: CGSize(width: bounds.width - 1.0, height: bounds.height - 1.0))) + } } } } private let percentageFont = Font.bold(14.5) +private let percentageSmallFont = Font.bold(12.5) -private func generatePercentageImage(presentationData: ChatPresentationData, incoming: Bool, value: Int) -> UIImage { +private func generatePercentageImage(presentationData: ChatPresentationData, incoming: Bool, value: Int, targetValue: Int) -> UIImage { return generateImage(CGSize(width: 42.0, height: 20.0), rotatedContext: { size, context in UIGraphicsPushContext(context) context.clear(CGRect(origin: CGPoint(), size: size)) - let string = NSAttributedString(string: "\(value)%", font: percentageFont, textColor: incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor, paragraphAlignment: .right) - string.draw(in: CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: size)) + let font: UIFont + if targetValue == 100 { + font = percentageSmallFont + } else { + font = percentageFont + } + let string = NSAttributedString(string: "\(value)%", font: font, textColor: incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor, paragraphAlignment: .right) + string.draw(in: CGRect(origin: CGPoint(x: 0.0, y: targetValue == 100 ? 3.0 : 2.0), size: size)) UIGraphicsPopContext() })! } @@ -300,7 +436,7 @@ private func generatePercentageAnimationImages(presentationData: ChatPresentatio var images: [UIImage] = [] for i in 0 ..< numberOfFrames { let t = CGFloat(i) / CGFloat(numberOfFrames) - images.append(generatePercentageImage(presentationData: presentationData, incoming: incoming, value: Int((1.0 - t) * CGFloat(fromValue) + t * CGFloat(toValue)))) + images.append(generatePercentageImage(presentationData: presentationData, incoming: incoming, value: Int((1.0 - t) * CGFloat(fromValue) + t * CGFloat(toValue)), targetValue: toValue)) } return images } @@ -311,19 +447,27 @@ private struct ChatMessagePollOptionResult: Equatable { let count: Int32 } +private struct ChatMessagePollOptionSelection: Equatable { + var isSelected: Bool + var isCorrect: Bool +} + private final class ChatMessagePollOptionNode: ASDisplayNode { private let highlightedBackgroundNode: ASDisplayNode - private var radioNode: ChatMessagePollOptionRadioNode? + private(set) var radioNode: ChatMessagePollOptionRadioNode? private let percentageNode: ASDisplayNode private var percentageImage: UIImage? private var titleNode: TextNode? private let buttonNode: HighlightTrackingButtonNode private let separatorNode: ASDisplayNode private let resultBarNode: ASImageNode - + private let resultBarIconNode: ASImageNode var option: TelegramMediaPollOption? - public private(set) var currentResult: ChatMessagePollOptionResult? + private(set) var currentResult: ChatMessagePollOptionResult? + private(set) var currentSelection: ChatMessagePollOptionSelection? var pressed: (() -> Void)? + var selectionUpdated: (() -> Void)? + private var theme: PresentationTheme? override init() { self.highlightedBackgroundNode = ASDisplayNode() @@ -338,6 +482,9 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { self.resultBarNode.isLayerBacked = true self.resultBarNode.alpha = 0.0 + self.resultBarIconNode = ASImageNode() + self.resultBarIconNode.isLayerBacked = true + self.percentageNode = ASDisplayNode() self.percentageNode.alpha = 0.0 self.percentageNode.isLayerBacked = true @@ -347,6 +494,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { self.addSubnode(self.highlightedBackgroundNode) self.addSubnode(self.separatorNode) self.addSubnode(self.resultBarNode) + self.addSubnode(self.resultBarIconNode) self.addSubnode(self.percentageNode) self.addSubnode(self.buttonNode) @@ -365,14 +513,21 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } @objc private func buttonPressed() { - self.pressed?() + if let radioNode = self.radioNode, let isChecked = radioNode.isChecked { + radioNode.updateIsChecked(!isChecked, animated: true) + self.selectionUpdated?() + } else { + self.pressed?() + } } - static func asyncLayout(_ maybeNode: ChatMessagePollOptionNode?) -> (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode))) { + static func asyncLayout(_ maybeNode: ChatMessagePollOptionNode?) -> (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode))) { let makeTitleLayout = TextNode.asyncLayout(maybeNode?.titleNode) let currentResult = maybeNode?.currentResult + let currentSelection = maybeNode?.currentSelection + let currentTheme = maybeNode?.theme - return { accountPeerId, presentationData, message, option, optionResult, constrainedWidth in + return { accountPeerId, presentationData, message, poll, option, optionResult, constrainedWidth in let leftInset: CGFloat = 50.0 let rightInset: CGFloat = 12.0 @@ -383,10 +538,88 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { let contentHeight: CGFloat = max(46.0, titleLayout.size.height + 22.0) let shouldHaveRadioNode = optionResult == nil + let isSelectable: Bool + if shouldHaveRadioNode, case .poll(multipleAnswers: true) = poll.kind, !Namespaces.Message.allScheduled.contains(message.id.namespace) { + isSelectable = true + } else { + isSelectable = false + } + + let themeUpdated = presentationData.theme.theme !== currentTheme var updatedPercentageImage: UIImage? - if currentResult != optionResult { - updatedPercentageImage = generatePercentageImage(presentationData: presentationData, incoming: incoming, value: optionResult?.percent ?? 0) + if currentResult != optionResult || themeUpdated { + let value = optionResult?.percent ?? 0 + updatedPercentageImage = generatePercentageImage(presentationData: presentationData, incoming: incoming, value: value, targetValue: value) + } + + var resultIcon: UIImage? + var updatedResultIcon = false + + var selection: ChatMessagePollOptionSelection? + if optionResult != nil { + if let voters = poll.results.voters { + for voter in voters { + if voter.opaqueIdentifier == option.opaqueIdentifier { + if voter.selected || voter.isCorrect { + selection = ChatMessagePollOptionSelection(isSelected: voter.selected, isCorrect: voter.isCorrect) + } + break + } + } + } + } + if selection != currentSelection || themeUpdated { + updatedResultIcon = true + if let selection = selection { + var isQuiz = false + if case .quiz = poll.kind { + isQuiz = true + } + resultIcon = generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + var isIncorrect = false + let fillColor: UIColor + if selection.isSelected { + if isQuiz { + if selection.isCorrect { + fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barPositive : presentationData.theme.theme.chat.message.outgoing.polls.barPositive + } else { + fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barNegative : presentationData.theme.theme.chat.message.outgoing.polls.barNegative + isIncorrect = true + } + } else { + fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar + } + } else if isQuiz && selection.isCorrect { + fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar + } else { + fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar + } + context.setFillColor(fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + let strokeColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barIconForeground : presentationData.theme.theme.chat.message.outgoing.polls.barIconForeground + if strokeColor.alpha.isZero { + context.setBlendMode(.copy) + } + context.setStrokeColor(strokeColor.cgColor) + context.setLineWidth(1.5) + context.setLineJoin(.round) + context.setLineCap(.round) + if isIncorrect { + context.translateBy(x: 5.0, y: 5.0) + context.move(to: CGPoint(x: 0.0, y: 6.0)) + context.addLine(to: CGPoint(x: 6.0, y: 0.0)) + context.strokePath() + context.move(to: CGPoint(x: 0.0, y: 0.0)) + context.addLine(to: CGPoint(x: 6.0, y: 6.0)) + context.strokePath() + } else { + let _ = try? drawSvgPath(context, path: "M4,8.5 L6.44778395,10.9477839 C6.47662208,10.9766221 6.52452135,10.9754786 6.54754782,10.9524522 L12,5.5 S ") + } + }) + } } return (titleLayout.size.width + leftInset + rightInset, { width in @@ -401,6 +634,8 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { node.option = option let previousResult = node.currentResult node.currentResult = optionResult + node.currentSelection = selection + node.theme = presentationData.theme.theme node.highlightedBackgroundNode.backgroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.highlight : presentationData.theme.theme.chat.message.outgoing.polls.highlight @@ -432,7 +667,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } let radioSize: CGFloat = 22.0 radioNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 12.0), size: CGSize(width: radioSize, height: radioSize)) - radioNode.update(staticColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioButton : presentationData.theme.theme.chat.message.outgoing.polls.radioButton, animatedColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioProgress : presentationData.theme.theme.chat.message.outgoing.polls.radioProgress, isAnimating: inProgress) + radioNode.update(staticColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioButton : presentationData.theme.theme.chat.message.outgoing.polls.radioButton, animatedColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioProgress : presentationData.theme.theme.chat.message.outgoing.polls.radioProgress, fillColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar, foregroundColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.barIconForeground : presentationData.theme.theme.chat.message.outgoing.polls.barIconForeground, isSelectable: isSelectable, isAnimating: inProgress) } else if let radioNode = node.radioNode { node.radioNode = nil if animated { @@ -469,26 +704,60 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { node.separatorNode.backgroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.separator : presentationData.theme.theme.chat.message.outgoing.polls.separator node.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel)) - if node.resultBarNode.image == nil { - node.resultBarNode.image = generateStretchableFilledCircleImage(diameter: 6.0, color: incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar) + if node.resultBarNode.image == nil || updatedResultIcon { + var isQuiz = false + if case .quiz = poll.kind { + isQuiz = true + } + let fillColor: UIColor + if let selection = selection { + if selection.isSelected { + if isQuiz { + if selection.isCorrect { + fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barPositive : presentationData.theme.theme.chat.message.outgoing.polls.barPositive + } else { + fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barNegative : presentationData.theme.theme.chat.message.outgoing.polls.barNegative + } + } else { + fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar + } + } else if isQuiz && selection.isCorrect { + fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar + } else { + fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar + } + } else { + fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar + } + + node.resultBarNode.image = generateStretchableFilledCircleImage(diameter: 6.0, color: fillColor) + } + + if updatedResultIcon { + node.resultBarIconNode.image = resultIcon } let minBarWidth: CGFloat = 6.0 let resultBarWidth = minBarWidth + floor((width - leftInset - rightInset - minBarWidth) * (optionResult?.normalized ?? 0.0)) - node.resultBarNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - 6.0 - 1.0), size: CGSize(width: resultBarWidth, height: 6.0)) + let barFrame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - 6.0 - 1.0), size: CGSize(width: resultBarWidth, height: 6.0)) + node.resultBarNode.frame = barFrame + node.resultBarIconNode.frame = CGRect(origin: CGPoint(x: barFrame.minX - 6.0 - 16.0, y: barFrame.minY + floor((barFrame.height - 16.0) / 2.0)), size: CGSize(width: 16.0, height: 16.0)) node.resultBarNode.alpha = optionResult != nil ? 1.0 : 0.0 node.percentageNode.alpha = optionResult != nil ? 1.0 : 0.0 node.separatorNode.alpha = optionResult == nil ? 1.0 : 0.0 + node.resultBarIconNode.alpha = optionResult != nil ? 1.0 : 0.0 if animated, currentResult != optionResult { if (currentResult != nil) != (optionResult != nil) { if optionResult != nil { node.resultBarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) node.percentageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) node.separatorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08) + node.resultBarIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } else { node.resultBarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4) node.percentageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) node.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + node.resultBarIconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) } } @@ -513,10 +782,17 @@ private let labelsFont = Font.regular(14.0) class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { private let textNode: TextNode private let typeNode: TextNode + private let avatarsNode: MergedAvatarsNode private let votersNode: TextNode + private let buttonSubmitInactiveTextNode: TextNode + private let buttonSubmitActiveTextNode: TextNode + private let buttonViewResultsTextNode: TextNode + private let buttonNode: HighlightableButtonNode private let statusNode: ChatMessageDateAndStatusNode private var optionNodes: [ChatMessagePollOptionNode] = [] + private var poll: TelegramMediaPoll? + required init() { self.textNode = TextNode() self.textNode.isUserInteractionEnabled = false @@ -530,29 +806,106 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { self.typeNode.contentsScale = UIScreenScale self.typeNode.displaysAsynchronously = true + self.avatarsNode = MergedAvatarsNode() + self.votersNode = TextNode() self.votersNode.isUserInteractionEnabled = false self.votersNode.contentMode = .topLeft self.votersNode.contentsScale = UIScreenScale self.votersNode.displaysAsynchronously = true + self.buttonSubmitInactiveTextNode = TextNode() + self.buttonSubmitInactiveTextNode.isUserInteractionEnabled = false + self.buttonSubmitInactiveTextNode.contentMode = .topLeft + self.buttonSubmitInactiveTextNode.contentsScale = UIScreenScale + self.buttonSubmitInactiveTextNode.displaysAsynchronously = true + + self.buttonSubmitActiveTextNode = TextNode() + self.buttonSubmitActiveTextNode.isUserInteractionEnabled = false + self.buttonSubmitActiveTextNode.contentMode = .topLeft + self.buttonSubmitActiveTextNode.contentsScale = UIScreenScale + self.buttonSubmitActiveTextNode.displaysAsynchronously = true + + self.buttonViewResultsTextNode = TextNode() + self.buttonViewResultsTextNode.isUserInteractionEnabled = false + self.buttonViewResultsTextNode.contentMode = .topLeft + self.buttonViewResultsTextNode.contentsScale = UIScreenScale + self.buttonViewResultsTextNode.displaysAsynchronously = true + + self.buttonNode = HighlightableButtonNode() + self.statusNode = ChatMessageDateAndStatusNode() super.init() self.addSubnode(self.textNode) self.addSubnode(self.typeNode) + self.addSubnode(self.avatarsNode) self.addSubnode(self.votersNode) + self.addSubnode(self.buttonSubmitInactiveTextNode) + self.addSubnode(self.buttonSubmitActiveTextNode) + self.addSubnode(self.buttonViewResultsTextNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.buttonSubmitActiveTextNode.layer.removeAnimation(forKey: "opacity") + strongSelf.buttonSubmitActiveTextNode.alpha = 0.6 + strongSelf.buttonViewResultsTextNode.layer.removeAnimation(forKey: "opacity") + strongSelf.buttonViewResultsTextNode.alpha = 0.6 + } else { + strongSelf.buttonSubmitActiveTextNode.alpha = 1.0 + strongSelf.buttonSubmitActiveTextNode.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.3) + strongSelf.buttonViewResultsTextNode.alpha = 1.0 + strongSelf.buttonViewResultsTextNode.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.3) + } + } + } + + self.avatarsNode.pressed = { [weak self] in + self?.buttonPressed() + } } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + @objc private func buttonPressed() { + guard let item = self.item, let poll = self.poll, let pollId = poll.id else { + return + } + + var hasSelection = false + var selectedOpaqueIdentifiers: [Data] = [] + for optionNode in self.optionNodes { + if let option = optionNode.option { + if let isChecked = optionNode.radioNode?.isChecked { + hasSelection = true + if isChecked { + selectedOpaqueIdentifiers.append(option.opaqueIdentifier) + } + } + } + } + if !hasSelection { + if !Namespaces.Message.allScheduled.contains(item.message.id.namespace) { + item.controllerInteraction.requestOpenMessagePollResults(item.message.id, pollId) + } + } else if !selectedOpaqueIdentifiers.isEmpty { + item.controllerInteraction.requestSelectMessagePollOptions(item.message.id, selectedOpaqueIdentifiers) + } + } + override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { let makeTextLayout = TextNode.asyncLayout(self.textNode) let makeTypeLayout = TextNode.asyncLayout(self.typeNode) let makeVotersLayout = TextNode.asyncLayout(self.votersNode) + let makeSubmitInactiveTextLayout = TextNode.asyncLayout(self.buttonSubmitInactiveTextNode) + let makeSubmitActiveTextLayout = TextNode.asyncLayout(self.buttonSubmitActiveTextNode) + let makeViewResultsTextLayout = TextNode.asyncLayout(self.buttonViewResultsTextNode) let statusLayout = self.statusNode.asyncLayout() var previousPoll: TelegramMediaPoll? @@ -564,11 +917,15 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } - var previousOptionNodeLayouts: [Data: (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)))] = [:] + var previousOptionNodeLayouts: [Data: (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)))] = [:] + var hasSelectedOptions = false for optionNode in self.optionNodes { if let option = optionNode.option { previousOptionNodeLayouts[option.opaqueIdentifier] = ChatMessagePollOptionNode.asyncLayout(optionNode) } + if let isChecked = optionNode.radioNode?.isChecked, isChecked { + hasSelectedOptions = true + } } return { item, layoutConstants, _, _, _ in @@ -583,6 +940,9 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset, height: constrainedSize.height) var edited = false + if item.attributes.updatingMedia != nil { + edited = true + } var viewCount: Int? for attribute in item.message.attributes { if let attribute = attribute as? EditedMessageAttribute { @@ -615,7 +975,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } else { if message.flags.contains(.Failed) { statusType = .BubbleOutgoing(.Failed) - } else if message.flags.isSending && !message.isSentOrAcknowledged { + } else if (message.flags.isSending && !message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil { statusType = .BubbleOutgoing(.Sending) } else { statusType = .BubbleOutgoing(.Sent(read: item.read)) @@ -650,25 +1010,65 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) let typeText: String + + var avatarPeers: [Peer] = [] + if let poll = poll { + for peerId in poll.results.recentVoters { + if let peer = item.message.peers[peerId] { + avatarPeers.append(peer) + } + } + } + if let poll = poll, poll.isClosed { typeText = item.presentationData.strings.MessagePoll_LabelClosed + } else if let poll = poll { + switch poll.kind { + case .poll: + switch poll.publicity { + case .anonymous: + typeText = item.presentationData.strings.MessagePoll_LabelAnonymous + case .public: + typeText = item.presentationData.strings.MessagePoll_LabelPoll + } + case .quiz: + switch poll.publicity { + case .anonymous: + typeText = item.presentationData.strings.MessagePoll_LabelAnonymousQuiz + case .public: + typeText = item.presentationData.strings.MessagePoll_LabelQuiz + } + } } else { typeText = item.presentationData.strings.MessagePoll_LabelAnonymous } let (typeLayout, typeApply) = makeTypeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: typeText, font: labelsFont, textColor: messageTheme.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let votersString: String - if let totalVoters = poll?.results.totalVoters { - if totalVoters == 0 { - votersString = item.presentationData.strings.MessagePoll_NoVotes - } else { - votersString = item.presentationData.strings.MessagePoll_VotedCount(totalVoters) + if let poll = poll, let totalVoters = poll.results.totalVoters { + switch poll.kind { + case .poll: + if totalVoters == 0 { + votersString = item.presentationData.strings.MessagePoll_NoVotes + } else { + votersString = item.presentationData.strings.MessagePoll_VotedCount(totalVoters) + } + case .quiz: + if totalVoters == 0 { + votersString = item.presentationData.strings.MessagePoll_QuizNoUsers + } else { + votersString = item.presentationData.strings.MessagePoll_QuizCount(totalVoters) + } } } else { votersString = " " } let (votersLayout, votersApply) = makeVotersLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: votersString, font: labelsFont, textColor: messageTheme.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) + let (buttonSubmitInactiveTextLayout, buttonSubmitInactiveTextApply) = makeSubmitInactiveTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.MessagePoll_SubmitVote, font: Font.regular(17.0), textColor: messageTheme.accentControlDisabledColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) + let (buttonSubmitActiveTextLayout, buttonSubmitActiveTextApply) = makeSubmitActiveTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.MessagePoll_SubmitVote, font: Font.regular(17.0), textColor: messageTheme.polls.bar), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) + let (buttonViewResultsTextLayout, buttonViewResultsTextApply) = makeViewResultsTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.MessagePoll_ViewResults, font: Font.regular(17.0), textColor: messageTheme.polls.bar), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) + var textFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), size: textLayout.size) var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom)) @@ -684,6 +1084,8 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { var boundingSize: CGSize = textFrameWithoutInsets.size boundingSize.width = max(boundingSize.width, typeLayout.size.width) boundingSize.width = max(boundingSize.width, votersLayout.size.width + 4.0 + (statusSize?.width ?? 0.0)) + boundingSize.width = max(boundingSize.width, buttonSubmitInactiveTextLayout.size.width + 4.0 + (statusSize?.width ?? 0.0)) + boundingSize.width = max(boundingSize.width, buttonViewResultsTextLayout.size.width + 4.0 + (statusSize?.width ?? 0.0)) boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom @@ -730,7 +1132,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { for i in 0 ..< poll.options.count { let option = poll.options[i] - let makeLayout: (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode))) + let makeLayout: (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode))) if let previous = previousOptionNodeLayouts[option.opaqueIdentifier] { makeLayout = previous } else { @@ -746,7 +1148,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } else if poll.isClosed { optionResult = ChatMessagePollOptionResult(normalized: 0, percent: 0, count: 0) } - let result = makeLayout(item.context.account.peerId, item.presentationData, item.message, option, optionResult, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0) + let result = makeLayout(item.context.account.peerId, item.presentationData, item.message, poll, option, optionResult, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0) boundingSize.width = max(boundingSize.width, result.minimumWidth + layoutConstants.bubble.borderInset * 2.0) pollOptionsFinalizeLayouts.append(result.1) } @@ -755,7 +1157,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { boundingSize.width = max(boundingSize.width, min(270.0, constrainedSize.width)) var canVote = false - if item.message.id.namespace == Namespaces.Message.Cloud, let poll = poll, poll.pollId.namespace == Namespaces.Media.CloudPoll, !poll.isClosed { + if (item.message.id.namespace == Namespaces.Message.Cloud || Namespaces.Message.allScheduled.contains(item.message.id.namespace)), let poll = poll, poll.pollId.namespace == Namespaces.Media.CloudPoll, !poll.isClosed { var hasVoted = false if let voters = poll.results.voters { for voter in voters { @@ -769,9 +1171,6 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { canVote = true } } - if Namespaces.Message.allScheduled.contains(item.message.id.namespace) { - canVote = true - } return (boundingSize.width, { boundingWidth in var resultSize = CGSize(width: max(boundingSize.width, boundingWidth), height: boundingSize.height) @@ -782,24 +1181,35 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { var optionNodesSizesAndApply: [(CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)] = [] for finalizeLayout in pollOptionsFinalizeLayouts { - let result = finalizeLayout(boundingWidth - layoutConstants.bubble.borderInset * 2.0) + let result = finalizeLayout(boundingWidth - layoutConstants.bubble.borderInset * 2.0) resultSize.width = max(resultSize.width, result.0.width + layoutConstants.bubble.borderInset * 2.0) resultSize.height += result.0.height optionNodesSizesAndApply.append(result) } let optionsVotersSpacing: CGFloat = 11.0 - let votersBottomSpacing: CGFloat = 8.0 + let optionsButtonSpacing: CGFloat = 9.0 + let votersBottomSpacing: CGFloat = 11.0 resultSize.height += optionsVotersSpacing + votersLayout.size.height + votersBottomSpacing + let buttonSubmitInactiveTextFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonSubmitInactiveTextLayout.size.width) / 2.0), y: optionsButtonSpacing), size: buttonSubmitInactiveTextLayout.size) + let buttonSubmitActiveTextFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonSubmitActiveTextLayout.size.width) / 2.0), y: optionsButtonSpacing), size: buttonSubmitActiveTextLayout.size) + let buttonViewResultsTextFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonViewResultsTextLayout.size.width) / 2.0), y: optionsButtonSpacing), size: buttonViewResultsTextLayout.size) + var adjustedStatusFrame: CGRect? if let statusFrame = statusFrame { - adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - layoutConstants.text.bubbleInsets.right, y: resultSize.height - statusFrame.size.height - 6.0), size: statusFrame.size) + var localStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - layoutConstants.text.bubbleInsets.right, y: resultSize.height - statusFrame.size.height - 6.0), size: statusFrame.size) + if localStatusFrame.minX <= buttonViewResultsTextFrame.maxX || localStatusFrame.minX <= buttonSubmitActiveTextFrame.maxX { + localStatusFrame.origin.y += 10.0 + resultSize.height += 10.0 + } + adjustedStatusFrame = localStatusFrame } - return (resultSize, { [weak self] animation, _ in + return (resultSize, { [weak self] animation, synchronousLoad in if let strongSelf = self { strongSelf.item = item + strongSelf.poll = poll let cachedLayout = strongSelf.textNode.cachedLayout @@ -831,7 +1241,9 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { let (size, apply) = optionNodesSizesAndApply[i] var isRequesting = false if let poll = poll, i < poll.options.count { - isRequesting = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] == poll.options[i].opaqueIdentifier + if let inProgressOpaqueIds = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] { + isRequesting = inProgressOpaqueIds.contains(poll.options[i].opaqueIdentifier) + } } let optionNode = apply(animation.isAnimated, isRequesting) if optionNode.supernode !== strongSelf { @@ -842,7 +1254,13 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { return } - item.controllerInteraction.requestSelectMessagePollOption(item.message.id, option.opaqueIdentifier) + item.controllerInteraction.requestSelectMessagePollOptions(item.message.id, [option.opaqueIdentifier]) + } + optionNode.selectionUpdated = { + guard let strongSelf = self else { + return + } + strongSelf.updateSelection() } } optionNode.frame = CGRect(origin: CGPoint(x: layoutConstants.bubble.borderInset, y: verticalOffset), size: size) @@ -884,15 +1302,40 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } else { strongSelf.textNode.frame = textFrame } - strongSelf.typeNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + titleTypeSpacing), size: typeLayout.size) + let typeFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + titleTypeSpacing), size: typeLayout.size) + strongSelf.typeNode.frame = typeFrame + let avatarsFrame = CGRect(origin: CGPoint(x: typeFrame.maxX + 6.0, y: typeFrame.minY + floor((typeFrame.height - mergedImageSize) / 2.0)), size: CGSize(width: mergedImageSize + mergedImageSpacing * 2.0, height: mergedImageSize)) + strongSelf.avatarsNode.frame = avatarsFrame + strongSelf.avatarsNode.updateLayout(size: avatarsFrame.size) + strongSelf.avatarsNode.update(context: item.context, peers: avatarPeers, synchronousLoad: synchronousLoad) + let alphaTransition: ContainedViewLayoutTransition + if animation.isAnimated { + alphaTransition = .animated(duration: 0.25, curve: .easeInOut) + alphaTransition.updateAlpha(node: strongSelf.avatarsNode, alpha: avatarPeers.isEmpty ? 0.0 : 1.0) + } else { + alphaTransition = .immediate + } let _ = votersApply() - strongSelf.votersNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: verticalOffset + optionsVotersSpacing), size: votersLayout.size) + strongSelf.votersNode.frame = CGRect(origin: CGPoint(x: floor((resultSize.width - votersLayout.size.width) / 2.0), y: verticalOffset + optionsVotersSpacing), size: votersLayout.size) if animation.isAnimated, let previousPoll = previousPoll, let poll = poll { if previousPoll.results.totalVoters == nil && poll.results.totalVoters != nil { strongSelf.votersNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } } + + let _ = buttonSubmitInactiveTextApply() + strongSelf.buttonSubmitInactiveTextNode.frame = buttonSubmitInactiveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset) + + let _ = buttonSubmitActiveTextApply() + strongSelf.buttonSubmitActiveTextNode.frame = buttonSubmitActiveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset) + + let _ = buttonViewResultsTextApply() + strongSelf.buttonViewResultsTextNode.frame = buttonViewResultsTextFrame.offsetBy(dx: 0.0, dy: verticalOffset) + + strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: resultSize.width, height: 44.0)) + + strongSelf.updateSelection() } }) }) @@ -900,6 +1343,81 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } + private func updateSelection() { + guard let item = self.item, let poll = self.poll else { + return + } + + let disableAllActions = false + + var hasSelection = false + switch poll.kind { + case .poll(true): + hasSelection = true + default: + break + } + + var hasSelectedOptions = false + for optionNode in self.optionNodes { + if let isChecked = optionNode.radioNode?.isChecked { + if isChecked { + hasSelectedOptions = true + } + } + } + + var hasResults = false + if poll.isClosed { + hasResults = true + hasSelection = false + if let totalVoters = poll.results.totalVoters, totalVoters == 0 { + hasResults = false + } + } else { + if let totalVoters = poll.results.totalVoters, totalVoters != 0 { + if let voters = poll.results.voters { + for voter in voters { + if voter.selected { + hasResults = true + break + } + } + } + } + } + + if !disableAllActions && hasSelection && !hasResults && poll.pollId.namespace == Namespaces.Media.CloudPoll { + self.votersNode.isHidden = true + self.buttonViewResultsTextNode.isHidden = true + self.buttonSubmitInactiveTextNode.isHidden = hasSelectedOptions + self.buttonSubmitActiveTextNode.isHidden = !hasSelectedOptions + self.buttonNode.isHidden = !hasSelectedOptions + self.buttonNode.isUserInteractionEnabled = true + } else { + if case .public = poll.publicity, hasResults, !disableAllActions { + self.votersNode.isHidden = true + self.buttonViewResultsTextNode.isHidden = false + self.buttonNode.isHidden = false + + if Namespaces.Message.allScheduled.contains(item.message.id.namespace) { + self.buttonNode.isUserInteractionEnabled = false + } else { + self.buttonNode.isUserInteractionEnabled = true + } + } else { + self.votersNode.isHidden = false + self.buttonViewResultsTextNode.isHidden = true + self.buttonNode.isHidden = true + self.buttonNode.isUserInteractionEnabled = true + } + self.buttonSubmitInactiveTextNode.isHidden = true + self.buttonSubmitActiveTextNode.isHidden = true + } + + self.avatarsNode.isUserInteractionEnabled = !self.buttonViewResultsTextNode.isHidden + } + override func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -915,7 +1433,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction { + override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.textNode.frame if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { @@ -937,28 +1455,247 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } else { for optionNode in self.optionNodes { - if optionNode.frame.contains(point) { + if optionNode.frame.contains(point), case .tap = gesture { if optionNode.isUserInteractionEnabled { return .ignore - } else if let result = optionNode.currentResult, let item = self.item { - let string: String - if result.count == 0 { - string = item.presentationData.strings.MessagePoll_NoVotes - } else { - string = item.presentationData.strings.MessagePoll_VotedCount(result.count) + } else if let result = optionNode.currentResult, let item = self.item, !Namespaces.Message.allScheduled.contains(item.message.id.namespace), let poll = self.poll, let option = optionNode.option { + switch poll.publicity { + case .anonymous: + let string: String + switch poll.kind { + case .poll: + if result.count == 0 { + string = item.presentationData.strings.MessagePoll_NoVotes + } else { + string = item.presentationData.strings.MessagePoll_VotedCount(result.count) + } + case .quiz: + if result.count == 0 { + string = item.presentationData.strings.MessagePoll_QuizNoUsers + } else { + string = item.presentationData.strings.MessagePoll_QuizCount(result.count) + } + } + return .tooltip(string, optionNode, optionNode.bounds.offsetBy(dx: 0.0, dy: 10.0)) + case .public: + var hasNonZeroVoters = false + if let voters = poll.results.voters { + for voter in voters { + if voter.count != 0 { + hasNonZeroVoters = true + break + } + } + } + if hasNonZeroVoters { + if !isEstimating { + item.controllerInteraction.openMessagePollResults(item.message.id, option.opaqueIdentifier) + return .ignore + } + return .openMessage + } } - return .tooltip(string, optionNode, optionNode.bounds.offsetBy(dx: 0.0, dy: 10.0)) } } } + if self.buttonNode.isUserInteractionEnabled, !self.buttonNode.isHidden, self.buttonNode.frame.contains(point) { + return .ignore + } + if self.avatarsNode.isUserInteractionEnabled, !self.avatarsNode.isHidden, self.avatarsNode.frame.contains(point) { + return .ignore + } return .none } } - override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? { if !self.statusNode.isHidden { return self.statusNode.reactionNode(value: value) } return nil } } + +private enum PeerAvatarReference: Equatable { + case letters(PeerId, [String]) + case image(PeerReference, TelegramMediaImageRepresentation) + + var peerId: PeerId { + switch self { + case let .letters(value, _): + return value + case let .image(value, _): + return value.id + } + } +} + +private extension PeerAvatarReference { + init(peer: Peer) { + if let photo = peer.smallProfileImage, let peerReference = PeerReference(peer) { + self = .image(peerReference, photo) + } else { + self = .letters(peer.id, peer.displayLetters) + } + } +} + +private final class MergedAvatarsNodeArguments: NSObject { + let peers: [PeerAvatarReference] + let images: [PeerId: UIImage] + + init(peers: [PeerAvatarReference], images: [PeerId: UIImage]) { + self.peers = peers + self.images = images + } +} + +private let mergedImageSize: CGFloat = 16.0 +private let mergedImageSpacing: CGFloat = 15.0 + +private let avatarFont = avatarPlaceholderFont(size: 8.0) + +private final class MergedAvatarsNode: ASDisplayNode { + private var peers: [PeerAvatarReference] = [] + private var images: [PeerId: UIImage] = [:] + private var disposables: [PeerId: Disposable] = [:] + private let buttonNode: HighlightTrackingButtonNode + + var pressed: (() -> Void)? + + override init() { + self.buttonNode = HighlightTrackingButtonNode() + + super.init() + + self.isOpaque = false + self.displaysAsynchronously = true + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.addSubnode(self.buttonNode) + } + + deinit { + for (_, disposable) in self.disposables { + disposable.dispose() + } + } + + @objc private func buttonPressed() { + self.pressed?() + } + + func updateLayout(size: CGSize) { + self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) + } + + func update(context: AccountContext, peers: [Peer], synchronousLoad: Bool) { + var filteredPeers = peers.map(PeerAvatarReference.init) + if filteredPeers.count > 3 { + filteredPeers.dropLast(filteredPeers.count - 3) + } + if filteredPeers != self.peers { + self.peers = filteredPeers + + var validImageIds: [PeerId] = [] + for peer in filteredPeers { + if case .image = peer { + validImageIds.append(peer.peerId) + } + } + + var removedImageIds: [PeerId] = [] + for (id, _) in self.images { + if !validImageIds.contains(id) { + removedImageIds.append(id) + } + } + var removedDisposableIds: [PeerId] = [] + for (id, disposable) in self.disposables { + if !validImageIds.contains(id) { + disposable.dispose() + removedDisposableIds.append(id) + } + } + for id in removedImageIds { + self.images.removeValue(forKey: id) + } + for id in removedDisposableIds { + self.disposables.removeValue(forKey: id) + } + for peer in filteredPeers { + switch peer { + case let .image(peerReference, representation): + if self.disposables[peer.peerId] == nil { + if let signal = peerAvatarImage(account: context.account, peerReference: peerReference, authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: mergedImageSize, height: mergedImageSize), synchronousLoad: synchronousLoad) { + let disposable = (signal + |> deliverOnMainQueue).start(next: { [weak self] imageVersions in + guard let strongSelf = self else { + return + } + let image = imageVersions?.0 + if let image = image { + strongSelf.images[peer.peerId] = image + strongSelf.setNeedsDisplay() + } + }) + self.disposables[peer.peerId] = disposable + } + } + case .letters: + break + } + } + self.setNeedsDisplay() + } + } + + override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol { + return MergedAvatarsNodeArguments(peers: self.peers, images: self.images) + } + + @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + assertNotOnMainThread() + + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + guard let parameters = parameters as? MergedAvatarsNodeArguments else { + return + } + + let imageOverlaySpacing: CGFloat = 1.0 + context.setBlendMode(.copy) + + var currentX = mergedImageSize + mergedImageSpacing * CGFloat(parameters.peers.count - 1) - mergedImageSize + for i in (0 ..< parameters.peers.count).reversed() { + let imageRect = CGRect(origin: CGPoint(x: currentX, y: 0.0), size: CGSize(width: mergedImageSize, height: mergedImageSize)) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: imageRect.insetBy(dx: -1.0, dy: -1.0)) + + context.saveGState() + switch parameters.peers[i] { + case let .letters(peerId, letters): + context.translateBy(x: currentX, y: 0.0) + drawPeerAvatarLetters(context: context, size: CGSize(width: mergedImageSize, height: mergedImageSize), font: avatarFont, letters: letters, peerId: peerId) + context.translateBy(x: -currentX, y: 0.0) + case let .image(reference): + if let image = parameters.images[parameters.peers[i].peerId] { + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + context.draw(image.cgImage!, in: imageRect) + } else { + context.setFillColor(UIColor.gray.cgColor) + context.fillEllipse(in: imageRect) + } + } + context.restoreGState() + currentX -= mergedImageSpacing + } + } +} diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageReplyInfoNode.swift index 4dc9f3022f..b546deccc4 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageReplyInfoNode.swift @@ -12,9 +12,6 @@ import LocalizedPeerData import PhotoResources import TelegramStringFormatting -private let titleFont = Font.medium(14.0) -private let textFont = Font.regular(14.0) - enum ChatMessageReplyInfoType { case bubble(incoming: Bool) case standalone @@ -54,8 +51,12 @@ class ChatMessageReplyInfoNode: ASDisplayNode { let previousMediaReference = maybeNode?.previousMediaReference return { presentationData, strings, context, type, message, constrainedSize in + let fontSize = floor(presentationData.fontSize.baseDisplaySize * 14.0 / 17.0) + let titleFont = Font.medium(fontSize) + let textFont = Font.regular(fontSize) + let titleString = message.effectiveAuthor?.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) ?? strings.User_DeletedAccount - let (textString, isMedia) = descriptionStringForMessage(message, strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: context.account.peerId) + let (textString, isMedia) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: context.account.peerId) let placeholderColor: UIColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor let titleColor: UIColor @@ -75,7 +76,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) titleColor = serviceColor.primaryText - let graphics = PresentationResourcesChat.additionalGraphics(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) lineImage = graphics.chatServiceVerticalLineImage textColor = titleColor } @@ -110,10 +111,26 @@ class ChatMessageReplyInfoNode: ASDisplayNode { } } + var imageTextInset: CGFloat = 0.0 + if let _ = imageDimensions { + imageTextInset += floor(presentationData.fontSize.baseDisplaySize * 32.0 / 17.0) + } + + let maximumTextWidth = max(0.0, constrainedSize.width - imageTextInset) + + let contrainedTextSize = CGSize(width: maximumTextWidth, height: constrainedSize.height) + + let textInsets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0) + + let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) + let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: textString, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) + + let imageSide = titleLayout.size.height + textLayout.size.height - 16.0 + var applyImage: (() -> TransformImageNode)? if let imageDimensions = imageDimensions { - leftInset += 32.0 - let boundingSize = CGSize(width: 30.0, height: 30.0) + let boundingSize = CGSize(width: imageSide, height: imageSide) + leftInset += imageSide + 2.0 var radius: CGFloat = 2.0 var imageSize = imageDimensions.aspectFilled(boundingSize) if hasRoundImage { @@ -144,15 +161,6 @@ class ChatMessageReplyInfoNode: ASDisplayNode { } } - let maximumTextWidth = max(0.0, constrainedSize.width - leftInset) - - let contrainedTextSize = CGSize(width: maximumTextWidth, height: constrainedSize.height) - - let textInsets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0) - - let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) - let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: textString, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) - let size = CGSize(width: max(titleLayout.size.width - textInsets.left - textInsets.right, textLayout.size.width - textInsets.left - textInsets.right) + leftInset, height: titleLayout.size.height + textLayout.size.height - 2 * (textInsets.top + textInsets.bottom) + 2 * spacing) return (size, { @@ -190,7 +198,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { node.addSubnode(imageNode) node.imageNode = imageNode } - imageNode.frame = CGRect(origin: CGPoint(x: 8.0, y: 4.0 + UIScreenPixel), size: CGSize(width: 30.0, height: 30.0)) + imageNode.frame = CGRect(origin: CGPoint(x: 8.0, y: 4.0 + UIScreenPixel), size: CGSize(width: imageSide, height: imageSide)) if let updateImageSignal = updateImageSignal { imageNode.setSignal(updateImageSignal) diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageRestrictedBubbleContentNode.swift index 218cb6c4ef..2a410ec9ed 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageRestrictedBubbleContentNode.swift @@ -48,6 +48,9 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { let textConstrainedSize = CGSize(width: min(maxTextWidth, constrainedSize.width - horizontalInset), height: constrainedSize.height) var edited = false + if item.attributes.updatingMedia != nil { + edited = true + } var viewCount: Int? var rawText = "" for attribute in item.message.attributes { @@ -56,7 +59,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { } else if let attribute = attribute as? ViewCountMessageAttribute { viewCount = attribute.count } else if let attribute = attribute as? RestrictedContentMessageAttribute { - rawText = attribute.platformText(platform: "ios") ?? "" + rawText = attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) ?? "" } } @@ -245,7 +248,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } - override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? { if !self.statusNode.isHidden { return self.statusNode.reactionNode(value: value) } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageSelectionInputPanelNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageSelectionInputPanelNode.swift index 1f2423e1a6..986f52b784 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageSelectionInputPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageSelectionInputPanelNode.swift @@ -15,12 +15,14 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { private let reportButton: HighlightableButtonNode private let forwardButton: HighlightableButtonNode private let shareButton: HighlightableButtonNode + private let separatorNode: ASDisplayNode - private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, metrics: LayoutMetrics)? + private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, metrics: LayoutMetrics, isSecondary: Bool)? private var presentationInterfaceState: ChatPresentationInterfaceState? private var actions: ChatAvailableMessageActions? private var theme: PresentationTheme + private let peerMedia: Bool private let canDeleteMessagesDisposable = MetaDisposable() @@ -31,8 +33,8 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { if self.selectedMessages.isEmpty { self.actions = nil - if let (width, leftInset, rightInset, maxHeight, metrics) = self.validLayout, let interfaceState = self.presentationInterfaceState { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, transition: .immediate, interfaceState: interfaceState, metrics: metrics) + if let (width, leftInset, rightInset, maxHeight, metrics, isSecondary) = self.validLayout, let interfaceState = self.presentationInterfaceState { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics) } self.canDeleteMessagesDisposable.set(nil) } else if let context = self.context { @@ -40,8 +42,8 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { |> deliverOnMainQueue).start(next: { [weak self] actions in if let strongSelf = self { strongSelf.actions = actions - if let (width, leftInset, rightInset, maxHeight, metrics) = strongSelf.validLayout, let interfaceState = strongSelf.presentationInterfaceState { - let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, transition: .immediate, interfaceState: interfaceState, metrics: metrics) + if let (width, leftInset, rightInset, maxHeight, metrics, isSecondary) = strongSelf.validLayout, let interfaceState = strongSelf.presentationInterfaceState { + let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics) } } })) @@ -50,8 +52,9 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { } } - init(theme: PresentationTheme, strings: PresentationStrings) { + init(theme: PresentationTheme, strings: PresentationStrings, peerMedia: Bool = false) { self.theme = theme + self.peerMedia = peerMedia self.deleteButton = HighlightableButtonNode() self.deleteButton.isEnabled = false @@ -81,12 +84,16 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor + super.init() self.addSubnode(self.deleteButton) self.addSubnode(self.reportButton) self.addSubnode(self.forwardButton) self.addSubnode(self.shareButton) + self.addSubnode(self.separatorNode) self.forwardButton.isEnabled = false @@ -110,6 +117,8 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.reportButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionReport"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) + + self.separatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor } } @@ -129,8 +138,8 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.shareSelectedMessages() } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - self.validLayout = (width, leftInset, rightInset, maxHeight, metrics) + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + self.validLayout = (width, leftInset, rightInset, maxHeight, metrics, isSecondary) let panelHeight = defaultHeight(metrics: metrics) @@ -143,24 +152,34 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.forwardButton.isEnabled = actions.options.contains(.forward) self.shareButton.isEnabled = false - self.deleteButton.isEnabled = true + if self.peerMedia { + self.deleteButton.isEnabled = !actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty + } else { + self.deleteButton.isEnabled = true + } self.shareButton.isEnabled = !actions.options.intersection([.forward]).isEmpty self.reportButton.isEnabled = !actions.options.intersection([.report]).isEmpty - self.deleteButton.isHidden = false + if self.peerMedia { + self.deleteButton.isHidden = !self.deleteButton.isEnabled + } else { + self.deleteButton.isHidden = false + } self.reportButton.isHidden = !self.reportButton.isEnabled } else { self.deleteButton.isEnabled = false - self.deleteButton.isHidden = false + self.deleteButton.isHidden = self.peerMedia self.reportButton.isEnabled = false self.reportButton.isHidden = true self.forwardButton.isEnabled = false self.shareButton.isEnabled = false } - if self.reportButton.isHidden { + if self.reportButton.isHidden || (self.peerMedia && self.deleteButton.isHidden && self.reportButton.isHidden) { if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = peer.info { self.reportButton.isHidden = false + } else if self.peerMedia { + self.deleteButton.isHidden = false } } @@ -196,6 +215,9 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.shareButton.frame = CGRect(origin: CGPoint(x: floor((width - rightInset - 57.0) / 2.0), y: 0.0), size: CGSize(width: 57.0, height: panelHeight)) } + transition.updateAlpha(node: self.separatorNode, alpha: isSecondary ? 1.0 : 0.0) + self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: panelHeight), size: CGSize(width: width, height: UIScreenPixel)) + return panelHeight } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageSelectionNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageSelectionNode.swift index facf67d2dc..ab47b38038 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageSelectionNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageSelectionNode.swift @@ -3,6 +3,7 @@ import UIKit import AsyncDisplayKit import TelegramPresentationData import CheckNode +import SyncCore final class ChatMessageSelectionNode: ASDisplayNode { private let toggle: (Bool) -> Void @@ -10,9 +11,17 @@ final class ChatMessageSelectionNode: ASDisplayNode { private(set) var selected = false private let checkNode: CheckNode - init(theme: PresentationTheme, toggle: @escaping (Bool) -> Void) { + init(wallpaper: TelegramWallpaper, theme: PresentationTheme, toggle: @escaping (Bool) -> Void) { self.toggle = toggle - self.checkNode = CheckNode(strokeColor: theme.list.itemCheckColors.strokeColor, fillColor: theme.list.itemCheckColors.fillColor, foregroundColor: theme.list.itemCheckColors.foregroundColor, style: .overlay) + + let style: CheckNodeStyle + if wallpaper == theme.chat.defaultWallpaper, case .color = wallpaper { + style = .plain + } else { + style = .overlay + } + + self.checkNode = CheckNode(strokeColor: theme.list.itemCheckColors.strokeColor, fillColor: theme.list.itemCheckColors.fillColor, foregroundColor: theme.list.itemCheckColors.foregroundColor, style: style) self.checkNode.isUserInteractionEnabled = false super.init() @@ -41,10 +50,8 @@ final class ChatMessageSelectionNode: ASDisplayNode { } } - override func layout() { - super.layout() - + func updateLayout(size: CGSize) { let checkSize = CGSize(width: 32.0, height: 32.0) - self.checkNode.frame = CGRect(origin: CGPoint(x: 4.0, y: floor((self.bounds.size.height - checkSize.height) / 2.0)), size: checkSize) + self.checkNode.frame = CGRect(origin: CGPoint(x: 4.0, y: floor((size.height - checkSize.height) / 2.0)), size: checkSize) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageStickerItemNode.swift index 868162599d..d4b5f3b248 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageStickerItemNode.swift @@ -143,7 +143,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let currentItem = self.item return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in - let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params) + let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData) let incoming = item.message.effectivelyIncoming(item.context.account.peerId) var imageSize: CGSize = CGSize(width: 100.0, height: 100.0) if let telegramFile = telegramFile { @@ -353,7 +353,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { updatedReplyBackgroundNode = ASImageNode() } - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) replyBackgroundImage = graphics.chatFreeformContentAdditionalInfoBackgroundImage } @@ -364,7 +364,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if currentShareButtonNode != nil { updatedShareButtonNode = currentShareButtonNode if item.presentationData.theme !== currentItem?.presentationData.theme { - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) if item.message.id.peerId == item.context.account.peerId { updatedShareButtonBackground = graphics.chatBubbleNavigateButtonImage } else { @@ -374,7 +374,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } else { let buttonNode = HighlightableButtonNode() let buttonIcon: UIImage? - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) if item.message.id.peerId == item.context.account.peerId { buttonIcon = graphics.chatBubbleNavigateButtonImage } else { @@ -389,7 +389,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { var maxContentWidth = imageSize.width var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))? if let replyMarkup = replyMarkup { - let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.strings, replyMarkup, item.message, maxContentWidth) + let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maxContentWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout } @@ -790,17 +790,20 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let offset: CGFloat = incoming ? 42.0 : 0.0 if let selectionNode = self.selectionNode { + let selectionFrame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) + selectionNode.frame = selectionFrame + selectionNode.updateLayout(size: selectionFrame.size) selectionNode.updateSelected(selected, animated: animated) - selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0); } else { - let selectionNode = ChatMessageSelectionNode(theme: item.presentationData.theme.theme, toggle: { [weak self] value in + let selectionNode = ChatMessageSelectionNode(wallpaper: item.presentationData.theme.wallpaper, theme: item.presentationData.theme.theme, toggle: { [weak self] value in if let strongSelf = self, let item = strongSelf.item { item.controllerInteraction.toggleMessagesSelection([item.message.id], value) } }) - - selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) + let selectionFrame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) + selectionNode.frame = selectionFrame + selectionNode.updateLayout(size: selectionFrame.size) self.addSubnode(selectionNode) self.selectionNode = selectionNode selectionNode.updateSelected(selected, animated: false) diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageTextBubbleContentNode.swift index ee8f9339ad..5df3808ac2 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -64,7 +64,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.textAccessibilityOverlayNode) self.textAccessibilityOverlayNode.openUrl = { [weak self] url in - self?.item?.controllerInteraction.openUrl(url, false, false) + self?.item?.controllerInteraction.openUrl(url, false, false, nil) } self.statusNode.openReactions = { [weak self] in @@ -105,6 +105,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let textConstrainedSize = CGSize(width: min(maxTextWidth, constrainedSize.width - horizontalInset), height: constrainedSize.height) var edited = false + if item.attributes.updatingMedia != nil { + edited = true + } var viewCount: Int? for attribute in item.message.attributes { if let attribute = attribute as? EditedMessageAttribute { @@ -137,7 +140,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } else { if message.flags.contains(.Failed) { statusType = .BubbleOutgoing(.Failed) - } else if message.flags.isSending && !message.isSentOrAcknowledged { + } else if (message.flags.isSending && !message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil { statusType = .BubbleOutgoing(.Sending) } else { statusType = .BubbleOutgoing(.Sent(read: item.read)) @@ -178,7 +181,11 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { rawText = item.presentationData.strings.Conversation_UnsupportedMediaPlaceholder messageEntities = [MessageTextEntity(range: 0.. ChatMessageBubbleContentTapAction { + override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.textNode.frame if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { @@ -421,6 +428,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { return .hashtag(hashtag.peerName, hashtag.hashtag) } else if let timecode = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Timecode)] as? TelegramTimecode { return .timecode(timecode.time, timecode.text) + } else if let bankCard = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard)] as? String { + return .bankCard(bankCard) } else { return .none } @@ -444,7 +453,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { TelegramTextAttributes.PeerTextMention, TelegramTextAttributes.BotCommand, TelegramTextAttributes.Hashtag, - TelegramTextAttributes.Timecode + TelegramTextAttributes.Timecode, + TelegramTextAttributes.BankCard ] for name in possibleNames { if let _ = attributes[NSAttributedString.Key(rawValue: name)] { @@ -579,7 +589,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? { if !self.statusNode.isHidden { return self.statusNode.reactionNode(value: value) } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageUnsupportedBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageUnsupportedBubbleContentNode.swift index d5f3b64ed0..81c3423d01 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageUnsupportedBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageUnsupportedBubbleContentNode.swift @@ -100,7 +100,7 @@ final class ChatMessageUnsupportedBubbleContentNode: ChatMessageBubbleContentNod self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction { + override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.bounds.contains(point) { if self.buttonNode.frame.contains(point) { return .ignore diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index ef05e0e0b6..981c37021d 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -113,17 +113,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { if let strongSelf = self, let item = strongSelf.item { if let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content { if let image = content.image, let instantPage = content.instantPage { - var isGallery = false - switch instantPageType(of: content) { - case .album: - let count = instantPageGalleryMedia(webpageId: webPage.webpageId, page: instantPage, galleryMedia: image).count - if count > 1 { - isGallery = true - } - default: - break - } - if !isGallery { + if instantPageType(of: content) != .album { item.controllerInteraction.openInstantPage(item.message, item.associatedData) return } @@ -159,7 +149,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } if let webpage = webPageContent { - item.controllerInteraction.openUrl(webpage.url, false, nil) + item.controllerInteraction.openUrl(webpage.url, false, nil, nil) } } } @@ -257,11 +247,15 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { mediaAndFlags = (webpage.image ?? file, [.preferMediaBeforeText]) } } else if webpage.type == "telegram_background" { - var patternColor: UIColor? - if let wallpaper = parseWallpaperUrl(webpage.url), case let .slug(_, _, color, intensity) = wallpaper { - patternColor = color?.withAlphaComponent(CGFloat(intensity ?? 50) / 100.0) + var topColor: UIColor? + var bottomColor: UIColor? + var rotation: Int32? + if let wallpaper = parseWallpaperUrl(webpage.url), case let .slug(_, _, firstColor, secondColor, intensity, rotationValue) = wallpaper { + topColor = firstColor?.withAlphaComponent(CGFloat(intensity ?? 50) / 100.0) + bottomColor = secondColor?.withAlphaComponent(CGFloat(intensity ?? 50) / 100.0) + rotation = rotationValue } - let media = WallpaperPreviewMedia(content: .file(file, patternColor, false, false)) + let media = WallpaperPreviewMedia(content: .file(file, topColor, bottomColor, rotation, false, false)) mediaAndFlags = (media, [.preferMediaAspectFilled]) if let fileSize = file.size { badge = dataSizeString(fileSize, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) @@ -288,30 +282,58 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { mediaAndFlags = (image, flags) } } else if let type = webpage.type { - if type == "telegram_backgroud" { - if let text = webpage.text, let colorCodeRange = text.range(of: "#") { - let colorCode = String(text[colorCodeRange.upperBound...]) - if colorCode.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789abcdefABCDEF").inverted) == nil, let color = UIColor(hexString: colorCode) { - let media = WallpaperPreviewMedia(content: .color(color)) - mediaAndFlags = (media, ChatMessageAttachedContentNodeMediaFlags()) + if type == "telegram_background" { + var topColor: UIColor? + var bottomColor: UIColor? + var rotation: Int32? + if let wallpaper = parseWallpaperUrl(webpage.url) { + if case let .color(color) = wallpaper { + topColor = color + } else if case let .gradient(topColorValue, bottomColorValue, rotationValue) = wallpaper { + topColor = topColorValue + bottomColor = bottomColorValue + rotation = rotationValue } } + + var content: WallpaperPreviewMediaContent? + if let topColor = topColor { + if let bottomColor = bottomColor { + content = .gradient(topColor, bottomColor, rotation) + } else { + content = .color(topColor) + } + } + if let content = content { + let media = WallpaperPreviewMedia(content: content) + mediaAndFlags = (media, []) + } } else if type == "telegram_theme" { var file: TelegramMediaFile? + var settings: TelegramThemeSettings? var isSupported = false - if let contentFiles = webpage.files { - if let filteredFile = contentFiles.filter({ $0.mimeType == themeMimeType }).first { - isSupported = true - file = filteredFile - } else { - file = contentFiles.first + + for attribute in webpage.attributes { + if case let .theme(attribute) = attribute { + if let attributeSettings = attribute.settings { + settings = attributeSettings + isSupported = true + } else if let filteredFile = attribute.files.filter({ $0.mimeType == themeMimeType }).first { + file = filteredFile + isSupported = true + } } - } else if let contentFile = webpage.file { + } + + if !isSupported, let contentFile = webpage.file { isSupported = true file = contentFile } if let file = file { - let media = WallpaperPreviewMedia(content: .file(file, nil, true, isSupported)) + let media = WallpaperPreviewMedia(content: .file(file, nil, nil, nil, true, isSupported)) + mediaAndFlags = (media, ChatMessageAttachedContentNodeMediaFlags()) + } else if let settings = settings { + let media = WallpaperPreviewMedia(content: .themeSettings(settings)) mediaAndFlags = (media, ChatMessageAttachedContentNodeMediaFlags()) } } @@ -348,7 +370,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.context, item.controllerInteraction, item.message, item.read, title, subtitle, text, entities, mediaAndFlags, badge, actionIcon, actionTitle, true, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, title, subtitle, text, entities, mediaAndFlags, badge, actionIcon, actionTitle, true, layoutConstants, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) @@ -393,10 +415,10 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { return self.contentNode.playMediaWithSound() } - override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction { + override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.bounds.contains(point) { let contentNodeFrame = self.contentNode.frame - let result = self.contentNode.tapActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY), gesture: gesture) + let result = self.contentNode.tapActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY), gesture: gesture, isEstimating: isEstimating) switch result { case .none: break @@ -488,7 +510,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } - override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id != messageId { return nil } @@ -521,7 +543,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { self.contentNode.updateTouchesAtPoint(point.flatMap { $0.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY) }) } - override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? { return self.contentNode.reactionTargetNode(value: value) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatOverlayNavigationBar.swift b/submodules/TelegramUI/TelegramUI/ChatOverlayNavigationBar.swift index a19a48aa66..720135e850 100644 --- a/submodules/TelegramUI/TelegramUI/ChatOverlayNavigationBar.swift +++ b/submodules/TelegramUI/TelegramUI/ChatOverlayNavigationBar.swift @@ -14,6 +14,7 @@ final class ChatOverlayNavigationBar: ASDisplayNode { private let theme: PresentationTheme private let strings: PresentationStrings private let nameDisplayOrder: PresentationPersonNameOrder + private let tapped: () -> Void private let close: () -> Void private let separatorNode: ASDisplayNode @@ -40,10 +41,11 @@ final class ChatOverlayNavigationBar: ASDisplayNode { } } - init(theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, close: @escaping () -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, tapped: @escaping () -> Void, close: @escaping () -> Void) { self.theme = theme self.strings = strings self.nameDisplayOrder = nameDisplayOrder + self.tapped = tapped self.close = close self.separatorNode = ASDisplayNode() @@ -83,6 +85,13 @@ final class ChatOverlayNavigationBar: ASDisplayNode { self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) } + override func didLoad() { + super.didLoad() + + let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap)) + self.view.addGestureRecognizer(gestureRecognizer) + } + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) @@ -93,11 +102,15 @@ final class ChatOverlayNavigationBar: ASDisplayNode { let _ = titleApply() transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)) - let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) - transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: size.width - sideInset - closeButtonSize.width - 6.0, y: floor((size.height - closeButtonSize.height) / 2.0)), size: closeButtonSize)) + let closeButtonSize = CGSize(width: size.height, height: size.height) + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: size.width - sideInset - closeButtonSize.width + 10.0, y: 0.0), size: closeButtonSize)) } - @objc func closePressed() { + @objc private func handleTap() { + self.tapped() + } + + @objc private func closePressed() { self.close() } } diff --git a/submodules/TelegramUI/TelegramUI/ChatPanelInterfaceInteraction.swift b/submodules/TelegramUI/TelegramUI/ChatPanelInterfaceInteraction.swift index 47c6496061..7aa8bd50b5 100644 --- a/submodules/TelegramUI/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/submodules/TelegramUI/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -111,12 +111,12 @@ final class ChatPanelInterfaceInteraction { let openLinkEditing: () -> Void let reportPeerIrrelevantGeoLocation: () -> Void let displaySlowmodeTooltip: (ASDisplayNode, CGRect) -> Void - let displaySendMessageOptions: () -> Void + let displaySendMessageOptions: (ASDisplayNode, ContextGesture) -> Void let openScheduledMessages: () -> Void let displaySearchResultsTooltip: (ASDisplayNode, CGRect) -> Void let statuses: ChatPanelInterfaceInteractionStatuses? - init(setupReplyMessage: @escaping (MessageId, @escaping (ContainedViewLayoutTransition) -> Void) -> Void, setupEditMessage: @escaping (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void, beginMessageSelection: @escaping ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void, deleteSelectedMessages: @escaping () -> Void, reportSelectedMessages: @escaping () -> Void, reportMessages: @escaping ([Message], ContextController?) -> Void, deleteMessages: @escaping ([Message], ContextController?, @escaping (ContextMenuActionResult) -> Void) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardCurrentForwardMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputStateAndMode: @escaping ((ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, openStickers: @escaping () -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain, String) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, openSearchResults: @escaping () -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, navigateToChat: @escaping (PeerId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, displayRestrictedInfo: @escaping (ChatPanelRestrictionInfoSubject, ChatPanelRestrictionInfoDisplayType) -> Void, displayVideoUnmuteTip: @escaping (CGPoint?) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, shareAccountContact: @escaping () -> Void, reportPeer: @escaping () -> Void, presentPeerContact: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, requestUnvoteInMessage: @escaping (MessageId) -> Void, requestStopPollInMessage: @escaping (MessageId) -> Void, updateInputLanguage: @escaping ((String?) -> String?) -> Void, unarchiveChat: @escaping () -> Void, openLinkEditing: @escaping () -> Void, reportPeerIrrelevantGeoLocation: @escaping () -> Void, displaySlowmodeTooltip: @escaping (ASDisplayNode, CGRect) -> Void, displaySendMessageOptions: @escaping () -> Void, openScheduledMessages: @escaping () -> Void, displaySearchResultsTooltip: @escaping (ASDisplayNode, CGRect) -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { + init(setupReplyMessage: @escaping (MessageId, @escaping (ContainedViewLayoutTransition) -> Void) -> Void, setupEditMessage: @escaping (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void, beginMessageSelection: @escaping ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void, deleteSelectedMessages: @escaping () -> Void, reportSelectedMessages: @escaping () -> Void, reportMessages: @escaping ([Message], ContextController?) -> Void, deleteMessages: @escaping ([Message], ContextController?, @escaping (ContextMenuActionResult) -> Void) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardCurrentForwardMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputStateAndMode: @escaping ((ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, openStickers: @escaping () -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain, String) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, openSearchResults: @escaping () -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, navigateToChat: @escaping (PeerId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, displayRestrictedInfo: @escaping (ChatPanelRestrictionInfoSubject, ChatPanelRestrictionInfoDisplayType) -> Void, displayVideoUnmuteTip: @escaping (CGPoint?) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, shareAccountContact: @escaping () -> Void, reportPeer: @escaping () -> Void, presentPeerContact: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, requestUnvoteInMessage: @escaping (MessageId) -> Void, requestStopPollInMessage: @escaping (MessageId) -> Void, updateInputLanguage: @escaping ((String?) -> String?) -> Void, unarchiveChat: @escaping () -> Void, openLinkEditing: @escaping () -> Void, reportPeerIrrelevantGeoLocation: @escaping () -> Void, displaySlowmodeTooltip: @escaping (ASDisplayNode, CGRect) -> Void, displaySendMessageOptions: @escaping (ASDisplayNode, ContextGesture) -> Void, openScheduledMessages: @escaping () -> Void, displaySearchResultsTooltip: @escaping (ASDisplayNode, CGRect) -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { self.setupReplyMessage = setupReplyMessage self.setupEditMessage = setupEditMessage self.beginMessageSelection = beginMessageSelection diff --git a/submodules/TelegramUI/TelegramUI/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/TelegramUI/ChatPinnedMessageTitlePanelNode.swift index f5108d5bde..96052f5d0b 100644 --- a/submodules/TelegramUI/TelegramUI/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatPinnedMessageTitlePanelNode.swift @@ -13,6 +13,23 @@ import StickerResources import PhotoResources import TelegramStringFormatting +private func foldLineBreaks(_ text: String) -> String { + var lines = text.split { $0.isNewline } + var startedBothLines = false + var result = "" + for line in lines { + if line.isEmpty { + continue + } + if result.isEmpty { + result += line + } else { + result += " " + line + } + } + return result +} + final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { private let context: AccountContext private let tapButton: HighlightTrackingButtonNode @@ -194,8 +211,13 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { imageDimensions = representation.dimensions.cgSize } break - } else if let _ = media as? TelegramMediaPoll { - titleString = strings.Conversation_PinnedPoll + } else if let poll = media as? TelegramMediaPoll { + switch poll.kind { + case .poll: + titleString = strings.Conversation_PinnedPoll + case .quiz: + titleString = strings.Conversation_PinnedQuiz + } } } @@ -238,7 +260,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: descriptionStringForMessage(message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId).0, font: Font.regular(15.0), textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: foldLineBreaks(descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId).0), font: Font.regular(15.0), textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) Queue.mainQueue().async { if let strongSelf = self { diff --git a/submodules/TelegramUI/TelegramUI/ChatPresentationData.swift b/submodules/TelegramUI/TelegramUI/ChatPresentationData.swift index 6d601590aa..253882e904 100644 --- a/submodules/TelegramUI/TelegramUI/ChatPresentationData.swift +++ b/submodules/TelegramUI/TelegramUI/ChatPresentationData.swift @@ -6,27 +6,6 @@ import SyncCore import TelegramPresentationData import TelegramUIPreferences -extension PresentationFontSize { - var baseDisplaySize: CGFloat { - switch self { - case .extraSmall: - return 14.0 - case .small: - return 15.0 - case .medium: - return 16.0 - case .regular: - return 17.0 - case .large: - return 19.0 - case .extraLarge: - return 23.0 - case .extraLargeX2: - return 26.0 - } - } -} - public final class ChatPresentationThemeData: Equatable { public let theme: PresentationTheme public let wallpaper: TelegramWallpaper @@ -49,6 +28,7 @@ public final class ChatPresentationData { let nameDisplayOrder: PresentationPersonNameOrder let disableAnimations: Bool let largeEmoji: Bool + let chatBubbleCorners: PresentationChatBubbleCorners let animatedEmojiScale: CGFloat let isPreview: Bool @@ -60,13 +40,14 @@ public final class ChatPresentationData { let messageFixedFont: UIFont let messageBlockQuoteFont: UIFont - init(theme: ChatPresentationThemeData, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool, largeEmoji: Bool, animatedEmojiScale: CGFloat = 1.0, isPreview: Bool = false) { + init(theme: ChatPresentationThemeData, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool, largeEmoji: Bool, chatBubbleCorners: PresentationChatBubbleCorners, animatedEmojiScale: CGFloat = 1.0, isPreview: Bool = false) { self.theme = theme self.fontSize = fontSize self.strings = strings self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder self.disableAnimations = disableAnimations + self.chatBubbleCorners = chatBubbleCorners self.largeEmoji = largeEmoji self.isPreview = isPreview diff --git a/submodules/TelegramUI/TelegramUI/ChatPresentationInterfaceState.swift b/submodules/TelegramUI/TelegramUI/ChatPresentationInterfaceState.swift index 3848c0c89f..3697ec4e39 100644 --- a/submodules/TelegramUI/TelegramUI/ChatPresentationInterfaceState.swift +++ b/submodules/TelegramUI/TelegramUI/ChatPresentationInterfaceState.swift @@ -82,6 +82,7 @@ enum ChatMediaInputMode { enum ChatMediaInputSearchMode { case gif case sticker + case trending } enum ChatMediaInputExpanded: Equatable { @@ -305,12 +306,13 @@ final class ChatPresentationInterfaceState: Equatable { let nameDisplayOrder: PresentationPersonNameOrder let limitsConfiguration: LimitsConfiguration let fontSize: PresentationFontSize + let bubbleCorners: PresentationChatBubbleCorners let accountPeerId: PeerId let mode: ChatControllerPresentationMode let hasScheduledMessages: Bool let isScheduledMessages: Bool - init(chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, accountPeerId: PeerId, mode: ChatControllerPresentationMode, chatLocation: ChatLocation, isScheduledMessages: Bool) { + init(chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, chatLocation: ChatLocation, isScheduledMessages: Bool) { self.interfaceState = ChatInterfaceState() self.inputTextPanelState = ChatTextInputPanelState() self.editMessageState = nil @@ -348,13 +350,14 @@ final class ChatPresentationInterfaceState: Equatable { self.nameDisplayOrder = nameDisplayOrder self.limitsConfiguration = limitsConfiguration self.fontSize = fontSize + self.bubbleCorners = bubbleCorners self.accountPeerId = accountPeerId self.mode = mode self.hasScheduledMessages = false self.isScheduledMessages = isScheduledMessages } - init(interfaceState: ChatInterfaceState, chatLocation: ChatLocation, renderedPeer: RenderedPeer?, isNotAccessible: Bool, explicitelyCanPinMessages: Bool, contactStatus: ChatContactStatus?, hasBots: Bool, isArchived: Bool, inputTextPanelState: ChatTextInputPanelState, editMessageState: ChatEditInterfaceMessageState?, recordedMediaPreview: ChatRecordedMediaPreview?, inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult], inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: Message?, peerIsBlocked: Bool, peerIsMuted: Bool, peerDiscussionId: PeerId?, peerGeoLocation: PeerGeoLocation?, callsAvailable: Bool, callsPrivate: Bool, slowmodeState: ChatSlowmodeState?, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: (String, TelegramMediaWebpage)?, editingUrlPreview: (String, TelegramMediaWebpage)?, search: ChatSearchData?, searchQuerySuggestionResult: ChatPresentationInputQueryResult?, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, accountPeerId: PeerId, mode: ChatControllerPresentationMode, hasScheduledMessages: Bool, isScheduledMessages: Bool) { + init(interfaceState: ChatInterfaceState, chatLocation: ChatLocation, renderedPeer: RenderedPeer?, isNotAccessible: Bool, explicitelyCanPinMessages: Bool, contactStatus: ChatContactStatus?, hasBots: Bool, isArchived: Bool, inputTextPanelState: ChatTextInputPanelState, editMessageState: ChatEditInterfaceMessageState?, recordedMediaPreview: ChatRecordedMediaPreview?, inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult], inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: Message?, peerIsBlocked: Bool, peerIsMuted: Bool, peerDiscussionId: PeerId?, peerGeoLocation: PeerGeoLocation?, callsAvailable: Bool, callsPrivate: Bool, slowmodeState: ChatSlowmodeState?, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: (String, TelegramMediaWebpage)?, editingUrlPreview: (String, TelegramMediaWebpage)?, search: ChatSearchData?, searchQuerySuggestionResult: ChatPresentationInputQueryResult?, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, hasScheduledMessages: Bool, isScheduledMessages: Bool) { self.interfaceState = interfaceState self.chatLocation = chatLocation self.renderedPeer = renderedPeer @@ -392,6 +395,7 @@ final class ChatPresentationInterfaceState: Equatable { self.nameDisplayOrder = nameDisplayOrder self.limitsConfiguration = limitsConfiguration self.fontSize = fontSize + self.bubbleCorners = bubbleCorners self.accountPeerId = accountPeerId self.mode = mode self.hasScheduledMessages = hasScheduledMessages @@ -529,6 +533,9 @@ final class ChatPresentationInterfaceState: Equatable { if lhs.fontSize != rhs.fontSize { return false } + if lhs.bubbleCorners != rhs.bubbleCorners { + return false + } if lhs.accountPeerId != rhs.accountPeerId { return false } @@ -545,31 +552,31 @@ final class ChatPresentationInterfaceState: Equatable { } func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedPeer(_ f: (RenderedPeer?) -> RenderedPeer?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: f(self.renderedPeer), isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: f(self.renderedPeer), isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedIsNotAccessible(_ isNotAccessible: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedExplicitelyCanPinMessages(_ explicitelyCanPinMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedContactStatus(_ contactStatus: ChatContactStatus?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedHasBots(_ hasBots: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedIsArchived(_ isArchived: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedInputQueryResult(queryKind: ChatPresentationInputQueryKind, _ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { @@ -580,115 +587,119 @@ final class ChatPresentationInterfaceState: Equatable { } else { inputQueryResults.removeValue(forKey: queryKind) } - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedInputTextPanelState(_ f: (ChatTextInputPanelState) -> ChatTextInputPanelState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: f(self.inputTextPanelState), editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: f(self.inputTextPanelState), editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedEditMessageState(_ editMessageState: ChatEditInterfaceMessageState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedRecordedMediaPreview(_ recordedMediaPreview: ChatRecordedMediaPreview?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedInputMode(_ f: (ChatInputMode) -> ChatInputMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedTitlePanelContext(_ f: ([ChatTitlePanelContext]) -> [ChatTitlePanelContext]) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedKeyboardButtonsMessage(_ message: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedPinnedMessage(_ pinnedMessage: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedPeerIsBlocked(_ peerIsBlocked: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedPeerIsMuted(_ peerIsMuted: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedPeerDiscussionId(_ peerDiscussionId: PeerId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedPeerGeoLocation(_ peerGeoLocation: PeerGeoLocation?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedCallsAvailable(_ callsAvailable: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedCallsPrivate(_ callsPrivate: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedSlowmodeState(_ slowmodeState: ChatSlowmodeState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedBotStartPayload(_ botStartPayload: String?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedChatHistoryState(_ chatHistoryState: ChatHistoryNodeHistoryState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedUrlPreview(_ urlPreview: (String, TelegramMediaWebpage)?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedEditingUrlPreview(_ editingUrlPreview: (String, TelegramMediaWebpage)?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedSearch(_ search: ChatSearchData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedSearchQuerySuggestionResult(_ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: f(self.searchQuerySuggestionResult), chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: f(self.searchQuerySuggestionResult), chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedMode(_ mode: ChatControllerPresentationMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedTheme(_ theme: PresentationTheme) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedStrings(_ strings: PresentationStrings) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedDateTimeFormat(_ dateTimeFormat: PresentationDateTimeFormat) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedChatWallpaper(_ chatWallpaper: TelegramWallpaper) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + } + + func updatedBubbleCorners(_ bubbleCorners: PresentationChatBubbleCorners) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } func updatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: hasScheduledMessages, isScheduledMessages: self.isScheduledMessages) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatRecentActionsController.swift b/submodules/TelegramUI/TelegramUI/ChatRecentActionsController.swift index 472b19bd5a..02ef88da19 100644 --- a/submodules/TelegramUI/TelegramUI/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatRecentActionsController.swift @@ -40,7 +40,13 @@ final class ChatRecentActionsController: TelegramBaseController { self.interaction = ChatRecentActionsInteraction(displayInfoAlert: { [weak self] in if let strongSelf = self { - self?.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.Channel_AdminLog_InfoPanelAlertTitle, text: strongSelf.presentationData.strings.Channel_AdminLog_InfoPanelAlertText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + let text: String + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + text = strongSelf.presentationData.strings.Channel_AdminLog_InfoPanelAlertText + } else { + text = strongSelf.presentationData.strings.Channel_AdminLog_InfoPanelChannelAlertText + } + self?.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.Channel_AdminLog_InfoPanelAlertTitle, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } }) @@ -112,7 +118,7 @@ final class ChatRecentActionsController: TelegramBaseController { }, openLinkEditing: { }, reportPeerIrrelevantGeoLocation: { }, displaySlowmodeTooltip: { _, _ in - }, displaySendMessageOptions: { + }, displaySendMessageOptions: { _, _ in }, openScheduledMessages: { }, displaySearchResultsTooltip: { _, _ in }, statuses: nil) diff --git a/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift index e09deb9a21..e484f7412e 100644 --- a/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift @@ -107,13 +107,13 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self.listNode = ListView() self.listNode.dynamicBounceEnabled = !self.presentationData.disableAnimations self.listNode.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) - self.loadingNode = ChatLoadingNode(theme: self.presentationData.theme, chatWallpaper: self.presentationData.chatWallpaper) - self.emptyNode = ChatRecentActionsEmptyNode(theme: self.presentationData.theme, chatWallpaper: self.presentationData.chatWallpaper) + self.loadingNode = ChatLoadingNode(theme: self.presentationData.theme, chatWallpaper: self.presentationData.chatWallpaper, bubbleCorners: self.presentationData.chatBubbleCorners) + self.emptyNode = ChatRecentActionsEmptyNode(theme: self.presentationData.theme, chatWallpaper: self.presentationData.chatWallpaper, chatBubbleCorners: self.presentationData.chatBubbleCorners) self.emptyNode.alpha = 0.0 - self.state = ChatRecentActionsControllerState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, fontSize: self.presentationData.fontSize) + self.state = ChatRecentActionsControllerState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, fontSize: self.presentationData.chatFontSize) - self.chatPresentationDataPromise = Promise(ChatPresentationData(theme: ChatPresentationThemeData(theme: self.presentationData.theme, wallpaper: self.presentationData.chatWallpaper), fontSize: self.presentationData.fontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations, largeEmoji: self.presentationData.largeEmoji)) + self.chatPresentationDataPromise = Promise(ChatPresentationData(theme: ChatPresentationThemeData(theme: self.presentationData.theme, wallpaper: self.presentationData.chatWallpaper), fontSize: self.presentationData.chatFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations, largeEmoji: self.presentationData.largeEmoji, chatBubbleCorners: self.presentationData.chatBubbleCorners)) self.eventLogContext = ChannelAdminEventLogContext(postbox: self.context.account.postbox, network: self.context.account.network, peerId: self.peer.id) @@ -146,7 +146,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { switch entry.entry.event.action { case let .changeStickerPack(_, new): if let new = new { - strongSelf.presentController(StickerPackPreviewController(context: strongSelf.context, stickerPack: new, parentNavigationController: strongSelf.getNavigationController()), nil) + strongSelf.presentController(StickerPackScreen(context: strongSelf.context, mainStickerPack: new, stickerPacks: [new], parentNavigationController: strongSelf.getNavigationController()), nil) return true } default: @@ -161,7 +161,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, present: { c, a in self?.presentController(c, a) }, transitionNode: { messageId, media in - var selectedNode: (ASDisplayNode, () -> (UIView?, UIView?))? + var selectedNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? if let strongSelf = self { strongSelf.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { @@ -195,7 +195,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, openMessageContextMenu: { [weak self] message, selectAll, node, frame, _ in self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame) }, openMessageContextActions: { _, _, _, _ in - }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _ in + }, navigateToMessage: { _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _, _ in self?.openUrl(url) }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { @@ -262,7 +262,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } else if canOpenIn { openText = strongSelf.presentationData.strings.Conversation_FileOpenIn } - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: cleanUrl)) @@ -285,13 +285,13 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { })) } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.presentController(actionSheet, nil) case let .peerMention(peerId, mention): - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] if !mention.isEmpty { items.append(ActionSheetTextItem(title: mention)) @@ -309,13 +309,13 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { })) } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.presentController(actionSheet, nil) case let .mention(mention): - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: mention), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in @@ -329,13 +329,13 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { UIPasteboard.general.string = mention }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.presentController(actionSheet, nil) case let .command(command): - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: command), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in @@ -343,13 +343,13 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { UIPasteboard.general.string = command }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.presentController(actionSheet, nil) case let .hashtag(hashtag): - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: hashtag), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in @@ -364,7 +364,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { UIPasteboard.general.string = hashtag }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -373,7 +373,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { guard let message = message else { return } - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: text), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in @@ -387,11 +387,13 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { UIPasteboard.general.string = text }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.presentController(actionSheet, nil) + case .bankCard: + break } } }, openCheckoutOrReceipt: { _ in @@ -403,7 +405,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, requestRedeliveryOfFailedMessages: { _ in }, addContact: { _ in }, rateCall: { _, _ in - }, requestSelectMessagePollOption: { _, _ in + }, requestSelectMessagePollOptions: { _, _ in + }, requestOpenMessagePollResults: { _, _ in }, openAppStorePage: { [weak self] in if let strongSelf = self { strongSelf.context.sharedContext.applicationBindings.openAppStorePage() @@ -417,6 +420,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, updateMessageReaction: { _, _ in }, openMessageReactions: { _ in }, displaySwipeToReplyHint: { + }, dismissReplyMarkupMessage: { _ in + }, openMessagePollResults: { _, _ in + }, openPollCreation: { _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, @@ -492,7 +498,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { strongSelf.presentationData = presentationData - strongSelf.chatPresentationDataPromise.set(.single(ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations, largeEmoji: presentationData.largeEmoji))) + strongSelf.chatPresentationDataPromise.set(.single(ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper), fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners))) strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) } @@ -543,31 +549,11 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { transition.updateFrame(node: self.emptyNode, frame: emptyFrame) self.emptyNode.updateLayout(size: emptyFrame.size, transition: transition) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - let contentBottomInset: CGFloat = panelHeight + 4.0 let listInsets = UIEdgeInsets(top: contentBottomInset, left: layout.safeInsets.right, bottom: insets.top, right: layout.safeInsets.left) - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: listInsets, duration: duration, curve: listViewCurve) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: listInsets, duration: duration, curve: curve) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) @@ -675,7 +661,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { if peer is TelegramChannel, let navigationController = strongSelf.getNavigationController() { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), animated: true)) } else { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { strongSelf.pushController(infoController) } } @@ -697,7 +683,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self { if let peer = peer { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { strongSelf.pushController(infoController) } } @@ -719,6 +705,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { if !channel.hasPermission(.banMembers) { canBan = false } + if case .broadcast = channel.info { + canBan = false + } } for member in adminsState.list { if member.peer.id == author.id { @@ -798,7 +787,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), subject: .message(messageId))) } case let .stickerPack(name): - strongSelf.presentController(StickerPackPreviewController(context: strongSelf.context, stickerPack: .name(name), parentNavigationController: strongSelf.getNavigationController()), nil) + let packReference: StickerPackReference = .name(name) + strongSelf.presentController(StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.getNavigationController()), nil) case let .instantView(webpage, anchor): strongSelf.pushController(InstantPageController(context: strongSelf.context, webPage: webpage, sourcePeerType: .channel, anchor: anchor)) case let .join(link): @@ -806,7 +796,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { if let strongSelf = self { strongSelf.openPeer(peerId: peerId, peer: nil) } - }), nil) + }, parentNavigationController: strongSelf.getNavigationController()), nil) case let .localization(identifier): strongSelf.presentController(LanguageLinkPreviewController(context: strongSelf.context, identifier: identifier), nil) case .proxy, .confirmationCode, .cancelAccountReset, .share: @@ -820,13 +810,17 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self?.presentController(c, a) }, dismissInput: { self?.view.endEditing(true) - }) + }, contentContext: nil) case .wallpaper: break case .theme: break + #if ENABLE_WALLET case .wallet: break + #endif + case .settings: + break } } })) diff --git a/submodules/TelegramUI/TelegramUI/ChatRecentActionsEmptyNode.swift b/submodules/TelegramUI/TelegramUI/ChatRecentActionsEmptyNode.swift index 3d45cd250d..8b4dfd82d5 100644 --- a/submodules/TelegramUI/TelegramUI/ChatRecentActionsEmptyNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatRecentActionsEmptyNode.swift @@ -22,7 +22,7 @@ final class ChatRecentActionsEmptyNode: ASDisplayNode { private var title: String = "" private var text: String = "" - init(theme: PresentationTheme, chatWallpaper: TelegramWallpaper) { + init(theme: PresentationTheme, chatWallpaper: TelegramWallpaper, chatBubbleCorners: PresentationChatBubbleCorners) { self.theme = theme self.chatWallpaper = chatWallpaper @@ -37,7 +37,7 @@ final class ChatRecentActionsEmptyNode: ASDisplayNode { super.init() - let graphics = PresentationResourcesChat.additionalGraphics(theme, wallpaper: chatWallpaper) + let graphics = PresentationResourcesChat.additionalGraphics(theme, wallpaper: chatWallpaper, bubbleCorners: chatBubbleCorners) self.backgroundNode.image = graphics.chatEmptyItemBackgroundImage self.addSubnode(self.backgroundNode) diff --git a/submodules/TelegramUI/TelegramUI/ChatRecentActionsFilterController.swift b/submodules/TelegramUI/TelegramUI/ChatRecentActionsFilterController.swift index f3b8b97688..4095675a6e 100644 --- a/submodules/TelegramUI/TelegramUI/ChatRecentActionsFilterController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatRecentActionsFilterController.swift @@ -13,15 +13,15 @@ import AccountContext import ItemListPeerItem private final class ChatRecentActionsFilterControllerArguments { - let account: Account + let context: AccountContext let toggleAllActions: (Bool) -> Void let toggleAction: ([AdminLogEventsFlags]) -> Void let toggleAllAdmins: (Bool) -> Void let toggleAdmin: (PeerId) -> Void - init(account: Account, toggleAllActions: @escaping (Bool) -> Void, toggleAction: @escaping ([AdminLogEventsFlags]) -> Void, toggleAllAdmins: @escaping (Bool) -> Void, toggleAdmin: @escaping (PeerId) -> Void) { - self.account = account + init(context: AccountContext, toggleAllActions: @escaping (Bool) -> Void, toggleAction: @escaping ([AdminLogEventsFlags]) -> Void, toggleAllAdmins: @escaping (Bool) -> Void, toggleAdmin: @escaping (PeerId) -> Void) { + self.context = context self.toggleAllActions = toggleAllActions self.toggleAction = toggleAction self.toggleAllAdmins = toggleAllAdmins @@ -206,23 +206,23 @@ private enum ChatRecentActionsFilterEntry: ItemListNodeEntry { } } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ChatRecentActionsFilterControllerArguments switch self { case let .actionsTitle(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .allActions(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleAllActions(value) }) case let .actionItem(theme, _, events, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .right, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .right, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.toggleAction(events) }) case let .adminsTitle(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .allAdmins(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleAllAdmins(value) }) case let .adminPeerItem(theme, strings, dateTimeFormat, nameDisplayOrder, _, participant, checked): @@ -233,7 +233,7 @@ private enum ChatRecentActionsFilterEntry: ItemListNodeEntry { case .member: peerText = strings.ChatAdmins_AdminLabel.capitalized } - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: checked, style: .check), enabled: true, selectable: true, sectionId: self.section, action: { + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: participant.peer, presence: nil, text: .text(peerText), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: checked, style: .check), enabled: true, selectable: true, sectionId: self.section, action: { arguments.toggleAdmin(participant.peer.id) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) @@ -380,7 +380,7 @@ public func channelRecentActionsFilterController(context: AccountContext, peer: let actionsDisposable = DisposableSet() - let arguments = ChatRecentActionsFilterControllerArguments(account: context.account, toggleAllActions: { value in + let arguments = ChatRecentActionsFilterControllerArguments(context: context, toggleAllActions: { value in updateState { current in if value { return current.withUpdatedEvents(.all) @@ -486,8 +486,8 @@ public func channelRecentActionsFilterController(context: AccountContext, peer: let previous = previousPeers previousPeers = sortedAdmins - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ChatAdmins_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(entries: channelRecentActionsFilterControllerEntries(presentationData: presentationData, accountPeerId: context.account.peerId, peer: peer, state: state, participants: sortedAdmins), style: .blocks, animateChanges: previous != nil && admins != nil && previous!.count >= sortedAdmins!.count) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatAdmins_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelRecentActionsFilterControllerEntries(presentationData: presentationData, accountPeerId: context.account.peerId, peer: peer, state: state, participants: sortedAdmins), style: .blocks, animateChanges: previous != nil && admins != nil && previous!.count >= sortedAdmins!.count) return (controllerState, (listState, arguments)) } diff --git a/submodules/TelegramUI/TelegramUI/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/TelegramUI/ChatRecentActionsHistoryTransition.swift index 4bb05c55c0..f4aa666109 100644 --- a/submodules/TelegramUI/TelegramUI/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/TelegramUI/ChatRecentActionsHistoryTransition.swift @@ -220,7 +220,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { var photo: TelegramMediaImage? if !new.isEmpty { - photo = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: new, immediateThumbnailData: nil, reference: nil, partialReference: nil) + photo = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: new, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) } let action = TelegramMediaActionType.photoUpdated(image: photo) diff --git a/submodules/TelegramUI/TelegramUI/ChatRecordingPreviewInputPanelNode.swift b/submodules/TelegramUI/TelegramUI/ChatRecordingPreviewInputPanelNode.swift index d2b2509ad3..b1279dc1bd 100644 --- a/submodules/TelegramUI/TelegramUI/ChatRecordingPreviewInputPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatRecordingPreviewInputPanelNode.swift @@ -9,6 +9,7 @@ import SwiftSignalKit import TelegramPresentationData import UniversalMediaPlayer import AppBundle +import ContextUI private func generatePauseIcon(_ theme: PresentationTheme) -> UIImage? { return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPause"), color: theme.chat.inputPanel.actionControlForegroundColor) @@ -38,6 +39,8 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { private let statusDisposable = MetaDisposable() + private var gestureRecognizer: ContextGesture? + init(theme: PresentationTheme) { self.deleteButton = HighlightableButtonNode() self.deleteButton.displaysAsynchronously = false @@ -112,18 +115,18 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { override func didLoad() { super.didLoad() - let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:))) - gestureRecognizer.minimumPressDuration = 0.4 + let gestureRecognizer = ContextGesture(target: nil, action: nil) self.sendButton.view.addGestureRecognizer(gestureRecognizer) - } - - @objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { - if gestureRecognizer.state == .began { - self.interfaceInteraction?.displaySendMessageOptions() + self.gestureRecognizer = gestureRecognizer + gestureRecognizer.activated = { [weak self] gesture in + guard let strongSelf = self else { + return + } + strongSelf.interfaceInteraction?.displaySendMessageOptions(strongSelf.sendButton, gesture) } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { if self.presentationInterfaceState != interfaceState { var updateWaveform = false if self.presentationInterfaceState?.recordedMediaPreview != interfaceState.recordedMediaPreview { diff --git a/submodules/TelegramUI/TelegramUI/ChatReportPeerTitlePanelNode.swift b/submodules/TelegramUI/TelegramUI/ChatReportPeerTitlePanelNode.swift index df0528f89e..ef1b9e7aec 100644 --- a/submodules/TelegramUI/TelegramUI/ChatReportPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatReportPeerTitlePanelNode.swift @@ -13,6 +13,7 @@ private enum ChatReportPeerTitleButton: Equatable { case addContact(String?) case shareMyPhoneNumber case reportSpam + case reportUserSpam case reportIrrelevantGeoLocation func title(strings: PresentationStrings) -> String { @@ -29,6 +30,8 @@ private enum ChatReportPeerTitleButton: Equatable { return strings.Conversation_ShareMyPhoneNumber case .reportSpam: return strings.Conversation_ReportSpamAndLeave + case .reportUserSpam: + return strings.Conversation_ReportSpam case .reportIrrelevantGeoLocation: return strings.Conversation_ReportGroupLocation } @@ -49,6 +52,16 @@ private func peerButtons(_ state: ChatPresentationInterfaceState) -> [ChatReport } else { buttons.append(.addContact(nil)) } + } else { + if peerStatusSettings.contains(.canBlock) || peerStatusSettings.contains(.canReport) { + if peer.isDeleted { + buttons.append(.reportUserSpam) + } else { + if !state.peerIsBlocked { + buttons.append(.block) + } + } + } } if buttons.isEmpty { if peerStatusSettings.contains(.canShareContact) { @@ -134,7 +147,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { view.setTitle(button.title(strings: interfaceState.strings), for: []) view.titleLabel?.font = Font.regular(16.0) switch button { - case .block, .reportSpam: + case .block, .reportSpam, .reportUserSpam: view.setTitleColor(interfaceState.theme.chat.inputPanel.panelControlDestructiveColor, for: []) view.setTitleColor(interfaceState.theme.chat.inputPanel.panelControlDestructiveColor.withAlphaComponent(0.7), for: [.highlighted]) default: @@ -188,7 +201,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { switch button { case .shareMyPhoneNumber: self.interfaceInteraction?.shareAccountContact() - case .block, .reportSpam: + case .block, .reportSpam, .reportUserSpam: self.interfaceInteraction?.reportPeer() case .addContact: self.interfaceInteraction?.presentPeerContact() diff --git a/submodules/TelegramUI/TelegramUI/ChatRestrictedInputPanelNode.swift b/submodules/TelegramUI/TelegramUI/ChatRestrictedInputPanelNode.swift index 11764aa82e..32b6829c4a 100644 --- a/submodules/TelegramUI/TelegramUI/ChatRestrictedInputPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatRestrictedInputPanelNode.swift @@ -23,7 +23,7 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { self.addSubnode(self.textNode) } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState } diff --git a/submodules/TelegramUI/TelegramUI/ChatScheduleTimeController.swift b/submodules/TelegramUI/TelegramUI/ChatScheduleTimeController.swift index 6638eb88e1..641e481c43 100644 --- a/submodules/TelegramUI/TelegramUI/ChatScheduleTimeController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatScheduleTimeController.swift @@ -21,6 +21,7 @@ final class ChatScheduleTimeController: ViewController { private var animatedIn = false private let context: AccountContext + private let peerId: PeerId private let mode: ChatScheduleTimeControllerMode private let currentTime: Int32? private let minimalTime: Int32? @@ -29,10 +30,11 @@ final class ChatScheduleTimeController: ViewController { private var presentationDataDisposable: Disposable? - init(context: AccountContext, mode: ChatScheduleTimeControllerMode, currentTime: Int32? = nil, minimalTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) { + init(context: AccountContext, peerId: PeerId, mode: ChatScheduleTimeControllerMode, currentTime: Int32? = nil, minimalTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) { self.context = context + self.peerId = peerId self.mode = mode - self.currentTime = currentTime + self.currentTime = currentTime != scheduleWhenOnlineTimestamp ? currentTime : nil self.minimalTime = minimalTime self.dismissByTapOutside = dismissByTapOutside self.completion = completion @@ -64,8 +66,11 @@ final class ChatScheduleTimeController: ViewController { override public func loadDisplayNode() { self.displayNode = ChatScheduleTimeControllerNode(context: self.context, mode: self.mode, currentTime: self.currentTime, minimalTime: self.minimalTime, dismissByTapOutside: self.dismissByTapOutside) self.controllerNode.completion = { [weak self] time in - self?.completion(time != scheduleWhenOnlineTimestamp ? time + 5 : time) - self?.dismiss() + guard let strongSelf = self else { + return + } + strongSelf.completion(time == scheduleWhenOnlineTimestamp ? time : time + 5) + strongSelf.dismiss() } self.controllerNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) diff --git a/submodules/TelegramUI/TelegramUI/ChatScheduleTimeControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatScheduleTimeControllerNode.swift index b8174870dd..e69a824772 100644 --- a/submodules/TelegramUI/TelegramUI/ChatScheduleTimeControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatScheduleTimeControllerNode.swift @@ -103,7 +103,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel self.contentContainerNode.addSubnode(self.cancelButton) self.contentContainerNode.addSubnode(self.doneButton) if case .scheduledMessages(true) = self.mode { - //self.contentContainerNode.addSubnode(self.onlineButton) + self.contentContainerNode.addSubnode(self.onlineButton) } self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) @@ -316,7 +316,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel var buttonOffset: CGFloat = 0.0 if case .scheduledMessages(true) = self.mode { - //buttonOffset += 60.0 + buttonOffset += 60.0 } let bottomInset: CGFloat = 10.0 + cleanInsets.bottom @@ -352,7 +352,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - buttonHeight - insets.bottom - 10.0 - buttonOffset, width: contentFrame.width, height: buttonHeight)) let onlineSize = self.onlineButton.measure(CGSize(width: width, height: titleHeight)) - let onlineFrame = CGRect(origin: CGPoint(x: ceil((layout.size.width - onlineSize.width) / 2.0), y: contentHeight - 45.0 - insets.bottom), size: onlineSize) + let onlineFrame = CGRect(origin: CGPoint(x: ceil((contentFrame.width - onlineSize.width) / 2.0), y: contentHeight - 45.0 - insets.bottom), size: onlineSize) transition.updateFrame(node: self.onlineButton, frame: onlineFrame) self.pickerView?.frame = CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: contentFrame.width, height: pickerHeight)) diff --git a/submodules/TelegramUI/TelegramUI/ChatSearchInputPanelNode.swift b/submodules/TelegramUI/TelegramUI/ChatSearchInputPanelNode.swift index d66c1d5c96..aaaedf9446 100644 --- a/submodules/TelegramUI/TelegramUI/ChatSearchInputPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatSearchInputPanelNode.swift @@ -28,7 +28,7 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { private var needsSearchResultsTooltip = true - private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, LayoutMetrics)? + private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, LayoutMetrics, Bool)? override var interfaceInteraction: ChatPanelInterfaceInteraction? { didSet { @@ -39,7 +39,7 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { strongSelf.displayActivity = value strongSelf.activityIndicator.isHidden = !value if let interfaceState = strongSelf.presentationInterfaceState, let validLayout = strongSelf.validLayout { - strongSelf.updateLayout(width: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, maxHeight: validLayout.3, transition: .immediate, interfaceState: interfaceState, metrics: validLayout.4) + strongSelf.updateLayout(width: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, maxHeight: validLayout.3, isSecondary: validLayout.5, transition: .immediate, interfaceState: interfaceState, metrics: validLayout.4) } } })) @@ -125,8 +125,8 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - self.validLayout = (width, leftInset, rightInset, maxHeight, metrics) + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + self.validLayout = (width, leftInset, rightInset, maxHeight, metrics, isSecondary) if self.presentationInterfaceState != interfaceState { let themeUpdated = self.presentationInterfaceState?.theme !== interfaceState.theme diff --git a/submodules/TelegramUI/TelegramUI/ChatSearchNavigationContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatSearchNavigationContentNode.swift index 1d1523fc78..9a504c8862 100644 --- a/submodules/TelegramUI/TelegramUI/ChatSearchNavigationContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatSearchNavigationContentNode.swift @@ -8,6 +8,7 @@ import SyncCore import TelegramPresentationData import SearchBarNode import LocalizedPeerData +import SwiftSignalKit private let searchBarFont = Font.regular(17.0) @@ -19,6 +20,8 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode { private let searchBar: SearchBarNode private let interaction: ChatPanelInterfaceInteraction + private var searchingActivityDisposable: Disposable? + init(theme: PresentationTheme, strings: PresentationStrings, chatLocation: ChatLocation, interaction: ChatPanelInterfaceInteraction) { self.theme = theme self.strings = strings @@ -51,6 +54,17 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode { self.searchBar.clearPrefix = { [weak self] in self?.interaction.toggleMembersSearch(false) } + + if let statuses = interaction.statuses { + self.searchingActivityDisposable = (statuses.searching + |> deliverOnMainQueue).start(next: { [weak self] value in + self?.searchBar.activity = value + }) + } + } + + deinit { + self.searchingActivityDisposable?.dispose() } override var nominalHeight: CGFloat { diff --git a/submodules/TelegramUI/TelegramUI/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/TelegramUI/ChatSearchResultsContollerNode.swift index b5f82bd021..eaf72dfcfe 100644 --- a/submodules/TelegramUI/TelegramUI/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatSearchResultsContollerNode.swift @@ -11,6 +11,8 @@ import TelegramStringFormatting import MergeLists import ChatListUI import AccountContext +import ContextUI +import ChatListSearchItemHeader private enum ChatListSearchEntryStableId: Hashable { case messageId(MessageId) @@ -76,7 +78,7 @@ private enum ChatListSearchEntry: Comparable, Identifiable { public func item(context: AccountContext, interaction: ChatListNodeInteraction) -> ListViewItem { switch self { case let .message(message, peer, readState, presentationData): - return ChatListItem(presentationData: presentationData, context: context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: message.index), content: .peer(message: message, peer: peer, combinedReadState: readState, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: true, displayAsMessage: true), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) + return ChatListItem(presentationData: presentationData, context: context, peerGroupId: .root, index: ChatListIndex(pinningIndex: nil, messageIndex: message.index), content: .peer(message: message, peer: peer, combinedReadState: readState, notificationSettings: nil, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(), embeddedState: nil, inputActivities: nil, isAd: false, ignoreUnreadBadge: true, displayAsMessage: true, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) } } } @@ -129,14 +131,14 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe private let previousEntries = Atomic<[ChatListSearchEntry]?>(value: nil) - init(context: AccountContext, location: SearchMessagesLocation, searchQuery: String, searchResult: SearchMessagesResult, searchState: SearchMessagesState) { + init(context: AccountContext, location: SearchMessagesLocation, searchQuery: String, searchResult: SearchMessagesResult, searchState: SearchMessagesState, presentInGlobalOverlay: @escaping (ViewController) -> Void) { self.context = context self.location = location self.searchQuery = searchQuery self.searchResult = searchResult self.searchState = searchState self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.presentationDataPromise = Promise(ChatListPresentationData(theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations)) + self.presentationDataPromise = Promise(ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations)) self.listNode = ListView() self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor @@ -166,6 +168,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe let interaction = ChatListNodeInteraction(activateSearch: { }, peerSelected: { _ in + }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, messageSelected: { [weak self] peer, message, _ in if let strongSelf = self { @@ -183,7 +186,24 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in }, toggleArchivedFolderHiddenByDefault: { - }, activateChatPreview: { _, _, _ in + }, activateChatPreview: { [weak self] item, node, gesture in + guard let strongSelf = self else { + gesture?.cancel() + return + } + switch item.content { + case let .peer(peer): + if let message = peer.message { + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peer.peer.peerId), subject: .message(message.id), botStart: nil, mode: .standard(previewing: true)) + chatController.canReadHistory.set(false) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single([]), reactionItems: [], gesture: gesture) + presentInGlobalOverlay(contextController) + } else { + gesture?.cancel() + } + default: + gesture?.cancel() + } }) interaction.searchTextHighightState = searchQuery self.interaction = interaction @@ -273,7 +293,13 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe func updatePresentationData(_ presentationData: PresentationData) { let previousTheme = self.presentationData.theme self.presentationData = presentationData - self.presentationDataPromise.set(.single(ChatListPresentationData(theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations))) + self.presentationDataPromise.set(.single(ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations))) + + self.listNode.forEachItemHeaderNode({ itemHeaderNode in + if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode { + itemHeaderNode.updateTheme(theme: presentationData.theme) + } + }) } private func enqueueTransition(_ transition: ChatListSearchContainerTransition, firstTime: Bool) { @@ -305,30 +331,10 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe let topInset = navigationBarHeight - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if !hadValidLayout { while !self.enqueuedTransitions.isEmpty { @@ -337,3 +343,32 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe } } } + +private final class ContextControllerContentSourceImpl: ContextControllerContentSource { + let controller: ViewController + weak var sourceNode: ASDisplayNode? + + let navigationController: NavigationController? = nil + + let passthroughTouches: Bool = true + + 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() { + } +} + diff --git a/submodules/TelegramUI/TelegramUI/ChatSearchResultsController.swift b/submodules/TelegramUI/TelegramUI/ChatSearchResultsController.swift index fc3a3c14c4..313a5eb0dd 100644 --- a/submodules/TelegramUI/TelegramUI/ChatSearchResultsController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatSearchResultsController.swift @@ -64,7 +64,9 @@ final class ChatSearchResultsController: ViewController { } override public func loadDisplayNode() { - self.displayNode = ChatSearchResultsControllerNode(context: self.context, location: self.location, searchQuery: self.searchQuery, searchResult: self.searchResult, searchState: self.searchState) + self.displayNode = ChatSearchResultsControllerNode(context: self.context, location: self.location, searchQuery: self.searchQuery, searchResult: self.searchResult, searchState: self.searchState, presentInGlobalOverlay: { [weak self] c in + self?.presentInGlobalOverlay(c) + }) self.controllerNode.resultSelected = { [weak self] messageIndex in self?.navigateToMessageIndex(messageIndex) self?.dismiss() diff --git a/submodules/TelegramUI/TelegramUI/ChatSecretAutoremoveTimerActionSheet.swift b/submodules/TelegramUI/TelegramUI/ChatSecretAutoremoveTimerActionSheet.swift index c51fa40377..367006984c 100644 --- a/submodules/TelegramUI/TelegramUI/ChatSecretAutoremoveTimerActionSheet.swift +++ b/submodules/TelegramUI/TelegramUI/ChatSecretAutoremoveTimerActionSheet.swift @@ -23,11 +23,11 @@ final class ChatSecretAutoremoveTimerActionSheetController: ActionSheetControlle let theme = presentationData.theme let strings = presentationData.strings - super.init(theme: ActionSheetControllerTheme(presentationTheme: theme)) + super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationTheme: presentationData.theme) + strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) } }) diff --git a/submodules/TelegramUI/TelegramUI/ChatSendMessageActionSheetController.swift b/submodules/TelegramUI/TelegramUI/ChatSendMessageActionSheetController.swift index ea44a0a7bc..b3dc91fb9b 100644 --- a/submodules/TelegramUI/TelegramUI/ChatSendMessageActionSheetController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatSendMessageActionSheetController.swift @@ -5,6 +5,7 @@ import AsyncDisplayKit import SwiftSignalKit import TelegramPresentationData import AccountContext +import ContextUI final class ChatSendMessageActionSheetController: ViewController { var controllerNode: ChatSendMessageActionSheetControllerNode { @@ -14,6 +15,7 @@ final class ChatSendMessageActionSheetController: ViewController { private let context: AccountContext private let controllerInteraction: ChatControllerInteraction? private let interfaceState: ChatPresentationInterfaceState + private let gesture: ContextGesture private let sendButtonFrame: CGRect private let textInputNode: EditableTextNode private let completion: () -> Void @@ -26,10 +28,11 @@ final class ChatSendMessageActionSheetController: ViewController { private let hapticFeedback = HapticFeedback() - init(context: AccountContext, controllerInteraction: ChatControllerInteraction?, interfaceState: ChatPresentationInterfaceState, sendButtonFrame: CGRect, textInputNode: EditableTextNode, completion: @escaping () -> Void) { + init(context: AccountContext, controllerInteraction: ChatControllerInteraction?, interfaceState: ChatPresentationInterfaceState, gesture: ContextGesture, sendButtonFrame: CGRect, textInputNode: EditableTextNode, completion: @escaping () -> Void) { self.context = context self.controllerInteraction = controllerInteraction self.interfaceState = interfaceState + self.gesture = gesture self.sendButtonFrame = sendButtonFrame self.textInputNode = textInputNode self.completion = completion @@ -68,7 +71,7 @@ final class ChatSendMessageActionSheetController: ViewController { reminders = true } - self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, reminders: reminders, sendButtonFrame: self.sendButtonFrame, textInputNode: self.textInputNode, forwardedCount: forwardedCount, send: { [weak self] in + self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, reminders: reminders, gesture: gesture, sendButtonFrame: self.sendButtonFrame, textInputNode: self.textInputNode, forwardedCount: forwardedCount, send: { [weak self] in self?.controllerInteraction?.sendCurrentMessage(false) self?.dismiss(cancel: false) }, sendSilently: { [weak self] in @@ -107,6 +110,7 @@ final class ChatSendMessageActionSheetController: ViewController { } private func dismiss(cancel: Bool) { + self.statusBar.statusBarStyle = .Ignore self.controllerNode.animateOut(cancel: cancel, completion: { [weak self] in self?.completion() self?.didPlayPresentationAnimation = false diff --git a/submodules/TelegramUI/TelegramUI/ChatSendMessageActionSheetControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatSendMessageActionSheetControllerNode.swift index 8b9b07248d..9c845b4699 100644 --- a/submodules/TelegramUI/TelegramUI/ChatSendMessageActionSheetControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatSendMessageActionSheetControllerNode.swift @@ -9,6 +9,7 @@ import SyncCore import TelegramPresentationData import AccountContext import AppBundle +import ContextUI private let leftInset: CGFloat = 16.0 private let rightInset: CGFloat = 16.0 @@ -32,7 +33,7 @@ private enum ChatSendMessageActionIcon { private final class ActionSheetItemNode: ASDisplayNode { private let title: String private let icon: ChatSendMessageActionIcon - private let action: () -> Void + let action: () -> Void private let separatorNode: ASDisplayNode private let backgroundNode: ASDisplayNode @@ -92,13 +93,23 @@ private final class ActionSheetItemNode: ASDisplayNode { self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) self.buttonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { - if highlighted { - strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.highlightedBackgroundNode.alpha = 1.0 - } else { - strongSelf.highlightedBackgroundNode.alpha = 0.0 - strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - } + strongSelf.setHighlighted(highlighted, animated: true) + } + } + } + + func setHighlighted(_ highlighted: Bool, animated: Bool) { + if highlighted == (self.highlightedBackgroundNode.alpha == 1.0) { + return + } + + if highlighted { + self.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity") + self.highlightedBackgroundNode.alpha = 1.0 + } else { + self.highlightedBackgroundNode.alpha = 0.0 + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) } } } @@ -170,7 +181,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, private var validLayout: ContainerViewLayout? - init(context: AccountContext, reminders: Bool, sendButtonFrame: CGRect, textInputNode: EditableTextNode, forwardedCount: Int?, send: (() -> Void)?, sendSilently: (() -> Void)?, schedule: (() -> Void)?, cancel: (() -> Void)?) { + init(context: AccountContext, reminders: Bool, gesture: ContextGesture, sendButtonFrame: CGRect, textInputNode: EditableTextNode, forwardedCount: Int?, send: (() -> Void)?, sendSilently: (() -> Void)?, schedule: (() -> Void)?, cancel: (() -> Void)?) { self.context = context self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.sendButtonFrame = sendButtonFrame @@ -254,14 +265,17 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.toMessageTextNode.attributedText = toAttributedText } } else { - self.fromMessageTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.Conversation_InputTextPlaceholder, attributes: [NSAttributedString.Key.foregroundColor: self.presentationData.theme.chat.inputPanel.inputPlaceholderColor, NSAttributedString.Key.font: Font.regular(self.presentationData.fontSize.baseDisplaySize)]) + self.fromMessageTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.Conversation_InputTextPlaceholder, attributes: [NSAttributedString.Key.foregroundColor: self.presentationData.theme.chat.inputPanel.inputPlaceholderColor, NSAttributedString.Key.font: Font.regular(self.presentationData.chatFontSize.baseDisplaySize)]) - self.toMessageTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.ForwardedMessages(Int32(forwardedCount ?? 0)), attributes: [NSAttributedString.Key.foregroundColor: self.presentationData.theme.chat.message.outgoing.primaryTextColor, NSAttributedString.Key.font: Font.regular(self.presentationData.fontSize.baseDisplaySize)]) + self.toMessageTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.ForwardedMessages(Int32(forwardedCount ?? 0)), attributes: [NSAttributedString.Key.foregroundColor: self.presentationData.theme.chat.message.outgoing.primaryTextColor, NSAttributedString.Key.font: Font.regular(self.presentationData.chatFontSize.baseDisplaySize)]) } self.messageBackgroundNode.contentMode = .scaleToFill let outgoing: PresentationThemeBubbleColorComponents = self.presentationData.chatWallpaper.isEmpty ? self.presentationData.theme.chat.message.outgoing.bubble.withoutWallpaper : self.presentationData.theme.chat.message.outgoing.bubble.withWallpaper - self.messageBackgroundNode.image = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: self.presentationData.theme.chat, wallpaper: self.presentationData.chatWallpaper, knockout: false) + + let maxCornerRadius = self.presentationData.chatBubbleCorners.mainRadius + let minCornerRadius = self.presentationData.chatBubbleCorners.auxiliaryRadius + self.messageBackgroundNode.image = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: maxCornerRadius, incoming: false, fillColor: outgoing.gradientFill, strokeColor: outgoing.fill == outgoing.gradientFill ? outgoing.stroke : .clear, neighbors: .none, theme: self.presentationData.theme.chat, wallpaper: self.presentationData.chatWallpaper, knockout: false) self.view.addSubview(self.effectView) self.addSubnode(self.dimNode) @@ -280,6 +294,38 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, } self.contentNodes.forEach(self.contentContainerNode.addSubnode) + + gesture.externalUpdated = { [weak self] view, location in + guard let strongSelf = self else { + return + } + for contentNode in strongSelf.contentNodes { + let localPoint = contentNode.view.convert(location, from: view) + if contentNode.bounds.contains(localPoint) { + contentNode.setHighlighted(true, animated: false) + } else { + contentNode.setHighlighted(false, animated: false) + } + } + } + + gesture.externalEnded = { [weak self] viewAndLocation in + guard let strongSelf = self else { + return + } + for contentNode in strongSelf.contentNodes { + if let (view, location) = viewAndLocation { + let localPoint = contentNode.view.convert(location, from: view) + if contentNode.bounds.contains(localPoint) { + contentNode.action() + } else { + contentNode.setHighlighted(false, animated: false) + } + } else { + contentNode.setHighlighted(false, animated: false) + } + } + } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -333,7 +379,9 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, } let outgoing: PresentationThemeBubbleColorComponents = self.presentationData.chatWallpaper.isEmpty ? self.presentationData.theme.chat.message.outgoing.bubble.withoutWallpaper : self.presentationData.theme.chat.message.outgoing.bubble.withWallpaper - self.messageBackgroundNode.image = messageBubbleImage(incoming: false, fillColor: outgoing.fill, strokeColor: outgoing.stroke, neighbors: .none, theme: self.presentationData.theme.chat, wallpaper: self.presentationData.chatWallpaper, knockout: false) + let maxCornerRadius = self.presentationData.chatBubbleCorners.mainRadius + let minCornerRadius = self.presentationData.chatBubbleCorners.auxiliaryRadius + self.messageBackgroundNode.image = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: maxCornerRadius, incoming: false, fillColor: outgoing.gradientFill, strokeColor: outgoing.fill == outgoing.gradientFill ? outgoing.stroke : .clear, neighbors: .none, theme: self.presentationData.theme.chat, wallpaper: self.presentationData.chatWallpaper, knockout: false) for node in self.contentNodes { node.updateTheme(presentationData.theme) @@ -395,11 +443,13 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, } self.fromMessageTextNode.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.toMessageTextNode.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + + let contentOffset = CGPoint(x: self.sendButtonFrame.midX - self.contentContainerNode.frame.midX, y: self.sendButtonFrame.midY - self.contentContainerNode.frame.midY) let springDuration: Double = 0.42 let springDamping: CGFloat = 104.0 self.contentContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) - self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: 160.0, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) } } @@ -499,8 +549,10 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, completedBubble = true } - self.contentContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 160.0, y: 0.0), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - self.contentContainerNode.layer.animateScale(from: 1.0, to: 0.4, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + let contentOffset = CGPoint(x: self.sendButtonFrame.midX - self.contentContainerNode.frame.midX, y: self.sendButtonFrame.midY - self.contentContainerNode.frame.midY) + + self.contentContainerNode.layer.animatePosition(from: CGPoint(), to: contentOffset, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + self.contentContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) } } @@ -514,7 +566,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.validLayout = layout transition.updateFrame(node: self.textCoverNode, frame: self.textFieldFrame) - transition.updateFrame(node: self.buttonCoverNode, frame: self.sendButtonFrame.offsetBy(dx: 1.0, dy: 0.0)) + transition.updateFrame(node: self.buttonCoverNode, frame: self.sendButtonFrame.offsetBy(dx: 1.0, dy: 1.0)) transition.updateFrame(view: self.effectView, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) diff --git a/submodules/TelegramUI/TelegramUI/ChatSlowmodeHintController.swift b/submodules/TelegramUI/TelegramUI/ChatSlowmodeHintController.swift index fad91ee725..06cbd2a77d 100644 --- a/submodules/TelegramUI/TelegramUI/ChatSlowmodeHintController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatSlowmodeHintController.swift @@ -21,10 +21,10 @@ final class ChatSlowmodeHintController: TooltipController { private var timer: SwiftSignalKit.Timer? - init(strings: PresentationStrings, slowmodeState: ChatSlowmodeState) { - self.strings = strings + init(presentationData: PresentationData, slowmodeState: ChatSlowmodeState) { + self.strings = presentationData.strings self.slowmodeState = slowmodeState - super.init(content: .text(timeoutValue(strings: strings, slowmodeState: slowmodeState)), timeout: 2.0, dismissByTapOutside: false, dismissByTapOutsideSource: true) + super.init(content: .text(timeoutValue(strings: presentationData.strings, slowmodeState: slowmodeState)), baseFontSize: presentationData.listsFontSize.baseDisplaySize, timeout: 2.0, dismissByTapOutside: false, dismissByTapOutsideSource: true) } required init(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramUI/TelegramUI/ChatTextInputActionButtonsNode.swift b/submodules/TelegramUI/TelegramUI/ChatTextInputActionButtonsNode.swift index 9aebf3c921..da2860a01a 100644 --- a/submodules/TelegramUI/TelegramUI/ChatTextInputActionButtonsNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatTextInputActionButtonsNode.swift @@ -3,20 +3,21 @@ import UIKit import AsyncDisplayKit import Display import TelegramPresentationData +import ContextUI final class ChatTextInputActionButtonsNode: ASDisplayNode { private let strings: PresentationStrings let micButton: ChatTextInputMediaRecordingButton - let sendButton: HighlightTrackingButton + let sendButton: HighlightTrackingButtonNode var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode? var sendButtonHasApplyIcon = false var animatingSendButton = false let expandMediaInputButton: HighlightableButtonNode - var sendButtonLongPressed: (() -> Void)? + var sendButtonLongPressed: ((ASDisplayNode, ContextGesture) -> Void)? - private var gestureRecognizer: UILongPressGestureRecognizer? + private var gestureRecognizer: ContextGesture? var sendButtonLongPressEnabled = false { didSet { self.gestureRecognizer?.isEnabled = self.sendButtonLongPressEnabled @@ -27,9 +28,9 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode { self.strings = strings self.micButton = ChatTextInputMediaRecordingButton(theme: theme, presentController: presentController) - self.sendButton = HighlightTrackingButton() - self.sendButton.adjustsImageWhenHighlighted = false - self.sendButton.adjustsImageWhenDisabled = false + self.sendButton = HighlightTrackingButtonNode() + //self.sendButton.adjustsImageWhenHighlighted = false + //self.sendButton.adjustsImageWhenDisabled = false self.expandMediaInputButton = HighlightableButtonNode() @@ -59,22 +60,29 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode { } self.view.addSubview(self.micButton) - self.view.addSubview(self.sendButton) + self.addSubnode(self.sendButton) self.addSubnode(self.expandMediaInputButton) } override func didLoad() { super.didLoad() - let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:))) - gestureRecognizer.minimumPressDuration = 0.4 + let gestureRecognizer = ContextGesture(target: nil, action: nil) self.gestureRecognizer = gestureRecognizer - self.sendButton.addGestureRecognizer(gestureRecognizer) + self.sendButton.view.addGestureRecognizer(gestureRecognizer) + gestureRecognizer.activated = { [weak self] recognizer in + guard let strongSelf = self else { + return + } + if !strongSelf.sendButtonHasApplyIcon { + strongSelf.sendButtonLongPressed?(strongSelf.sendButton, recognizer) + } + } } @objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { if !self.sendButtonHasApplyIcon && gestureRecognizer.state == .began { - self.sendButtonLongPressed?() + //self.sendButtonLongPressed?() } } diff --git a/submodules/TelegramUI/TelegramUI/ChatTextInputPanelNode.swift b/submodules/TelegramUI/TelegramUI/ChatTextInputPanelNode.swift index 345b205cd2..e287057b97 100644 --- a/submodules/TelegramUI/TelegramUI/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatTextInputPanelNode.swift @@ -225,7 +225,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = [] - private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, LayoutMetrics)? + private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, LayoutMetrics, Bool)? var displayAttachmentMenu: () -> Void = { } var sendMessage: () -> Void = { } @@ -267,6 +267,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + var enablePredictiveInput: Bool = true { + didSet { + if let textInputNode = self.textInputNode { + textInputNode.textView.autocorrectionType = self.enablePredictiveInput ? .default : .no + } + } + } + override var context: AccountContext? { didSet { self.actionButtons.micButton.account = self.context?.account @@ -396,8 +404,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.addSubnode(self.actionButtons) - self.actionButtons.sendButtonLongPressed = { [weak self] in - self?.interfaceInteraction?.displaySendMessageOptions() + self.actionButtons.sendButtonLongPressed = { [weak self] node, gesture in + self?.interfaceInteraction?.displaySendMessageOptions(node, gesture) } self.actionButtons.micButton.recordingDisabled = { [weak self] in @@ -431,8 +439,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } self.actionButtons.micButton.offsetRecordingControls = { [weak self] in if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState { - if let (width, leftInset, rightInset, maxHeight, metrics) = strongSelf.validLayout { - let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics) + if let (width, leftInset, rightInset, maxHeight, metrics, isSecondary) = strongSelf.validLayout { + let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics) } } } @@ -452,7 +460,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - self.actionButtons.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), for: .touchUpInside) + self.actionButtons.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), forControlEvents: .touchUpInside) self.actionButtons.sendButton.alpha = 0.0 self.actionButtons.updateAccessibility() @@ -633,8 +641,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return minimalHeight } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - self.validLayout = (width, leftInset, rightInset, maxHeight, metrics) + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + self.validLayout = (width, leftInset, rightInset, maxHeight, metrics, isSecondary) let baseWidth = width - leftInset - rightInset var wasEditingMedia = false @@ -716,7 +724,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, metrics: metrics) let minimalInputHeight: CGFloat = 2.0 + textFieldMinHeight - self.textInputBackgroundNode.image = textInputBackgroundImage(backgroundColor: interfaceState.theme.chat.inputPanel.panelBackgroundColor, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight) + + let backgroundColor: UIColor + if case let .color(color) = interfaceState.chatWallpaper, UIColor(rgb: color).isEqual(interfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper) { + backgroundColor = interfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper + } else { + backgroundColor = interfaceState.theme.chat.inputPanel.panelBackgroundColor + } + + self.textInputBackgroundNode.image = textInputBackgroundImage(backgroundColor: backgroundColor, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight) self.searchLayoutClearButton.setImage(PresentationResourcesChat.chatInputTextFieldClearImage(interfaceState.theme), for: []) @@ -785,17 +801,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if updateSendButtonIcon { if !self.actionButtons.animatingSendButton { - if transition.isAnimated && !self.actionButtons.sendButton.alpha.isZero && self.actionButtons.sendButton.layer.animation(forKey: "opacity") == nil, let imageView = self.actionButtons.sendButton.imageView, let previousImage = imageView.image { + let imageNode = self.actionButtons.sendButton.imageNode + + if transition.isAnimated && !self.actionButtons.sendButton.alpha.isZero && self.actionButtons.sendButton.layer.animation(forKey: "opacity") == nil, let previousImage = imageNode.image { let tempView = UIImageView(image: previousImage) - self.actionButtons.sendButton.addSubview(tempView) - tempView.frame = imageView.frame + self.actionButtons.sendButton.view.addSubview(tempView) + tempView.frame = imageNode.frame tempView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak tempView] _ in tempView?.removeFromSuperview() }) tempView.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2, removeOnCompletion: false) - imageView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - imageView.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2) + imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + imageNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2) } self.actionButtons.sendButtonHasApplyIcon = sendButtonHasApplyIcon if self.actionButtons.sendButtonHasApplyIcon { @@ -1395,7 +1413,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } private func updateTextHeight(animated: Bool) { - if let (width, leftInset, rightInset, maxHeight, metrics) = self.validLayout { + if let (width, leftInset, rightInset, maxHeight, metrics, _) = self.validLayout { let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset, maxHeight: maxHeight, metrics: metrics) let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) if !self.bounds.size.height.isEqual(to: panelHeight) { @@ -1405,7 +1423,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } @objc func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { - if self.actionButtons.sendButton.superview != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendButton.alpha.isZero { + if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendButton.alpha.isZero { self.sendButtonPressed() } return false @@ -1485,7 +1503,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { @objc func _showTextStyleOptions(_ sender: Any) { if let textInputNode = self.textInputNode { - self.inputMenu.format(view: textInputNode.view, rect: textInputNode.selectionRect.insetBy(dx: 0.0, dy: -1.0)) + self.inputMenu.format(view: textInputNode.view, rect: textInputNode.selectionRect.offsetBy(dx: 0.0, dy: -textInputNode.textView.contentOffset.y).insetBy(dx: 0.0, dy: -1.0)) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatTextLinkEditController.swift b/submodules/TelegramUI/TelegramUI/ChatTextLinkEditController.swift index cb22e5ca92..0a10a4e024 100644 --- a/submodules/TelegramUI/TelegramUI/ChatTextLinkEditController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatTextLinkEditController.swift @@ -398,7 +398,7 @@ func chatTextLinkEditController(sharedContext: SharedAccountContext, account: Ac applyImpl?() })] - let contentNode = ChatTextLinkEditAlertContentNode(theme: AlertControllerTheme(presentationTheme: presentationData.theme), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, link: link) + let contentNode = ChatTextLinkEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, link: link) contentNode.complete = { applyImpl?() } @@ -415,9 +415,9 @@ func chatTextLinkEditController(sharedContext: SharedAccountContext, account: Ac } } - let controller = AlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), contentNode: contentNode) + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) let presentationDataDisposable = sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in - controller?.theme = AlertControllerTheme(presentationTheme: presentationData.theme) + controller?.theme = AlertControllerTheme(presentationData: presentationData) contentNode?.inputFieldNode.updateTheme(presentationData.theme) }) controller.dismissed = { diff --git a/submodules/TelegramUI/TelegramUI/ChatTitleView.swift b/submodules/TelegramUI/TelegramUI/ChatTitleView.swift index 04ae85c66b..2202a0e001 100644 --- a/submodules/TelegramUI/TelegramUI/ChatTitleView.swift +++ b/submodules/TelegramUI/TelegramUI/ChatTitleView.swift @@ -15,6 +15,7 @@ import PeerPresenceStatusManager import ChatTitleActivityNode import LocalizedPeerData import PhoneNumberFormat +import ChatTitleActivityNode enum ChatTitleContent { case peer(peerView: PeerView, onlineMemberCount: Int32?, isScheduledMessages: Bool) @@ -59,7 +60,7 @@ private final class ChatTitleNetworkStatusNode: ASDisplayNode { func updateTheme(theme: PresentationTheme) { self.theme = theme - self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.medium(24.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) self.activityIndicator.type = .custom(self.theme.rootController.navigationBar.primaryTextColor, 22.0, 1.5, false) } @@ -92,11 +93,11 @@ final class ChatTitleView: UIView, NavigationBarTitleView { private var nameDisplayOrder: PresentationPersonNameOrder private let contentContainer: ASDisplayNode - private let titleNode: ImmediateTextNode - private let titleLeftIconNode: ASImageNode - private let titleRightIconNode: ASImageNode - private let titleCredibilityIconNode: ASImageNode - private let activityNode: ChatTitleActivityNode + let titleNode: ImmediateTextNode + let titleLeftIconNode: ASImageNode + let titleRightIconNode: ASImageNode + let titleCredibilityIconNode: ASImageNode + let activityNode: ChatTitleActivityNode private let button: HighlightTrackingButtonNode @@ -106,7 +107,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { private var titleRightIcon: ChatTitleIcon = .none private var titleScamIcon = false - private var networkStatusNode: ChatTitleNetworkStatusNode? + //private var networkStatusNode: ChatTitleNetworkStatusNode? private var presenceManager: PeerPresenceStatusManager? @@ -122,7 +123,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { isOnline = true } - if isOnline || layout?.metrics.widthClass == .regular { + /*if isOnline || layout?.metrics.widthClass == .regular { self.contentContainer.isHidden = false if let networkStatusNode = self.networkStatusNode { self.networkStatusNode = nil @@ -136,7 +137,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } else { statusNode = ChatTitleNetworkStatusNode(theme: self.theme) self.networkStatusNode = statusNode - self.insertSubview(statusNode.view, belowSubview: self.button.view) + self.insertSubview(statusNode.view, aboveSubview: self.contentContainer.view) } switch self.networkState { case .waitingForNetwork: @@ -152,7 +153,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { case .online: break } - } + }*/ self.setNeedsLayout() } @@ -161,6 +162,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { didSet { if self.networkState != oldValue { updateNetworkStatusNode(networkState: self.networkState, layout: self.layout) + self.updateStatus() } } } @@ -174,6 +176,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } var pressed: (() -> Void)? + var longPressed: (() -> Void)? var titleContent: ChatTitleContent? { didSet { @@ -274,175 +277,191 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } var state = ChatTitleActivityNodeState.none - if let (peerId, inputActivities) = self.inputActivities, !inputActivities.isEmpty, inputActivitiesAllowed { - var stringValue = "" - var first = true - var mergedActivity = inputActivities[0].1 - for (_, activity) in inputActivities { - if activity != mergedActivity { - mergedActivity = .typingText - break - } + switch self.networkState { + case .waitingForNetwork, .connecting, .updating: + var infoText: String + switch self.networkState { + case .waitingForNetwork: + infoText = self.strings.ChatState_WaitingForNetwork + case let .connecting(proxy): + infoText = self.strings.ChatState_Connecting + case .updating: + infoText = self.strings.ChatState_Updating + case .online: + infoText = "" } - if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat { - switch mergedActivity { - case .typingText: - stringValue = strings.Conversation_typing - case .uploadingFile: - stringValue = strings.Activity_UploadingDocument - case .recordingVoice: - stringValue = strings.Activity_RecordingAudio - case .uploadingPhoto: - stringValue = strings.Activity_UploadingPhoto - case .uploadingVideo: - stringValue = strings.Activity_UploadingVideo - case .playingGame: - stringValue = strings.Activity_PlayingGame - case .recordingInstantVideo: - stringValue = strings.Activity_RecordingVideoMessage - case .uploadingInstantVideo: - stringValue = strings.Activity_UploadingVideoMessage - } - } else { - for (peer, _) in inputActivities { - let title = peer.compactDisplayTitle - if !title.isEmpty { - if first { - first = false - } else { - stringValue += ", " - } - stringValue += title + state = .info(NSAttributedString(string: infoText, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor), .generic) + case .online: + if let (peerId, inputActivities) = self.inputActivities, !inputActivities.isEmpty, inputActivitiesAllowed { + var stringValue = "" + var first = true + var mergedActivity = inputActivities[0].1 + for (_, activity) in inputActivities { + if activity != mergedActivity { + mergedActivity = .typingText + break } } - } - let color = self.theme.rootController.navigationBar.accentTextColor - let string = NSAttributedString(string: stringValue, font: Font.regular(13.0), textColor: color) - switch mergedActivity { - case .typingText: - state = .typingText(string, color) - case .recordingVoice: - state = .recordingVoice(string, color) - case .recordingInstantVideo: - state = .recordingVideo(string, color) - case .uploadingFile, .uploadingInstantVideo, .uploadingPhoto, .uploadingVideo: - state = .uploading(string, color) - case .playingGame: - state = .playingGame(string, color) - } - } else { - if let titleContent = self.titleContent { - switch titleContent { - case let .peer(peerView, onlineMemberCount, isScheduledMessages): - if let peer = peerViewMainPeer(peerView) { - let servicePeer = isServicePeer(peer) - if peer.id == self.account.peerId || isScheduledMessages { - let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - state = .info(string, .generic) - } else if let user = peer as? TelegramUser { - if servicePeer { + if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat { + switch mergedActivity { + case .typingText: + stringValue = strings.Conversation_typing + case .uploadingFile: + stringValue = strings.Activity_UploadingDocument + case .recordingVoice: + stringValue = strings.Activity_RecordingAudio + case .uploadingPhoto: + stringValue = strings.Activity_UploadingPhoto + case .uploadingVideo: + stringValue = strings.Activity_UploadingVideo + case .playingGame: + stringValue = strings.Activity_PlayingGame + case .recordingInstantVideo: + stringValue = strings.Activity_RecordingVideoMessage + case .uploadingInstantVideo: + stringValue = strings.Activity_UploadingVideoMessage + } + } else { + for (peer, _) in inputActivities { + let title = peer.compactDisplayTitle + if !title.isEmpty { + if first { + first = false + } else { + stringValue += ", " + } + stringValue += title + } + } + } + let color = self.theme.rootController.navigationBar.accentTextColor + let string = NSAttributedString(string: stringValue, font: Font.regular(13.0), textColor: color) + switch mergedActivity { + case .typingText: + state = .typingText(string, color) + case .recordingVoice: + state = .recordingVoice(string, color) + case .recordingInstantVideo: + state = .recordingVideo(string, color) + case .uploadingFile, .uploadingInstantVideo, .uploadingPhoto, .uploadingVideo: + state = .uploading(string, color) + case .playingGame: + state = .playingGame(string, color) + } + } else { + if let titleContent = self.titleContent { + switch titleContent { + case let .peer(peerView, onlineMemberCount, isScheduledMessages): + if let peer = peerViewMainPeer(peerView) { + let servicePeer = isServicePeer(peer) + if peer.id == self.account.peerId || isScheduledMessages { let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) - } else if user.flags.contains(.isSupport) { - let statusText = self.strings.Bot_GenericSupportStatus - - let string = NSAttributedString(string: statusText, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - state = .info(string, .generic) - } else if let _ = user.botInfo { - let statusText = self.strings.Bot_GenericBotStatus - - let string = NSAttributedString(string: statusText, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - state = .info(string, .generic) - } else if let peer = peerViewMainPeer(peerView) { - let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let userPresence: TelegramUserPresence - if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence { - userPresence = presence - self.presenceManager?.reset(presence: presence) + } else if let user = peer as? TelegramUser { + if servicePeer { + let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + state = .info(string, .generic) + } else if user.flags.contains(.isSupport) { + let statusText = self.strings.Bot_GenericSupportStatus + + let string = NSAttributedString(string: statusText, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + state = .info(string, .generic) + } else if let _ = user.botInfo { + let statusText = self.strings.Bot_GenericBotStatus + + let string = NSAttributedString(string: statusText, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + state = .info(string, .generic) + } else if let peer = peerViewMainPeer(peerView) { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + let userPresence: TelegramUserPresence + if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence { + userPresence = presence + self.presenceManager?.reset(presence: presence) + } else { + userPresence = TelegramUserPresence(status: .none, lastActivity: 0) + } + let (string, activity) = stringAndActivityForUserPresence(strings: self.strings, dateTimeFormat: self.dateTimeFormat, presence: userPresence, relativeTo: Int32(timestamp)) + let attributedString = NSAttributedString(string: string, font: Font.regular(13.0), textColor: activity ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.secondaryTextColor) + state = .info(attributedString, activity ? .online : .lastSeenTime) } else { - userPresence = TelegramUserPresence(status: .none, lastActivity: 0) + let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + state = .info(string, .generic) } - let (string, activity) = stringAndActivityForUserPresence(strings: self.strings, dateTimeFormat: self.dateTimeFormat, presence: userPresence, relativeTo: Int32(timestamp)) - let attributedString = NSAttributedString(string: string, font: Font.regular(13.0), textColor: activity ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.secondaryTextColor) - state = .info(attributedString, activity ? .online : .lastSeenTime) - } else { - let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - state = .info(string, .generic) - } - } else if let group = peer as? TelegramGroup { - var onlineCount = 0 - if let cachedGroupData = peerView.cachedData as? CachedGroupData, let participants = cachedGroupData.participants { - let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - for participant in participants.participants { - if let presence = peerView.peerPresences[participant.peerId] as? TelegramUserPresence { - let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: Int32(timestamp)) - switch relativeStatus { - case .online: - onlineCount += 1 - default: - break + } else if let group = peer as? TelegramGroup { + var onlineCount = 0 + if let cachedGroupData = peerView.cachedData as? CachedGroupData, let participants = cachedGroupData.participants { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + for participant in participants.participants { + if let presence = peerView.peerPresences[participant.peerId] as? TelegramUserPresence { + let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: Int32(timestamp)) + switch relativeStatus { + case .online: + onlineCount += 1 + default: + break + } } } } - } - if onlineCount > 1 { - let string = NSMutableAttributedString() - - string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(group.participantCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) - string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) - state = .info(string, .generic) - } else { - let string = NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - state = .info(string, .generic) - } - } else if let channel = peer as? TelegramChannel { - if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { - if memberCount == 0 { - let string: NSAttributedString - if case .group = channel.info { - string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - } else { - string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - } + if onlineCount > 1 { + let string = NSMutableAttributedString() + + string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(group.participantCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) + string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) state = .info(string, .generic) } else { - if case .group = channel.info, let onlineMemberCount = onlineMemberCount, onlineMemberCount > 1 { - let string = NSMutableAttributedString() - - string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(memberCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) - string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineMemberCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) + let string = NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + state = .info(string, .generic) + } + } else if let channel = peer as? TelegramChannel { + if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { + if memberCount == 0 { + let string: NSAttributedString + if case .group = channel.info { + string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + } else { + string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + } state = .info(string, .generic) } else { - let membersString: String - if case .group = channel.info { - membersString = strings.Conversation_StatusMembers(memberCount) + if case .group = channel.info, let onlineMemberCount = onlineMemberCount, onlineMemberCount > 1 { + let string = NSMutableAttributedString() + + string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(memberCount))), ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) + string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineMemberCount)), font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) + state = .info(string, .generic) } else { - membersString = strings.Conversation_StatusSubscribers(memberCount) + let membersString: String + if case .group = channel.info { + membersString = strings.Conversation_StatusMembers(memberCount) + } else { + membersString = strings.Conversation_StatusSubscribers(memberCount) + } + let string = NSAttributedString(string: membersString, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + state = .info(string, .generic) } - let string = NSAttributedString(string: membersString, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - state = .info(string, .generic) } - } - } else { - switch channel.info { - case .group: - let string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - state = .info(string, .generic) - case .broadcast: - let string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) - state = .info(string, .generic) + } else { + switch channel.info { + case .group: + let string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + state = .info(string, .generic) + case .broadcast: + let string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + state = .info(string, .generic) + } } } } - } - default: - break + default: + break + } + + self.accessibilityLabel = self.titleNode.attributedText?.string + self.accessibilityValue = state.string + } else { + self.accessibilityLabel = nil } - - self.accessibilityLabel = self.titleNode.attributedText?.string - self.accessibilityValue = state.string - } else { - self.accessibilityLabel = nil } } @@ -497,34 +516,25 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self?.updateStatus() }) - self.button.addTarget(self, action: #selector(buttonPressed), forControlEvents: [.touchUpInside]) + self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: [.touchUpInside]) self.button.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") strongSelf.activityNode.layer.removeAnimation(forKey: "opacity") - strongSelf.titleLeftIconNode.layer.removeAnimation(forKey: "opacity") - strongSelf.titleRightIconNode.layer.removeAnimation(forKey: "opacity") strongSelf.titleCredibilityIconNode.layer.removeAnimation(forKey: "opacity") strongSelf.titleNode.alpha = 0.4 strongSelf.activityNode.alpha = 0.4 - strongSelf.titleLeftIconNode.alpha = 0.4 - strongSelf.titleRightIconNode.alpha = 0.4 - strongSelf.titleCredibilityIconNode.alpha = 0.4 } else { strongSelf.titleNode.alpha = 1.0 strongSelf.activityNode.alpha = 1.0 - strongSelf.titleLeftIconNode.alpha = 1.0 - strongSelf.titleRightIconNode.alpha = 1.0 strongSelf.titleCredibilityIconNode.alpha = 1.0 strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) strongSelf.activityNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - strongSelf.titleLeftIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - strongSelf.titleRightIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - strongSelf.titleCredibilityIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } + self.button.view.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:)))) } required init?(coder aDecoder: NSCoder) { @@ -543,7 +553,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.theme = theme self.strings = strings - self.networkStatusNode?.updateTheme(theme: theme) + //self.networkStatusNode?.updateTheme(theme: theme) let titleContent = self.titleContent self.titleContent = titleContent self.updateStatus() @@ -567,7 +577,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if let image = self.titleLeftIconNode.image { if self.titleLeftIconNode.supernode == nil { - self.contentContainer.addSubnode(self.titleLeftIconNode) + self.titleNode.addSubnode(self.titleLeftIconNode) } leftIconWidth = image.size.width + 6.0 } else if self.titleLeftIconNode.supernode != nil { @@ -576,7 +586,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if let image = self.titleCredibilityIconNode.image { if self.titleCredibilityIconNode.supernode == nil { - self.contentContainer.addSubnode(self.titleCredibilityIconNode) + self.titleNode.addSubnode(self.titleCredibilityIconNode) } credibilityIconWidth = image.size.width + 3.0 } else if self.titleCredibilityIconNode.supernode != nil { @@ -585,7 +595,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if let image = self.titleRightIconNode.image { if self.titleRightIconNode.supernode == nil { - self.contentContainer.addSubnode(self.titleRightIconNode) + self.titleNode.addSubnode(self.titleRightIconNode) } rightIconWidth = image.size.width + 3.0 } else if self.titleRightIconNode.supernode != nil { @@ -625,13 +635,13 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } if let image = self.titleLeftIconNode.image { - self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.minX - image.size.width - 3.0 - UIScreenPixel, y: titleFrame.minY + 4.0), size: image.size) + self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: -image.size.width - 3.0 - UIScreenPixel, y: 4.0), size: image.size) } if let image = self.titleCredibilityIconNode.image { - self.titleCredibilityIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width - 1.0, y: titleFrame.minY + 2.0), size: image.size) + self.titleCredibilityIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.width - image.size.width - 1.0, y: 2.0), size: image.size) } if let image = self.titleRightIconNode.image { - self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 3.0, y: titleFrame.minY + 6.0), size: image.size) + self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.width + 3.0, y: 6.0), size: image.size) } } else { let titleSize = self.titleNode.updateLayout(CGSize(width: floor(clearBounds.width / 2.0 - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0), height: size.height)) @@ -654,16 +664,18 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width - 1.0, y: titleFrame.minY + 6.0), size: image.size) } } - - if let networkStatusNode = self.networkStatusNode { - transition.updateFrame(node: networkStatusNode, frame: CGRect(origin: CGPoint(), size: size)) - networkStatusNode.updateLayout(size: size, transition: transition) - } } - @objc func buttonPressed() { - if let pressed = self.pressed { - pressed() + @objc private func buttonPressed() { + self.pressed?() + } + + @objc private func longPressGesture(_ gesture: UILongPressGestureRecognizer) { + switch gesture.state { + case .began: + self.longPressed?() + default: + break } } @@ -671,4 +683,11 @@ final class ChatTitleView: UIView, NavigationBarTitleView { UIView.transition(with: self, duration: 0.25, options: [.transitionCrossDissolve], animations: { }, completion: nil) } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.button.frame.contains(point) { + return self.button.view + } + return super.hitTest(point, with: event) + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatUnblockInputPanelNode.swift b/submodules/TelegramUI/TelegramUI/ChatUnblockInputPanelNode.swift index f549a5e64c..79c9aff082 100644 --- a/submodules/TelegramUI/TelegramUI/ChatUnblockInputPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatUnblockInputPanelNode.swift @@ -82,7 +82,7 @@ final class ChatUnblockInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.unblockPeer() } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState diff --git a/submodules/TelegramUI/TelegramUI/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/TelegramUI/CommandChatInputContextPanelNode.swift index 64f424a6f0..92197a821b 100644 --- a/submodules/TelegramUI/TelegramUI/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/CommandChatInputContextPanelNode.swift @@ -6,6 +6,7 @@ import TelegramCore import SyncCore import Display import TelegramPresentationData +import TelegramUIPreferences import MergeLists import AccountContext @@ -34,8 +35,8 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { return lhs.index < rhs.index } - func item(account: Account, commandSelected: @escaping (PeerCommand, Bool) -> Void) -> ListViewItem { - return CommandChatInputPanelItem(account: account, theme: self.theme, command: self.command, commandSelected: commandSelected) + func item(context: AccountContext, fontSize: PresentationFontSize, commandSelected: @escaping (PeerCommand, Bool) -> Void) -> ListViewItem { + return CommandChatInputPanelItem(context: context, theme: self.theme, fontSize: fontSize, command: self.command, commandSelected: commandSelected) } } @@ -45,12 +46,12 @@ private struct CommandChatInputContextPanelTransition { let updates: [ListViewUpdateItem] } -private func preparedTransition(from fromEntries: [CommandChatInputContextPanelEntry], to toEntries: [CommandChatInputContextPanelEntry], account: Account, commandSelected: @escaping (PeerCommand, Bool) -> Void) -> CommandChatInputContextPanelTransition { +private func preparedTransition(from fromEntries: [CommandChatInputContextPanelEntry], to toEntries: [CommandChatInputContextPanelEntry], context: AccountContext, fontSize: PresentationFontSize, commandSelected: @escaping (PeerCommand, Bool) -> Void) -> CommandChatInputContextPanelTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, commandSelected: commandSelected), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, commandSelected: commandSelected), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, fontSize: fontSize, commandSelected: commandSelected), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, fontSize: fontSize, commandSelected: commandSelected), directionHint: nil) } return CommandChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) } @@ -62,7 +63,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { private var enqueuedTransitions: [(CommandChatInputContextPanelTransition, Bool)] = [] private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)? - override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { + override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { self.listView = ListView() self.listView.isOpaque = false self.listView.stackFromBottom = true @@ -70,7 +71,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { self.listView.limitHitTestToNodes = true self.listView.view.disablesInteractiveTransitionGestureRecognizer = true - super.init(context: context, theme: theme, strings: strings) + super.init(context: context, theme: theme, strings: strings, fontSize: fontSize) self.isOpaque = false self.clipsToBounds = true @@ -96,7 +97,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { private func prepareTransition(from: [CommandChatInputContextPanelEntry]? , to: [CommandChatInputContextPanelEntry]) { let firstTime = self.currentEntries == nil - let transition = preparedTransition(from: from ?? [], to: to, account: self.context.account, commandSelected: { [weak self] command, sendImmediately in + let transition = preparedTransition(from: from ?? [], to: to, context: self.context, fontSize: self.fontSize, commandSelected: { [weak self] command, sendImmediately in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { if sendImmediately { interfaceInteraction.sendBotCommand(command.peer, "/" + command.command.text) @@ -193,29 +194,8 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: listViewCurve) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve) self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) diff --git a/submodules/TelegramUI/TelegramUI/CommandChatInputPanelItem.swift b/submodules/TelegramUI/TelegramUI/CommandChatInputPanelItem.swift index a6e08d4cab..a6d809ba14 100644 --- a/submodules/TelegramUI/TelegramUI/CommandChatInputPanelItem.swift +++ b/submodules/TelegramUI/TelegramUI/CommandChatInputPanelItem.swift @@ -7,19 +7,23 @@ import SyncCore import SwiftSignalKit import Postbox import TelegramPresentationData +import TelegramUIPreferences import AvatarNode +import AccountContext final class CommandChatInputPanelItem: ListViewItem { - fileprivate let account: Account + fileprivate let context: AccountContext fileprivate let theme: PresentationTheme + fileprivate let fontSize: PresentationFontSize fileprivate let command: PeerCommand fileprivate let commandSelected: (PeerCommand, Bool) -> Void let selectable: Bool = true - public init(account: Account, theme: PresentationTheme, command: PeerCommand, commandSelected: @escaping (PeerCommand, Bool) -> Void) { - self.account = account + public init(context: AccountContext, theme: PresentationTheme, fontSize: PresentationFontSize, command: PeerCommand, commandSelected: @escaping (PeerCommand, Bool) -> Void) { + self.context = context self.theme = theme + self.fontSize = fontSize self.command = command self.commandSelected = commandSelected } @@ -77,8 +81,6 @@ final class CommandChatInputPanelItem: ListViewItem { } private let avatarFont = avatarPlaceholderFont(size: 16.0) -private let textFont = Font.medium(14.0) -private let descriptionFont = Font.regular(14.0) final class CommandChatInputPanelItemNode: ListViewItemNode { static let itemHeight: CGFloat = 42.0 @@ -133,6 +135,9 @@ final class CommandChatInputPanelItemNode: ListViewItemNode { let makeTextLayout = TextNode.asyncLayout(self.textNode) return { [weak self] item, params, mergedTop, mergedBottom in + let textFont = Font.medium(floor(item.fontSize.baseDisplaySize * 14.0 / 17.0)) + let descriptionFont = Font.regular(floor(item.fontSize.baseDisplaySize * 14.0 / 17.0)) + let leftInset: CGFloat = 55.0 + params.leftInset let rightInset: CGFloat = 10.0 + params.rightInset @@ -160,7 +165,7 @@ final class CommandChatInputPanelItemNode: ListViewItemNode { strongSelf.arrowNode.setImage(iconImage, for: []) - strongSelf.avatarNode.setPeer(account: item.account, theme: item.theme, peer: item.command.peer, emptyColor: item.theme.list.mediaPlaceholderColor) + strongSelf.avatarNode.setPeer(context: item.context, theme: item.theme, peer: item.command.peer, emptyColor: item.theme.list.mediaPlaceholderColor) let _ = textApply() diff --git a/submodules/TelegramUI/TelegramUI/ComposeController.swift b/submodules/TelegramUI/TelegramUI/ComposeController.swift index 3b90b76cc6..bf8741570a 100644 --- a/submodules/TelegramUI/TelegramUI/ComposeController.swift +++ b/submodules/TelegramUI/TelegramUI/ComposeController.swift @@ -241,9 +241,6 @@ public class ComposeController: ViewController { private func activateSearch() { if self.displayNavigationBar { - if let scrollToTop = self.scrollToTop { - scrollToTop() - } if let searchContentNode = self.searchContentNode { self.contactsNode.activateSearch(placeholderNode: searchContentNode.placeholderNode) } diff --git a/submodules/TelegramUI/TelegramUI/ConfettiView.swift b/submodules/TelegramUI/TelegramUI/ConfettiView.swift new file mode 100644 index 0000000000..c17a9f32a4 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/ConfettiView.swift @@ -0,0 +1,271 @@ +import Foundation +import UIKit +import Display + +private struct Vector2 { + var x: Float + var y: Float +} + +private final class NullActionClass: NSObject, CAAction { + @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { + } +} + +private let nullAction = NullActionClass() + +private final class ParticleLayer: CALayer { + let mass: Float + var velocity: Vector2 + var angularVelocity: Float + var rotationAngle: Float = 0.0 + var localTime: Float = 0.0 + var type: Int + + init(image: CGImage, size: CGSize, position: CGPoint, mass: Float, velocity: Vector2, angularVelocity: Float, type: Int) { + self.mass = mass + self.velocity = velocity + self.angularVelocity = angularVelocity + self.type = type + + super.init() + + self.contents = image + self.bounds = CGRect(origin: CGPoint(), size: size) + self.position = position + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func action(forKey event: String) -> CAAction? { + return nullAction + } +} + +final class ConfettiView: UIView { + private var particles: [ParticleLayer] = [] + private var displayLink: ConstantDisplayLinkAnimator? + + private var localTime: Float = 0.0 + + override init(frame: CGRect) { + super.init(frame: frame) + + self.isUserInteractionEnabled = false + + let colors: [UIColor] = ([ + 0x56CE6B, + 0xCD89D0, + 0x1E9AFF, + 0xFF8724 + ] as [UInt32]).map(UIColor.init(rgb:)) + let imageSize = CGSize(width: 8.0, height: 8.0) + var images: [(CGImage, CGSize)] = [] + for imageType in 0 ..< 2 { + for color in colors { + if imageType == 0 { + images.append((generateFilledCircleImage(diameter: imageSize.width, color: color)!.cgImage!, imageSize)) + } else { + let spriteSize = CGSize(width: 2.0, height: 6.0) + images.append((generateImage(spriteSize, opaque: false, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.width))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height - size.width), size: CGSize(width: size.width, height: size.width))) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.width / 2.0), size: CGSize(width: size.width, height: size.height - size.width))) + })!.cgImage!, spriteSize)) + } + } + } + let imageCount = images.count + + let originXRange = 0 ..< Int(frame.width) + let originYRange = Int(-frame.height) ..< Int(0) + let topMassRange: Range = 40.0 ..< 50.0 + let velocityYRange = Float(3.0) ..< Float(5.0) + let angularVelocityRange = Float(1.0) ..< Float(6.0) + let sizeVariation = Float(0.8) ..< Float(1.6) + let topDelayRange = Float(0.0) ..< Float(0.0) + + for i in 0 ..< 70 { + let (image, size) = images[i % imageCount] + let sizeScale = CGFloat(Float.random(in: sizeVariation)) + let particle = ParticleLayer(image: image, size: CGSize(width: size.width * sizeScale, height: size.height * sizeScale), position: CGPoint(x: CGFloat(Int.random(in: originXRange)), y: CGFloat(Int.random(in: originYRange))), mass: Float.random(in: topMassRange), velocity: Vector2(x: 0.0, y: Float.random(in: velocityYRange)), angularVelocity: Float.random(in: angularVelocityRange), type: 0) + self.particles.append(particle) + self.layer.addSublayer(particle) + } + + let sideMassRange: Range = 110.0 ..< 120.0 + let sideOriginYBase: Float = Float(frame.size.height * 9.0 / 10.0) + let sideOriginYVariation: Float = Float(frame.size.height / 12.0) + let sideOriginYRange = Float(sideOriginYBase - sideOriginYVariation) ..< Float(sideOriginYBase + sideOriginYVariation) + let sideOriginXRange = Float(0.0) ..< Float(100.0) + let sideOriginVelocityValueRange = Float(1.1) ..< Float(1.3) + let sideOriginVelocityValueScaling: Float = 2400.0 * Float(frame.height) / 896.0 + let sideOriginVelocityBase: Float = Float.pi / 2.0 + atanf(Float(CGFloat(sideOriginYBase) / (frame.size.width * 0.8))) + let sideOriginVelocityVariation: Float = 0.09 + let sideOriginVelocityAngleRange = Float(sideOriginVelocityBase - sideOriginVelocityVariation) ..< Float(sideOriginVelocityBase + sideOriginVelocityVariation) + let originAngleRange = Float(0.0) ..< (Float.pi * 2.0) + let originAmplitudeDiameter: CGFloat = 230.0 + let originAmplitudeRange = Float(0.0) ..< Float(originAmplitudeDiameter / 2.0) + + let sideTypes: [Int] = [0, 1, 2] + + for sideIndex in 0 ..< 2 { + let sideSign: Float = sideIndex == 0 ? 1.0 : -1.0 + let baseOriginX: CGFloat = sideIndex == 0 ? -originAmplitudeDiameter / 2.0 : (frame.width + originAmplitudeDiameter / 2.0) + + for i in 0 ..< 40 { + let originAngle = Float.random(in: originAngleRange) + let originAmplitude = Float.random(in: originAmplitudeRange) + let originX = baseOriginX + CGFloat(cosf(originAngle) * originAmplitude) + let originY = CGFloat(sideOriginYBase + sinf(originAngle) * originAmplitude) + + let velocityValue = Float.random(in: sideOriginVelocityValueRange) * sideOriginVelocityValueScaling + let velocityAngle = Float.random(in: sideOriginVelocityAngleRange) + let velocityX = sideSign * velocityValue * sinf(velocityAngle) + let velocityY = velocityValue * cosf(velocityAngle) + let (image, size) = images[i % imageCount] + let sizeScale = CGFloat(Float.random(in: sizeVariation)) + let particle = ParticleLayer(image: image, size: CGSize(width: size.width * sizeScale, height: size.height * sizeScale), position: CGPoint(x: originX, y: originY), mass: Float.random(in: sideMassRange), velocity: Vector2(x: velocityX, y: velocityY), angularVelocity: Float.random(in: angularVelocityRange), type: sideTypes[i % 3]) + self.particles.append(particle) + self.layer.addSublayer(particle) + } + } + + self.displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.step() + }) + + self.displayLink?.isPaused = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var slowdownStartTimestamps: [Float?] = [nil, nil, nil] + + private func step() { + self.slowdownStartTimestamps[0] = 0.33 + + var haveParticlesAboveGround = false + let minPositionY: CGFloat = 0.0 + let maxPositionY = self.bounds.height + 30.0 + let minDampingX: CGFloat = 40.0 + let maxDampingX: CGFloat = self.bounds.width - 40.0 + let centerX: CGFloat = self.bounds.width / 2.0 + let dt: Float = 1.0 * 1.0 / 60.0 + + let typeDelays: [Float] = [0.0, 0.01, 0.08] + var dtAndDamping: [(Float, Float)] = [] + + for i in 0 ..< 3 { + let typeDelay = typeDelays[i] + let currentTime = self.localTime - typeDelay + if currentTime < 0.0 { + dtAndDamping.append((0.0, 1.0)) + } else if let slowdownStart = self.slowdownStartTimestamps[i] { + let slowdownDt: Float + let slowdownDuration: Float = 0.5 + let damping: Float + if currentTime >= slowdownStart && currentTime <= slowdownStart + slowdownDuration { + let slowdownTimestamp: Float = currentTime - slowdownStart + + let slowdownRampInDuration: Float = 0.05 + let slowdownRampOutDuration: Float = 0.2 + let rawSlowdownT: Float + if slowdownTimestamp < slowdownRampInDuration { + rawSlowdownT = slowdownTimestamp / slowdownRampInDuration + } else if slowdownTimestamp >= slowdownDuration - slowdownRampOutDuration { + let reverseTransition = (slowdownTimestamp - (slowdownDuration - slowdownRampOutDuration)) / slowdownRampOutDuration + rawSlowdownT = 1.0 - reverseTransition + } else { + rawSlowdownT = 1.0 + } + + let slowdownTransition = rawSlowdownT * rawSlowdownT + + let slowdownFactor: Float = 0.8 * slowdownTransition + 1.0 * (1.0 - slowdownTransition) + slowdownDt = dt * slowdownFactor + let dampingFactor: Float = 0.937 * slowdownTransition + 1.0 * (1.0 - slowdownTransition) + + damping = dampingFactor + } else { + slowdownDt = dt + damping = 1.0 + } + if i == 1 { + //print("type 1 dt = \(slowdownDt), slowdownStart = \(slowdownStart), currentTime = \(currentTime)") + } + dtAndDamping.append((slowdownDt, damping)) + } else { + dtAndDamping.append((dt, 1.0)) + } + } + self.localTime += dt + + let g: Vector2 = Vector2(x: 0.0, y: 9.8) + CATransaction.begin() + CATransaction.setDisableActions(true) + var turbulenceVariation: [Float] = [] + for _ in 0 ..< 20 { + turbulenceVariation.append(Float.random(in: -16.0 ..< 16.0) * 60.0) + } + let turbulenceVariationCount = turbulenceVariation.count + var index = 0 + + var typesWithPositiveVelocity: [Bool] = [false, false, false] + + for particle in self.particles { + let (localDt, damping_) = dtAndDamping[particle.type] + if localDt.isZero { + continue + } + let damping: Float = 0.93 + + particle.localTime += localDt + + var position = particle.position + + position.x += CGFloat(particle.velocity.x * localDt) + position.y += CGFloat(particle.velocity.y * localDt) + particle.position = position + + particle.rotationAngle += particle.angularVelocity * localDt + particle.transform = CATransform3DMakeRotation(CGFloat(particle.rotationAngle), 0.0, 0.0, 1.0) + + let acceleration = g + + var velocity = particle.velocity + velocity.x += acceleration.x * particle.mass * localDt + velocity.y += acceleration.y * particle.mass * localDt + if velocity.y < 0.0 { + velocity.x *= damping + velocity.y *= damping + } else { + velocity.x += turbulenceVariation[index % turbulenceVariationCount] * localDt + typesWithPositiveVelocity[particle.type] = true + } + particle.velocity = velocity + + index += 1 + + if position.y < maxPositionY { + haveParticlesAboveGround = true + } + } + for i in 0 ..< 3 { + if typesWithPositiveVelocity[i] && self.slowdownStartTimestamps[i] == nil { + self.slowdownStartTimestamps[i] = max(0.0, self.localTime - typeDelays[i]) + } + } + CATransaction.commit() + if !haveParticlesAboveGround { + self.displayLink?.isPaused = true + self.removeFromSuperview() + } + } +} diff --git a/submodules/TelegramUI/TelegramUI/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/TelegramUI/ContactMultiselectionControllerNode.swift index c77bb99064..b276f8211c 100644 --- a/submodules/TelegramUI/TelegramUI/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ContactMultiselectionControllerNode.swift @@ -68,7 +68,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { } self.contactListNode = ContactListNode(context: context, presentation: .single(.natural(options: options, includeChatList: includeChatList)), filters: filters, selectionState: ContactListNodeGroupSelectionState()) - self.tokenListNode = EditableTokenListNode(theme: EditableTokenListNodeTheme(backgroundColor: self.presentationData.theme.rootController.navigationBar.backgroundColor, separatorColor: self.presentationData.theme.rootController.navigationBar.separatorColor, placeholderTextColor: self.presentationData.theme.list.itemPlaceholderTextColor, primaryTextColor: self.presentationData.theme.list.itemPrimaryTextColor, selectedTextColor: self.presentationData.theme.list.itemAccentColor, accentColor: self.presentationData.theme.list.itemAccentColor, keyboardColor: self.presentationData.theme.rootController.keyboardColor), placeholder: placeholder) + self.tokenListNode = EditableTokenListNode(theme: EditableTokenListNodeTheme(backgroundColor: self.presentationData.theme.rootController.navigationBar.backgroundColor, separatorColor: self.presentationData.theme.rootController.navigationBar.separatorColor, placeholderTextColor: self.presentationData.theme.list.itemPlaceholderTextColor, primaryTextColor: self.presentationData.theme.list.itemPrimaryTextColor, selectedTextColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, selectedBackgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, accentColor: self.presentationData.theme.list.itemAccentColor, keyboardColor: self.presentationData.theme.rootController.keyboardColor), placeholder: placeholder) super.init() diff --git a/submodules/TelegramUI/TelegramUI/ContactSelectionController.swift b/submodules/TelegramUI/TelegramUI/ContactSelectionController.swift index 90604e4c49..32cba1b357 100644 --- a/submodules/TelegramUI/TelegramUI/ContactSelectionController.swift +++ b/submodules/TelegramUI/TelegramUI/ContactSelectionController.swift @@ -238,9 +238,6 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController private func activateSearch() { if self.displayNavigationBar { - if let scrollToTop = self.scrollToTop { - scrollToTop() - } if let searchContentNode = self.searchContentNode { self.contactsNode.activateSearch(placeholderNode: searchContentNode.placeholderNode) } diff --git a/submodules/TelegramUI/TelegramUI/CreateChannelController.swift b/submodules/TelegramUI/TelegramUI/CreateChannelController.swift index dab232f8c5..8e599de8bb 100644 --- a/submodules/TelegramUI/TelegramUI/CreateChannelController.swift +++ b/submodules/TelegramUI/TelegramUI/CreateChannelController.swift @@ -19,7 +19,7 @@ import PeerInfoUI import MapResourceToAvatarSizes private struct CreateChannelArguments { - let account: Account + let context: AccountContext let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void let updateEditingDescriptionText: (String) -> Void @@ -135,24 +135,25 @@ private enum CreateChannelEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! CreateChannelArguments switch self { case let .channelInfo(theme, strings, dateTimeFormat, peer, state, avatar): - return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: nil, cachedData: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false, withExtendedBottomInset: false), editingNameUpdated: { editingName in + return ItemListAvatarAndNameInfoItem(accountContext: arguments.context, presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .editSettings, peer: peer, presence: nil, cachedData: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false, withExtendedBottomInset: false), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { + arguments.changeProfilePhoto() }, updatingImage: avatar, tag: CreateChannelEntryTag.info) case let .setProfilePhoto(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.changeProfilePhoto() }) case let .descriptionSetup(theme, text, value): - return ItemListMultilineInputItem(theme: theme, text: value, placeholder: text, maxLength: ItemListMultilineInputItemTextLimit(value: 255, display: true), sectionId: self.section, style: .blocks, textUpdated: { updatedText in + return ItemListMultilineInputItem(presentationData: presentationData, text: value, placeholder: text, maxLength: ItemListMultilineInputItemTextLimit(value: 255, display: true), sectionId: self.section, style: .blocks, textUpdated: { updatedText in arguments.updateEditingDescriptionText(updatedText) }) case let .descriptionInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -188,7 +189,6 @@ private func CreateChannelEntries(presentationData: PresentationData, state: Cre let peer = TelegramGroup(id: PeerId(namespace: -1, id: 0), title: state.editingName.composedTitle, photo: [], participantCount: 0, role: .creator(rank: nil), membership: .Member, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) entries.append(.channelInfo(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, groupInfoState, state.avatar)) - entries.append(.setProfilePhoto(presentationData.theme, presentationData.strings.Channel_UpdatePhotoItem)) entries.append(.descriptionSetup(presentationData.theme, presentationData.strings.Channel_Edit_AboutItem, state.editingDescriptionText)) entries.append(.descriptionInfo(presentationData.theme, presentationData.strings.Channel_About_Help)) @@ -205,6 +205,7 @@ public func createChannelController(context: AccountContext) -> ViewController { } var replaceControllerImpl: ((ViewController) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? var endEditingImpl: (() -> Void)? @@ -214,7 +215,7 @@ public func createChannelController(context: AccountContext) -> ViewController { let uploadedAvatar = Promise() - let arguments = CreateChannelArguments(account: context.account, updateEditingName: { editingName in + let arguments = CreateChannelArguments(context: context, updateEditingName: { editingName in updateState { current in var current = current switch editingName { @@ -273,7 +274,8 @@ public func createChannelController(context: AccountContext) -> ViewController { case .generic, .tooMuchLocationBasedGroups: text = presentationData.strings.Login_UnknownError case .tooMuchJoined: - text = presentationData.strings.CreateGroup_ChannelsTooMuch + pushControllerImpl?(oldChannelsController(context: context, intent: .create)) + return case .restricted: text = presentationData.strings.Common_ActionNotAllowedError default: @@ -372,8 +374,8 @@ public func createChannelController(context: AccountContext) -> ViewController { }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ChannelIntro_CreateChannel), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: CreateChannelEntries(presentationData: presentationData, state: state), style: .blocks, focusItemTag: CreateChannelEntryTag.info) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChannelIntro_CreateChannel), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: CreateChannelEntries(presentationData: presentationData, state: state), style: .blocks, focusItemTag: CreateChannelEntryTag.info) return (controllerState, (listState, arguments)) } |> afterDisposed { @@ -384,6 +386,9 @@ public func createChannelController(context: AccountContext) -> ViewController { replaceControllerImpl = { [weak controller] value in (controller?.navigationController as? NavigationController)?.replaceAllButRootController(value, animated: true) } + pushControllerImpl = { [weak controller] value in + controller?.push(value) + } presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) } diff --git a/submodules/TelegramUI/TelegramUI/CreateGroupController.swift b/submodules/TelegramUI/TelegramUI/CreateGroupController.swift index b153bf5dc3..d0f44d71b0 100644 --- a/submodules/TelegramUI/TelegramUI/CreateGroupController.swift +++ b/submodules/TelegramUI/TelegramUI/CreateGroupController.swift @@ -15,6 +15,7 @@ import AlertUI import PresentationDataUtils import MediaResources import PhotoResources +import LocationResources import LegacyUI import LocationUI import ItemListPeerItem @@ -24,20 +25,23 @@ import Geocoding import PeerInfoUI import MapResourceToAvatarSizes import ItemListAddressItem +import ItemListVenueItem private struct CreateGroupArguments { - let account: Account + let context: AccountContext let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void let done: () -> Void let changeProfilePhoto: () -> Void let changeLocation: () -> Void + let updateWithVenue: (TelegramMediaMap) -> Void } private enum CreateGroupSection: Int32 { case info case members case location + case venues } private enum CreateGroupEntryTag: ItemListItemTag { @@ -67,6 +71,8 @@ private enum CreateGroupEntry: ItemListNodeEntry { case location(PresentationTheme, PeerGeoLocation) case changeLocation(PresentationTheme, String) case locationInfo(PresentationTheme, String) + case venueHeader(PresentationTheme, String) + case venue(Int32, PresentationTheme, TelegramMediaMap) var section: ItemListSectionId { switch self { @@ -76,6 +82,8 @@ private enum CreateGroupEntry: ItemListNodeEntry { return CreateGroupSection.members.rawValue case .locationHeader, .location, .changeLocation, .locationInfo: return CreateGroupSection.location.rawValue + case .venueHeader, .venue: + return CreateGroupSection.venues.rawValue } } @@ -95,6 +103,10 @@ private enum CreateGroupEntry: ItemListNodeEntry { return 10002 case .locationInfo: return 10003 + case .venueHeader: + return 10004 + case let .venue(index, _, _): + return 10005 + index } } @@ -189,6 +201,27 @@ private enum CreateGroupEntry: ItemListNodeEntry { } else { return false } + case let .venueHeader(lhsTheme, lhsTitle): + if case let .venueHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle { + return true + } else { + return false + } + case let .venue(lhsIndex, lhsTheme, lhsVenue): + if case let .venue(rhsIndex, rhsTheme, rhsVenue) = rhs { + if lhsIndex != rhsIndex { + return false + } + if lhsTheme !== rhsTheme { + return false + } + if !lhsVenue.isEqual(to: rhsVenue) { + return false + } + return true + } else { + return false + } } } @@ -196,31 +229,38 @@ private enum CreateGroupEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! CreateGroupArguments switch self { case let .groupInfo(theme, strings, dateTimeFormat, peer, state, avatar): - return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: nil, cachedData: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false, withExtendedBottomInset: false), editingNameUpdated: { editingName in + return ItemListAvatarAndNameInfoItem(accountContext: arguments.context, presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .editSettings, peer: peer, presence: nil, cachedData: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false, withExtendedBottomInset: false), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { + arguments.changeProfilePhoto() }, updatingImage: avatar, tag: CreateGroupEntryTag.info) case let .setProfilePhoto(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.changeProfilePhoto() }) case let .member(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer, presence): - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, presence: presence, text: .presence, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer, presence: presence, text: .presence, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) case let .locationHeader(theme, title): - return ItemListSectionHeaderItem(theme: theme, text: title, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .location(theme, location): - let imageSignal = chatMapSnapshotImage(account: arguments.account, resource: MapSnapshotMediaResource(latitude: location.latitude, longitude: location.longitude, width: 90, height: 90)) + let imageSignal = chatMapSnapshotImage(account: arguments.context.account, resource: MapSnapshotMediaResource(latitude: location.latitude, longitude: location.longitude, width: 90, height: 90)) return ItemListAddressItem(theme: theme, label: "", text: location.address.replacingOccurrences(of: ", ", with: "\n"), imageSignal: imageSignal, selected: nil, sectionId: self.section, style: .blocks, action: nil) case let .changeLocation(theme, text): - return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.changeLocation() - }, clearHighlightAutomatically: false) + }) case let .locationInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .venueHeader(theme, title): + return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) + case let .venue(_, theme, venue): + return ItemListVenueItem(presentationData: presentationData, account: arguments.context.account, venue: venue, sectionId: self.section, style: .blocks, action: { + arguments.updateWithVenue(venue) + }) } } } @@ -228,6 +268,7 @@ private enum CreateGroupEntry: ItemListNodeEntry { private struct CreateGroupState: Equatable { var creating: Bool var editingName: ItemListAvatarAndNameInfoItemName + var nameSetFromVenue: Bool var avatar: ItemListAvatarAndNameInfoItemUpdatingAvatar? var location: PeerGeoLocation? @@ -238,6 +279,9 @@ private struct CreateGroupState: Equatable { if lhs.editingName != rhs.editingName { return false } + if lhs.nameSetFromVenue != rhs.nameSetFromVenue { + return false + } if lhs.avatar != rhs.avatar { return false } @@ -248,7 +292,7 @@ private struct CreateGroupState: Equatable { } } -private func createGroupEntries(presentationData: PresentationData, state: CreateGroupState, peerIds: [PeerId], view: MultiplePeersView) -> [CreateGroupEntry] { +private func createGroupEntries(presentationData: PresentationData, state: CreateGroupState, peerIds: [PeerId], view: MultiplePeersView, venues: [TelegramMediaMap]?) -> [CreateGroupEntry] { var entries: [CreateGroupEntry] = [] let groupInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: nil) @@ -256,7 +300,6 @@ private func createGroupEntries(presentationData: PresentationData, state: Creat let peer = TelegramGroup(id: PeerId(namespace: -1, id: 0), title: state.editingName.composedTitle, photo: [], participantCount: 0, role: .creator(rank: nil), membership: .Member, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) entries.append(.groupInfo(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, groupInfoState, state.avatar)) - entries.append(.setProfilePhoto(presentationData.theme, presentationData.strings.GroupInfo_SetGroupPhoto)) var peers: [Peer] = [] for peerId in peerIds { @@ -294,6 +337,21 @@ private func createGroupEntries(presentationData: PresentationData, state: Creat entries.append(.location(presentationData.theme, location)) entries.append(.changeLocation(presentationData.theme, presentationData.strings.Group_Location_ChangeLocation)) entries.append(.locationInfo(presentationData.theme, presentationData.strings.Group_Location_Info)) + + entries.append(.venueHeader(presentationData.theme, presentationData.strings.Group_Location_CreateInThisPlace.uppercased())) + if let venues = venues { + if !venues.isEmpty { + var index: Int32 = 0 + for venue in venues { + entries.append(.venue(index, presentationData.theme, venue)) + index += 1 + } + } else { + + } + } else { + + } } return entries @@ -305,7 +363,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] location = PeerGeoLocation(latitude: latitude, longitude: longitude, address: address ?? "") } - let initialState = CreateGroupState(creating: false, editingName: .title(title: initialTitle ?? "", type: .group), avatar: nil, location: location) + let initialState = CreateGroupState(creating: false, editingName: .title(title: initialTitle ?? "", type: .group), nameSetFromVenue: false, avatar: nil, location: location) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((CreateGroupState) -> CreateGroupState) -> Void = { f in @@ -318,6 +376,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] var pushImpl: ((ViewController) -> Void)? var endEditingImpl: (() -> Void)? var clearHighlightImpl: (() -> Void)? + var ensureItemVisibleImpl: ((CreateGroupEntryTag, Bool) -> Void)? let actionsDisposable = DisposableSet() @@ -326,6 +385,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] let uploadedAvatar = Promise() let addressPromise = Promise(nil) + let venuesPromise = Promise<[TelegramMediaMap]?>(nil) if case let .locatedGroup(latitude, longitude, address) = mode { if let address = address { addressPromise.set(.single(address)) @@ -335,12 +395,16 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] return placemark?.fullAddress ?? "\(latitude), \(longitude)" }) } + + venuesPromise.set(nearbyVenues(account: context.account, latitude: latitude, longitude: longitude) + |> map(Optional.init)) } - let arguments = CreateGroupArguments(account: context.account, updateEditingName: { editingName in + let arguments = CreateGroupArguments(context: context, updateEditingName: { editingName in updateState { current in var current = current current.editingName = editingName + current.nameSetFromVenue = false return current } }, done: { @@ -466,7 +530,8 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] case .restricted: text = presentationData.strings.Common_ActionNotAllowedError case .tooMuchJoined: - text = presentationData.strings.CreateGroup_ChannelsTooMuch + pushImpl?(oldChannelsController(context: context, intent: .create)) + return case .tooMuchLocationBasedGroups: text = presentationData.strings.CreateGroup_ErrorLocatedGroupsTooMuch default: @@ -555,41 +620,69 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] }) }, changeLocation: { endEditingImpl?() - - let peer = TelegramChannel(id: PeerId(0), accessHash: nil, title: "", username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(TelegramChannelGroupInfo(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil) + + let controller = LocationPickerController(context: context, mode: .pick, completion: { location, address in + let addressSignal: Signal + if let address = address { + addressSignal = .single(address) + } else { + addressSignal = reverseGeocodeLocation(latitude: location.latitude, longitude: location.longitude) + |> map { placemark in + if let placemark = placemark { + return placemark.fullAddress + } else { + return "\(location.latitude), \(location.longitude)" + } + } + } + + let _ = (addressSignal + |> deliverOnMainQueue).start(next: { address in + addressPromise.set(.single(address)) + updateState { current in + var current = current + current.location = PeerGeoLocation(latitude: location.latitude, longitude: location.longitude, address: address) + return current + } + }) + }) + pushImpl?(controller) + }, updateWithVenue: { venue in + guard let venueData = venue.venue else { + return + } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = legacyLocationPickerController(context: context, selfPeer: peer, peer: peer, sendLocation: { coordinate, _, address in - let addressSignal: Signal - if let address = address { - addressSignal = .single(address) - } else { - addressSignal = reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) - |> map { placemark in - if let placemark = placemark { - return placemark.fullAddress - } else { - return "\(coordinate.latitude), \(coordinate.longitude)" - } - } + updateState { current in + var current = current + if current.editingName.isEmpty || current.nameSetFromVenue { + current.editingName = .title(title: venueData.title ?? "", type: .group) + current.nameSetFromVenue = true + } + current.location = PeerGeoLocation(latitude: venue.latitude, longitude: venue.longitude, address: presentationData.strings.Map_Locating + "\n\n") + return current + } + + let _ = (reverseGeocodeLocation(latitude: venue.latitude, longitude: venue.longitude) + |> map { placemark -> String in + if let placemark = placemark { + return placemark.fullAddress + } else { + return venueData.address ?? "" + } + } + |> deliverOnMainQueue).start(next: { address in + addressPromise.set(.single(address)) + updateState { current in + var current = current + current.location = PeerGeoLocation(latitude: venue.latitude, longitude: venue.longitude, address: address) + return current } - - let _ = (addressSignal - |> deliverOnMainQueue).start(next: { address in - addressPromise.set(.single(address)) - updateState { current in - var current = current - current.location = PeerGeoLocation(latitude: coordinate.latitude, longitude: coordinate.longitude, address: address) - return current - } - }) - }, sendLiveLocation: { _, _ in }, theme: presentationData.theme, customLocationPicker: true, presentationCompleted: { - clearHighlightImpl?() }) - pushImpl?(controller) + ensureItemVisibleImpl?(.info, true) }) - let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), context.account.postbox.multiplePeersView(peerIds), .single(nil) |> then(addressPromise.get())) - |> map { presentationData, state, view, address -> (ItemListControllerState, (ItemListNodeState, Any)) in + let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), context.account.postbox.multiplePeersView(peerIds), .single(nil) |> then(addressPromise.get()), .single(nil) |> then(venuesPromise.get())) + |> map { presentationData, state, view, address, venues -> (ItemListControllerState, (ItemListNodeState, Any)) in let rightNavigationButton: ItemListNavigationButton if state.creating { @@ -600,8 +693,8 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Compose_NewGroupTitle), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: createGroupEntries(presentationData: presentationData, state: state, peerIds: peerIds, view: view), style: .blocks, focusItemTag: CreateGroupEntryTag.info) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Compose_NewGroupTitle), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createGroupEntries(presentationData: presentationData, state: state, peerIds: peerIds, view: view, venues: venues), style: .blocks, focusItemTag: CreateGroupEntryTag.info) return (controllerState, (listState, arguments)) } @@ -634,5 +727,28 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] clearHighlightImpl = { [weak controller] in controller?.clearItemNodesHighlight(animated: true) } + ensureItemVisibleImpl = { [weak controller] targetTag, animated in + controller?.afterLayout({ + guard let controller = controller else { + return + } + + var resultItemNode: ListViewItemNode? + let state = stateValue.with({ $0 }) + let _ = controller.frameForItemNode({ itemNode in + if let itemNode = itemNode as? ItemListItemNode { + if let tag = itemNode.tag, tag.isEqual(to: targetTag) { + resultItemNode = itemNode as? ListViewItemNode + return true + } + } + return false + }) + + if let resultItemNode = resultItemNode { + controller.ensureItemNodeVisible(resultItemNode, animated: animated) + } + }) + } return controller } diff --git a/submodules/TelegramUI/TelegramUI/DeclareEncodables.swift b/submodules/TelegramUI/TelegramUI/DeclareEncodables.swift index 1731b79a72..8de131f329 100644 --- a/submodules/TelegramUI/TelegramUI/DeclareEncodables.swift +++ b/submodules/TelegramUI/TelegramUI/DeclareEncodables.swift @@ -9,6 +9,7 @@ import WebSearchUI import InstantPageCache import SettingsUI import WallpaperResources +import LocationUI private var telegramUIDeclaredEncodables: Void = { declareEncodable(InAppNotificationSettings.self, f: { InAppNotificationSettings(decoder: $0) }) @@ -52,6 +53,7 @@ private var telegramUIDeclaredEncodables: Void = { declareEncodable(MediaPlaybackStoredState.self, f: { MediaPlaybackStoredState(decoder: $0) }) declareEncodable(WebBrowserSettings.self, f: { WebBrowserSettings(decoder: $0) }) declareEncodable(IntentsSettings.self, f: { IntentsSettings(decoder: $0) }) + declareEncodable(CachedGeocode.self, f: { CachedGeocode(decoder: $0) }) return }() diff --git a/submodules/TelegramUI/TelegramUI/DeleteChatInputPanelNode.swift b/submodules/TelegramUI/TelegramUI/DeleteChatInputPanelNode.swift index 9223a84239..6206cc5ed7 100644 --- a/submodules/TelegramUI/TelegramUI/DeleteChatInputPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/DeleteChatInputPanelNode.swift @@ -35,7 +35,7 @@ final class DeleteChatInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.deleteChat() } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState diff --git a/submodules/TelegramUI/TelegramUI/DeviceContactDataManager.swift b/submodules/TelegramUI/TelegramUI/DeviceContactDataManager.swift index 82ea24f4a1..89292acabb 100644 --- a/submodules/TelegramUI/TelegramUI/DeviceContactDataManager.swift +++ b/submodules/TelegramUI/TelegramUI/DeviceContactDataManager.swift @@ -416,7 +416,7 @@ private final class DeviceContactDataLegacyContext: DeviceContactDataContext { func getExtendedContactData(stableId: DeviceContactStableId) -> DeviceContactExtendedData? { if let contact = self.getContactById(stableId: stableId) { let basicData = DeviceContactDataLegacyContext.parseContact(contact).1 - return DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: []) + return DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") } else { return nil } @@ -449,7 +449,7 @@ private final class DeviceContactDataLegacyContext: DeviceContactDataContext { let stableId = "ab-\(ABRecordGetRecordID(contact))" if let contact = self.getContactById(stableId: stableId) { let parsedContact = DeviceContactDataLegacyContext.parseContact(contact).1 - result = (stableId, DeviceContactExtendedData(basicData: parsedContact, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [])) + result = (stableId, DeviceContactExtendedData(basicData: parsedContact, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")) } } } diff --git a/submodules/TelegramUI/TelegramUI/DisabledContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/TelegramUI/DisabledContextResultsChatInputContextPanelNode.swift index 8d8b412d1e..cb96e43f19 100644 --- a/submodules/TelegramUI/TelegramUI/DisabledContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/DisabledContextResultsChatInputContextPanelNode.swift @@ -6,6 +6,7 @@ import AsyncDisplayKit import Display import TelegramPresentationData import TelegramStringFormatting +import TelegramUIPreferences import AccountContext final class DisabledContextResultsChatInputContextPanelNode: ChatInputContextPanelNode { @@ -15,14 +16,14 @@ final class DisabledContextResultsChatInputContextPanelNode: ChatInputContextPan private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)? - override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { + override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { self.containerNode = ASDisplayNode() self.separatorNode = ASDisplayNode() self.textNode = ImmediateTextNode() self.textNode.maximumNumberOfLines = 0 self.textNode.textAlignment = .center - super.init(context: context, theme: theme, strings: strings) + super.init(context: context, theme: theme, strings: strings, fontSize: fontSize) self.isOpaque = false self.clipsToBounds = true diff --git a/submodules/TelegramUI/TelegramUI/EditAccessoryPanelNode.swift b/submodules/TelegramUI/TelegramUI/EditAccessoryPanelNode.swift index 6f2c11a500..a849ec4517 100644 --- a/submodules/TelegramUI/TelegramUI/EditAccessoryPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/EditAccessoryPanelNode.swift @@ -144,7 +144,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { if let currentEditMediaReference = self.currentEditMediaReference { effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media]) } - (text, _) = descriptionStringForMessage(effectiveMessage, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, accountPeerId: self.context.account.peerId) + (text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, accountPeerId: self.context.account.peerId) } var updatedMediaReference: AnyMediaReference? @@ -213,7 +213,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { if let currentEditMediaReference = self.currentEditMediaReference { effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media]) } - switch messageContentKind(effectiveMessage, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: self.context.account.peerId) { + switch messageContentKind(contentSettings: self.context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: self.context.account.peerId) { case .text: isMedia = false default: @@ -317,7 +317,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { self.statusNode.frame = CGRect(origin: CGPoint(x: 18.0, y: 15.0), size: indicatorSize).insetBy(dx: -2.0, dy: -2.0) let closeButtonSize = CGSize(width: 44.0, height: bounds.height) - let closeButtonFrame = CGRect(origin: CGPoint(x: bounds.width - rightInset - closeButtonSize.width + 12.0, y: 2.0), size: closeButtonSize) + let closeButtonFrame = CGRect(origin: CGPoint(x: bounds.width - rightInset - closeButtonSize.width + 16.0, y: 2.0), size: closeButtonSize) self.closeButton.frame = closeButtonFrame self.actionArea.frame = CGRect(origin: CGPoint(x: leftInset, y: 2.0), size: CGSize(width: closeButtonFrame.minX - leftInset, height: bounds.height)) diff --git a/submodules/TelegramUI/TelegramUI/EditableTokenListNode.swift b/submodules/TelegramUI/TelegramUI/EditableTokenListNode.swift index f87bd36ea9..b56223d772 100644 --- a/submodules/TelegramUI/TelegramUI/EditableTokenListNode.swift +++ b/submodules/TelegramUI/TelegramUI/EditableTokenListNode.swift @@ -32,15 +32,17 @@ final class EditableTokenListNodeTheme { let placeholderTextColor: UIColor let primaryTextColor: UIColor let selectedTextColor: UIColor + let selectedBackgroundColor: UIColor let accentColor: UIColor let keyboardColor: PresentationThemeKeyboardColor - init(backgroundColor: UIColor, separatorColor: UIColor, placeholderTextColor: UIColor, primaryTextColor: UIColor, selectedTextColor: UIColor, accentColor: UIColor, keyboardColor: PresentationThemeKeyboardColor) { + init(backgroundColor: UIColor, separatorColor: UIColor, placeholderTextColor: UIColor, primaryTextColor: UIColor, selectedTextColor: UIColor, selectedBackgroundColor: UIColor, accentColor: UIColor, keyboardColor: PresentationThemeKeyboardColor) { self.backgroundColor = backgroundColor self.separatorColor = separatorColor self.placeholderTextColor = placeholderTextColor self.primaryTextColor = primaryTextColor self.selectedTextColor = selectedTextColor + self.selectedBackgroundColor = selectedBackgroundColor self.accentColor = accentColor self.keyboardColor = keyboardColor } @@ -50,10 +52,12 @@ private final class TokenNode: ASDisplayNode { let theme: EditableTokenListNodeTheme let token: EditableTokenListToken let titleNode: ASTextNode + let selectedBackgroundNode: ASImageNode var isSelected: Bool { didSet { if self.isSelected != oldValue { self.titleNode.attributedText = NSAttributedString(string: token.title + ",", font: Font.regular(15.0), textColor: self.isSelected ? self.theme.selectedTextColor : self.theme.primaryTextColor) + self.selectedBackgroundNode.isHidden = !self.isSelected } } } @@ -64,10 +68,16 @@ private final class TokenNode: ASDisplayNode { self.titleNode = ASTextNode() self.titleNode.isUserInteractionEnabled = false self.titleNode.maximumNumberOfLines = 1 + self.selectedBackgroundNode = ASImageNode() + self.selectedBackgroundNode.displaysAsynchronously = false + self.selectedBackgroundNode.displayWithoutProcessing = true + self.selectedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 8.0, color: theme.selectedBackgroundColor) + self.selectedBackgroundNode.isHidden = !isSelected self.isSelected = isSelected super.init() + self.addSubnode(self.selectedBackgroundNode) self.titleNode.attributedText = NSAttributedString(string: token.title + ",", font: Font.regular(15.0), textColor: self.isSelected ? self.theme.selectedTextColor : self.theme.primaryTextColor) self.addSubnode(self.titleNode) } @@ -82,6 +92,7 @@ private final class TokenNode: ASDisplayNode { if titleSize.width.isZero { return } + self.selectedBackgroundNode.frame = self.bounds.insetBy(dx: 2.0, dy: 2.0) self.titleNode.frame = CGRect(origin: CGPoint(x: 4.0, y: floor((self.bounds.size.height - titleSize.height) / 2.0)), size: titleSize) } } @@ -102,6 +113,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { private let placeholderNode: ASTextNode private var tokenNodes: [TokenNode] = [] private let separatorNode: ASDisplayNode + private let textFieldScrollNode: ASScrollNode private let textFieldNode: TextFieldNode private let caretIndicatorNode: CaretIndicatorNode private var selectedTokenId: AnyHashable? @@ -120,6 +132,8 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.placeholderNode.maximumNumberOfLines = 1 self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: theme.placeholderTextColor) + self.textFieldScrollNode = ASScrollNode() + self.textFieldNode = TextFieldNode() self.textFieldNode.textField.font = Font.regular(15.0) self.textFieldNode.textField.textColor = theme.primaryTextColor @@ -144,7 +158,8 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.backgroundColor = theme.backgroundColor self.addSubnode(self.separatorNode) self.scrollNode.addSubnode(self.placeholderNode) - self.scrollNode.addSubnode(self.textFieldNode) + self.scrollNode.addSubnode(self.textFieldScrollNode) + self.textFieldScrollNode.addSubnode(self.textFieldNode) //self.scrollNode.addSubnode(self.caretIndicatorNode) self.clipsToBounds = true @@ -268,12 +283,15 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { let textNodeFrame = CGRect(origin: CGPoint(x: currentOffset.x + 4.0, y: currentOffset.y + UIScreenPixel), size: CGSize(width: width - currentOffset.x - sideInset - 8.0, height: 28.0)) let caretNodeFrame = CGRect(origin: CGPoint(x: textNodeFrame.minX, y: textNodeFrame.minY + 4.0 - UIScreenPixel), size: CGSize(width: 2.0, height: 19.0 + UIScreenPixel)) if case .immediate = transition { - transition.updateFrame(node: self.textFieldNode, frame: textNodeFrame) + transition.updateFrame(node: self.textFieldScrollNode, frame: textNodeFrame) + transition.updateFrame(node: self.textFieldNode, frame: CGRect(origin: CGPoint(), size: textNodeFrame.size)) transition.updateFrame(node: self.caretIndicatorNode, frame: caretNodeFrame) } else { - let previousFrame = self.textFieldNode.frame - self.textFieldNode.frame = textNodeFrame - self.textFieldNode.layer.animateFrame(from: previousFrame, to: textNodeFrame, duration: 0.2 + animationDelay, timingFunction: kCAMediaTimingFunctionSpring) + let previousFrame = self.textFieldScrollNode.frame + self.textFieldScrollNode.frame = textNodeFrame + self.textFieldScrollNode.layer.animateFrame(from: previousFrame, to: textNodeFrame, duration: 0.2 + animationDelay, timingFunction: kCAMediaTimingFunctionSpring) + + transition.updateFrame(node: self.textFieldNode, frame: CGRect(origin: CGPoint(), size: textNodeFrame.size)) let previousCaretFrame = self.caretIndicatorNode.frame self.caretIndicatorNode.frame = caretNodeFrame @@ -305,9 +323,13 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { } @objc func textFieldChanged(_ textField: UITextField) { - self.placeholderNode.isHidden = textField.text != nil && !textField.text!.isEmpty + let text = textField.text ?? "" + self.placeholderNode.isHidden = !text.isEmpty self.updateSelectedTokenId(nil) - self.textUpdated?(textField.text ?? "") + self.textUpdated?(text) + if !text.isEmpty { + self.scrollNode.view.scrollRectToVisible(textFieldScrollNode.frame.offsetBy(dx: 0.0, dy: 7.0), animated: true) + } } func textFieldDidBeginEditing(_ textField: UITextField) { @@ -332,7 +354,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { for tokenNode in self.tokenNodes { tokenNode.isSelected = id == tokenNode.token.id } - if id != nil { + if id != nil && !self.textFieldNode.textField.isFirstResponder { self.textFieldNode.textField.becomeFirstResponder() } } @@ -341,7 +363,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { if case .ended = recognizer.state { let point = recognizer.location(in: self.view) for tokenNode in self.tokenNodes { - if tokenNode.frame.contains(point) { + if tokenNode.bounds.contains(self.view.convert(point, to: tokenNode.view)) { self.updateSelectedTokenId(tokenNode.token.id) break } diff --git a/submodules/TelegramUI/TelegramUI/EmojisChatInputContextPanelNode.swift b/submodules/TelegramUI/TelegramUI/EmojisChatInputContextPanelNode.swift index 2ab319c268..7362cd166a 100644 --- a/submodules/TelegramUI/TelegramUI/EmojisChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/EmojisChatInputContextPanelNode.swift @@ -6,6 +6,7 @@ import TelegramCore import SyncCore import Display import TelegramPresentationData +import TelegramUIPreferences import MergeLists import AccountContext import Emoji @@ -103,8 +104,9 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { private var enqueuedTransitions: [(EmojisChatInputContextPanelTransition, Bool)] = [] private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)? + private var presentationInterfaceState: ChatPresentationInterfaceState? - override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { + override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { self.backgroundNode = ASImageNode() self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.displaysAsynchronously = false @@ -129,7 +131,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { self.listView.view.disablesInteractiveTransitionGestureRecognizer = true self.listView.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) - super.init(context: context, theme: theme, strings: strings) + super.init(context: context, theme: theme, strings: strings, fontSize: fontSize) self.placement = .overTextInput self.isOpaque = false @@ -191,6 +193,10 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { }) self.currentEntries = to self.enqueueTransition(transition, firstTime: firstTime) + + if let presentationInterfaceState = presentationInterfaceState, let (size, leftInset, rightInset, bottomInset) = self.validLayout { + self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, transition: .immediate, interfaceState: presentationInterfaceState) + } } private func enqueueTransition(_ transition: EmojisChatInputContextPanelTransition, firstTime: Bool) { @@ -208,12 +214,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { self.enqueuedTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() - if firstTime { - //options.insert(.Synchronous) - //options.insert(.LowLatency) - } else { - options.insert(.AnimateCrossfade) - } + options.insert(.Synchronous) let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: validLayout.0, insets: UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0), duration: 0.0, curve: .Default(duration: nil)) @@ -224,6 +225,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { let hadValidLayout = self.validLayout != nil self.validLayout = (size, leftInset, rightInset, bottomInset) + self.presentationInterfaceState = interfaceState let sideInsets: CGFloat = 10.0 + leftInset let contentWidth = min(size.width - sideInsets - sideInsets, max(24.0, CGFloat(self.currentEntries?.count ?? 0) * 45.0)) diff --git a/submodules/TelegramUI/TelegramUI/FetchCachedRepresentations.swift b/submodules/TelegramUI/TelegramUI/FetchCachedRepresentations.swift index 0f2a020c05..a2448e20a4 100644 --- a/submodules/TelegramUI/TelegramUI/FetchCachedRepresentations.swift +++ b/submodules/TelegramUI/TelegramUI/FetchCachedRepresentations.swift @@ -16,9 +16,12 @@ import WebP import Lottie import MediaResources import PhotoResources +import LocationResources import ImageBlur import TelegramAnimatedStickerNode import WallpaperResources +import Svg +import GZip public func fetchCachedResourceRepresentation(account: Account, resource: MediaResource, representation: CachedMediaResourceRepresentation) -> Signal { if let representation = representation as? CachedStickerAJpegRepresentation { @@ -81,7 +84,7 @@ public func fetchCachedResourceRepresentation(account: Account, resource: MediaR if !data.complete { return .complete() } - return fetchCachedPatternWallpaperRepresentation(account: account, resource: resource, resourceData: data, representation: representation) + return fetchCachedPatternWallpaperRepresentation(resource: resource, resourceData: data, representation: representation) } } else if let representation = representation as? CachedAlbumArtworkRepresentation { return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) @@ -407,12 +410,35 @@ private func fetchCachedBlurredWallpaperRepresentation(resource: MediaResource, private func fetchCachedPatternWallpaperMaskRepresentation(resource: MediaResource, resourceData: MediaResourceData, representation: CachedPatternWallpaperMaskRepresentation) -> Signal { return Signal({ subscriber in - if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { - if let image = UIImage(data: data) { - let path = NSTemporaryDirectory() + "\(arc4random64())" - let url = URL(fileURLWithPath: path) + if var data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { + let path = NSTemporaryDirectory() + "\(arc4random64())" + let url = URL(fileURLWithPath: path) + + if let unzippedData = TGGUnzipData(data, 2 * 1024 * 1024) { + data = unzippedData + } + + if data.count > 5, let string = String(data: data.subdata(in: 0 ..< 5), encoding: .utf8), string == " Signal { return Signal({ subscriber in - if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { - if let image = UIImage(data: data) { - let path = NSTemporaryDirectory() + "\(arc4random64())" - let url = URL(fileURLWithPath: path) - - let size = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale) - - let backgroundColor = UIColor(rgb: UInt32(bitPattern: representation.color)) - let foregroundColor = patternColor(for: backgroundColor, intensity: CGFloat(representation.intensity) / 100.0) - - let colorImage = generateImage(size, contextGenerator: { size, c in + if var data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { + if let unzippedData = TGGUnzipData(data, 2 * 1024 * 1024) { + data = unzippedData + } + + let path = NSTemporaryDirectory() + "\(arc4random64())" + let url = URL(fileURLWithPath: path) + + var colors: [UIColor] = [] + if let bottomColor = representation.bottomColor { + colors.append(UIColor(rgb: bottomColor)) + } + colors.append(UIColor(rgb: representation.color)) + + let intensity = CGFloat(representation.intensity) / 100.0 + + var size: CGSize? + var maskImage: UIImage? + if data.count > 5, let string = String(data: data.subdata(in: 0 ..< 5), encoding: .utf8), string == " runOn(Queue.concurrentDefaultQueue()) } -private func fetchCachedPatternWallpaperMaskRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedPatternWallpaperMaskRepresentation) -> Signal { - return Signal({ subscriber in - if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { - if let image = UIImage(data: data) { - let path = NSTemporaryDirectory() + "\(arc4random64())" - let url = URL(fileURLWithPath: path) - - let size = representation.size != nil ? image.size.aspectFitted(representation.size!) : CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale) - - let alphaImage = generateImage(size, contextGenerator: { size, context in - context.setFillColor(UIColor.black.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - context.clip(to: CGRect(origin: CGPoint(), size: size), mask: image.cgImage!) - context.setFillColor(UIColor.white.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - }, scale: 1.0) - - if let alphaImage = alphaImage, let alphaDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeJPEG, 1, nil) { - CGImageDestinationSetProperties(alphaDestination, [:] as CFDictionary) - - let colorQuality: Float = 0.87 - - let options = NSMutableDictionary() - options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) - - CGImageDestinationAddImage(alphaDestination, alphaImage.cgImage!, options as CFDictionary) - if CGImageDestinationFinalize(alphaDestination) { - subscriber.putNext(.temporaryPath(path)) - subscriber.putCompletion() - } - } - } - } - return EmptyDisposable - }) |> runOn(Queue.concurrentDefaultQueue()) -} - -private func fetchCachedPatternWallpaperRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedPatternWallpaperRepresentation) -> Signal { - return Signal({ subscriber in - if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { - if let image = UIImage(data: data) { - let path = NSTemporaryDirectory() + "\(arc4random64())" - let url = URL(fileURLWithPath: path) - - let size = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale) - - let backgroundColor = UIColor(rgb: UInt32(bitPattern: representation.color)) - let foregroundColor = patternColor(for: backgroundColor, intensity: CGFloat(representation.intensity) / 100.0) - - let colorImage = generateImage(size, contextGenerator: { size, c in - let rect = CGRect(origin: CGPoint(), size: size) - c.setBlendMode(.copy) - c.setFillColor(backgroundColor.cgColor) - c.fill(rect) - - c.setBlendMode(.normal) - if let cgImage = image.cgImage { - c.clip(to: rect, mask: cgImage) - } - c.setFillColor(foregroundColor.cgColor) - c.fill(rect) - }, scale: 1.0) - - if let colorImage = colorImage, let colorDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeJPEG, 1, nil) { - CGImageDestinationSetProperties(colorDestination, [:] as CFDictionary) - - let colorQuality: Float = 0.9 - - let options = NSMutableDictionary() - options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) - - CGImageDestinationAddImage(colorDestination, colorImage.cgImage!, options as CFDictionary) - if CGImageDestinationFinalize(colorDestination) { - subscriber.putNext(.temporaryPath(path)) - subscriber.putCompletion() - } - } - } - } - return EmptyDisposable - }) |> runOn(Queue.concurrentDefaultQueue()) -} - public enum FetchAlbumArtworkError { case moreDataNeeded(Int) } @@ -892,8 +896,9 @@ private func fetchAnimatedStickerRepresentation(account: Account, resource: Medi return Signal({ subscriber in if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { if #available(iOS 9.0, *) { - return experimentalConvertCompressedLottieToCombinedMp4(data: data, size: CGSize(width: CGFloat(representation.width), height: CGFloat(representation.height)), fitzModifier: representation.fitzModifier, cacheKey: "\(resource.id.uniqueId)-\(representation.uniqueId)").start(next: { path in - subscriber.putNext(.temporaryPath(path)) + return experimentalConvertCompressedLottieToCombinedMp4(data: data, size: CGSize(width: CGFloat(representation.width), height: CGFloat(representation.height)), fitzModifier: representation.fitzModifier, cacheKey: "\(resource.id.uniqueId)-\(representation.uniqueId)").start(next: { value in + subscriber.putNext(value) + }, completed: { subscriber.putCompletion() }) } else { diff --git a/submodules/TelegramUI/TelegramUI/FetchVideoMediaResource.swift b/submodules/TelegramUI/TelegramUI/FetchVideoMediaResource.swift index 904bf6c053..74896b11b0 100644 --- a/submodules/TelegramUI/TelegramUI/FetchVideoMediaResource.swift +++ b/submodules/TelegramUI/TelegramUI/FetchVideoMediaResource.swift @@ -360,7 +360,12 @@ func fetchLocalFileVideoMediaResource(postbox: Postbox, resource: LocalFileVideo let signal = Signal { subscriber in subscriber.putNext(.reset) - let avAsset = AVURLAsset(url: URL(fileURLWithPath: resource.path)) + var filteredPath = resource.path + if filteredPath.hasPrefix("file://") { + filteredPath = String(filteredPath[filteredPath.index(filteredPath.startIndex, offsetBy: "file://".count)]) + } + + let avAsset = AVURLAsset(url: URL(fileURLWithPath: filteredPath)) var adjustments: TGVideoEditAdjustments? if let videoAdjustments = resource.adjustments { if let dict = NSKeyedUnarchiver.unarchiveObject(with: videoAdjustments.data.makeData()) as? [AnyHashable : Any] { diff --git a/submodules/TelegramUI/TelegramUI/FileMediaResourceStatus.swift b/submodules/TelegramUI/TelegramUI/FileMediaResourceStatus.swift index df635fa8b4..03980fc8e1 100644 --- a/submodules/TelegramUI/TelegramUI/FileMediaResourceStatus.swift +++ b/submodules/TelegramUI/TelegramUI/FileMediaResourceStatus.swift @@ -33,7 +33,7 @@ func messageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMedia } } -func messageFileMediaResourceStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool) -> Signal { +func messageFileMediaResourceStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isSharedMedia: Bool = false) -> Signal { let playbackStatus = internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions) |> map { status -> MediaPlayerPlaybackStatus? in return status?.status } diff --git a/submodules/TelegramUI/TelegramUI/GifPaneSearchContentNode.swift b/submodules/TelegramUI/TelegramUI/GifPaneSearchContentNode.swift index 48c50b6f43..606f2431d9 100644 --- a/submodules/TelegramUI/TelegramUI/GifPaneSearchContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/GifPaneSearchContentNode.swift @@ -28,7 +28,7 @@ func paneGifSearchForQuery(account: Account, query: String, updateActivity: ((Bo } |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> in if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { - let results = requestContextResults(account: account, botId: user.id, query: query, peerId: account.peerId, limit: 64) + let results = requestContextResults(account: account, botId: user.id, query: query, peerId: account.peerId, limit: 15) |> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in return { _ in return .contextRequestResult(user, results) diff --git a/submodules/TelegramUI/TelegramUI/GridMessageItem.swift b/submodules/TelegramUI/TelegramUI/GridMessageItem.swift index 5aa593044b..a396526820 100644 --- a/submodules/TelegramUI/TelegramUI/GridMessageItem.swift +++ b/submodules/TelegramUI/TelegramUI/GridMessageItem.swift @@ -7,6 +7,7 @@ import SyncCore import Postbox import SwiftSignalKit import TelegramPresentationData +import TelegramUIPreferences import TelegramStringFormatting import AccountContext import RadialStatusNode @@ -39,6 +40,7 @@ final class GridMessageItemSection: GridSection { fileprivate let theme: PresentationTheme private let strings: PresentationStrings + private let fontSize: PresentationFontSize private let roundedTimestamp: Int32 private let month: Int32 @@ -48,9 +50,10 @@ final class GridMessageItemSection: GridSection { return self.roundedTimestamp.hashValue } - init(timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings) { + init(timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { self.theme = theme self.strings = strings + self.fontSize = fontSize var now = time_t(timestamp) var timeinfoNow: tm = tm() @@ -70,20 +73,20 @@ final class GridMessageItemSection: GridSection { } func node() -> ASDisplayNode { - return GridMessageItemSectionNode(theme: self.theme, strings: self.strings, roundedTimestamp: self.roundedTimestamp, month: self.month, year: self.year) + return GridMessageItemSectionNode(theme: self.theme, strings: self.strings, fontSize: self.fontSize, roundedTimestamp: self.roundedTimestamp, month: self.month, year: self.year) } } -private let sectionTitleFont = Font.regular(14.0) - final class GridMessageItemSectionNode: ASDisplayNode { var theme: PresentationTheme var strings: PresentationStrings + var fontSize: PresentationFontSize let titleNode: ASTextNode - init(theme: PresentationTheme, strings: PresentationStrings, roundedTimestamp: Int32, month: Int32, year: Int32) { + init(theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, roundedTimestamp: Int32, month: Int32, year: Int32) { self.theme = theme self.strings = strings + self.fontSize = fontSize self.titleNode = ASTextNode() self.titleNode.isUserInteractionEnabled = false @@ -92,6 +95,8 @@ final class GridMessageItemSectionNode: ASDisplayNode { self.backgroundColor = theme.list.plainBackgroundColor.withAlphaComponent(0.9) + let sectionTitleFont = Font.regular(floor(fontSize.baseDisplaySize * 14.0 / 17.0)) + let dateText = stringForMonth(strings: strings, month: month, ofYear: year) self.addSubnode(self.titleNode) self.titleNode.attributedText = NSAttributedString(string: dateText, font: sectionTitleFont, textColor: theme.list.itemPrimaryTextColor) @@ -105,7 +110,7 @@ final class GridMessageItemSectionNode: ASDisplayNode { let bounds = self.bounds let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude)) - self.titleNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 8.0), size: titleSize) + self.titleNode.frame = CGRect(origin: CGPoint(x: 12.0, y: floor((bounds.size.height - titleSize.height) / 2.0)), size: titleSize) } } @@ -117,13 +122,13 @@ final class GridMessageItem: GridItem { private let controllerInteraction: ChatControllerInteraction let section: GridSection? - init(theme: PresentationTheme, strings: PresentationStrings, context: AccountContext, message: Message, controllerInteraction: ChatControllerInteraction) { + init(theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, context: AccountContext, message: Message, controllerInteraction: ChatControllerInteraction) { self.theme = theme self.strings = strings self.context = context self.message = message self.controllerInteraction = controllerInteraction - self.section = GridMessageItemSection(timestamp: message.timestamp, theme: theme, strings: strings) + self.section = GridMessageItemSection(timestamp: message.timestamp, theme: theme, strings: strings, fontSize: fontSize) } func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { @@ -374,10 +379,10 @@ final class GridMessageItemNode: GridItemNode { } } - func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.messageId == id { let imageNode = self.imageNode - return (self.imageNode, { [weak self, weak imageNode] in + return (self.imageNode, self.imageNode.bounds, { [weak self, weak imageNode] in var statusNodeHidden = false var accessoryHidden = false if let strongSelf = self { diff --git a/submodules/TelegramUI/TelegramUI/HashtagChatInputContextPanelNode.swift b/submodules/TelegramUI/TelegramUI/HashtagChatInputContextPanelNode.swift index b39be5bc2a..13a84953cb 100644 --- a/submodules/TelegramUI/TelegramUI/HashtagChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/HashtagChatInputContextPanelNode.swift @@ -6,9 +6,11 @@ import TelegramCore import SyncCore import Display import TelegramPresentationData +import TelegramUIPreferences import MergeLists import AccountContext import AccountContext +import ItemListUI private struct HashtagChatInputContextPanelEntryStableId: Hashable { let text: String @@ -18,25 +20,26 @@ private struct HashtagChatInputContextPanelEntry: Comparable, Identifiable { let index: Int let theme: PresentationTheme let text: String + let revealed: Bool var stableId: HashtagChatInputContextPanelEntryStableId { return HashtagChatInputContextPanelEntryStableId(text: self.text) } func withUpdatedTheme(_ theme: PresentationTheme) -> HashtagChatInputContextPanelEntry { - return HashtagChatInputContextPanelEntry(index: self.index, theme: theme, text: self.text) + return HashtagChatInputContextPanelEntry(index: self.index, theme: theme, text: self.text, revealed: self.revealed) } static func ==(lhs: HashtagChatInputContextPanelEntry, rhs: HashtagChatInputContextPanelEntry) -> Bool { - return lhs.index == rhs.index && lhs.text == rhs.text && lhs.theme === rhs.theme + return lhs.index == rhs.index && lhs.text == rhs.text && lhs.theme === rhs.theme && lhs.revealed == rhs.revealed } static func <(lhs: HashtagChatInputContextPanelEntry, rhs: HashtagChatInputContextPanelEntry) -> Bool { return lhs.index < rhs.index } - func item(account: Account, hashtagSelected: @escaping (String) -> Void) -> ListViewItem { - return HashtagChatInputPanelItem(theme: self.theme, text: self.text, hashtagSelected: hashtagSelected) + func item(account: Account, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> ListViewItem { + return HashtagChatInputPanelItem(presentationData: ItemListPresentationData(presentationData), text: self.text, revealed: self.revealed, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested) } } @@ -46,12 +49,12 @@ private struct HashtagChatInputContextPanelTransition { let updates: [ListViewUpdateItem] } -private func preparedTransition(from fromEntries: [HashtagChatInputContextPanelEntry], to toEntries: [HashtagChatInputContextPanelEntry], account: Account, hashtagSelected: @escaping (String) -> Void) -> HashtagChatInputContextPanelTransition { +private func preparedTransition(from fromEntries: [HashtagChatInputContextPanelEntry], to toEntries: [HashtagChatInputContextPanelEntry], account: Account, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> HashtagChatInputContextPanelTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, hashtagSelected: hashtagSelected), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, hashtagSelected: hashtagSelected), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested), directionHint: nil) } return HashtagChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) } @@ -60,10 +63,13 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { private let listView: ListView private var currentEntries: [HashtagChatInputContextPanelEntry]? + private var currentResults: [String] = [] + private var revealedHashtag: String? + private var enqueuedTransitions: [(HashtagChatInputContextPanelTransition, Bool)] = [] private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)? - override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { + override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { self.listView = ListView() self.listView.isOpaque = false self.listView.stackFromBottom = true @@ -71,7 +77,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { self.listView.limitHitTestToNodes = true self.listView.view.disablesInteractiveTransitionGestureRecognizer = true - super.init(context: context, theme: theme, strings: strings) + super.init(context: context, theme: theme, strings: strings, fontSize: fontSize) self.isOpaque = false self.clipsToBounds = true @@ -80,11 +86,13 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { } func updateResults(_ results: [String]) { + self.currentResults = results + var entries: [HashtagChatInputContextPanelEntry] = [] var index = 0 var stableIds = Set() for text in results { - let entry = HashtagChatInputContextPanelEntry(index: index, theme: self.theme, text: text) + let entry = HashtagChatInputContextPanelEntry(index: index, theme: self.theme, text: text, revealed: text == self.revealedHashtag) if stableIds.contains(entry.stableId) { continue } @@ -97,7 +105,13 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { private func prepareTransition(from: [HashtagChatInputContextPanelEntry]? , to: [HashtagChatInputContextPanelEntry]) { let firstTime = from == nil - let transition = preparedTransition(from: from ?? [], to: to, account: self.context.account, hashtagSelected: { [weak self] text in + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let transition = preparedTransition(from: from ?? [], to: to, account: self.context.account, presentationData: presentationData, setHashtagRevealed: { [weak self] text in + if let strongSelf = self { + strongSelf.revealedHashtag = text + strongSelf.updateResults(strongSelf.currentResults) + } + }, hashtagSelected: { [weak self] text in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in var hashtagQueryRange: NSRange? @@ -122,6 +136,11 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { return (textInputState, inputMode) } } + }, removeRequested: { [weak self] text in + if let strongSelf = self { + let _ = removeRecentlyUsedHashtag(postbox: strongSelf.context.account.postbox, string: text).start() + strongSelf.revealedHashtag = nil + } }) self.currentEntries = to self.enqueueTransition(transition, firstTime: firstTime) @@ -192,29 +211,8 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: listViewCurve) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve) self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) diff --git a/submodules/TelegramUI/TelegramUI/HashtagChatInputPanelItem.swift b/submodules/TelegramUI/TelegramUI/HashtagChatInputPanelItem.swift index 9af929732a..1f8a32d4a7 100644 --- a/submodules/TelegramUI/TelegramUI/HashtagChatInputPanelItem.swift +++ b/submodules/TelegramUI/TelegramUI/HashtagChatInputPanelItem.swift @@ -7,18 +7,26 @@ import SyncCore import SwiftSignalKit import Postbox import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI final class HashtagChatInputPanelItem: ListViewItem { - fileprivate let theme: PresentationTheme + fileprivate let presentationData: ItemListPresentationData fileprivate let text: String + fileprivate let revealed: Bool + fileprivate let setHashtagRevealed: (String?) -> Void private let hashtagSelected: (String) -> Void + fileprivate let removeRequested: (String) -> Void let selectable: Bool = true - public init(theme: PresentationTheme, text: String, hashtagSelected: @escaping (String) -> Void) { - self.theme = theme + public init(presentationData: ItemListPresentationData, text: String, revealed: Bool, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) { + self.presentationData = presentationData self.text = text + self.revealed = revealed + self.setHashtagRevealed = setHashtagRevealed self.hashtagSelected = hashtagSelected + self.removeRequested = removeRequested } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -69,18 +77,31 @@ final class HashtagChatInputPanelItem: ListViewItem { } func selected(listView: ListView) { - self.hashtagSelected(self.text) + if self.revealed { + self.setHashtagRevealed(nil) + } else { + self.hashtagSelected(self.text) + } } } -private let textFont = Font.medium(14.0) - final class HashtagChatInputPanelItemNode: ListViewItemNode { static let itemHeight: CGFloat = 42.0 private let textNode: TextNode private let topSeparatorNode: ASDisplayNode private let separatorNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode + + private var revealNode: ItemListRevealOptionsNode? + private var revealOptions: [ItemListRevealOption] = [] + private var initialRevealOffset: CGFloat = 0.0 + public private(set) var revealOffset: CGFloat = 0.0 + private var recognizer: ItemListRevealOptionsGestureRecognizer? + private var hapticFeedback: HapticFeedback? + + private var item: HashtagChatInputPanelItem? + + private var validLayout: (CGSize, CGFloat, CGFloat)? init() { self.textNode = TextNode() @@ -101,6 +122,15 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { self.addSubnode(self.textNode) } + override func didLoad() { + super.didLoad() + + let recognizer = ItemListRevealOptionsGestureRecognizer(target: self, action: #selector(self.revealGesture(_:))) + self.recognizer = recognizer + recognizer.allowAnyDirection = false + self.view.addGestureRecognizer(recognizer) + } + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? HashtagChatInputPanelItem { let doLayout = self.asyncLayout() @@ -115,24 +145,31 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { func asyncLayout() -> (_ item: HashtagChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let makeTextLayout = TextNode.asyncLayout(self.textNode) return { [weak self] item, params, mergedTop, mergedBottom in + let textFont = Font.medium(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)) + let baseWidth = params.width - params.leftInset - params.rightInset let leftInset: CGFloat = 15.0 + params.leftInset let rightInset: CGFloat = 10.0 + params.rightInset - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "#\(item.text)", font: textFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "#\(item.text)", font: textFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) - return (nodeLayout, { _ in + return (nodeLayout, { animation in if let strongSelf = self { - strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.backgroundColor = item.theme.list.plainBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.item = item + strongSelf.validLayout = (nodeLayout.contentSize, params.leftInset, params.rightInset) + + let revealOffset = strongSelf.revealOffset + + strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor + strongSelf.topSeparatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor + strongSelf.backgroundColor = item.presentationData.theme.list.plainBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor let _ = textApply() - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: floor((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size) strongSelf.topSeparatorNode.isHidden = mergedTop strongSelf.separatorNode.isHidden = !mergedBottom @@ -141,14 +178,27 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset, height: UIScreenPixel)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel)) + + strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]) + strongSelf.setRevealOptionsOpened(item.revealed, animated: animation.isAnimated) } }) } } + func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + if let (size, leftInset, rightInset) = self.validLayout { + transition.updateFrameAdditive(node: self.textNode, frame: CGRect(origin: CGPoint(x: min(offset, 0.0) + 15.0 + leftInset, y: self.textNode.frame.minY), size: self.textNode.frame.size)) + } + } + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) + if let revealNode = self.revealNode, self.revealOffset != 0 { + return + } + if highlighted { self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { @@ -171,4 +221,199 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { } } } + + func setRevealOptions(_ options: [ItemListRevealOption]) { + if self.revealOptions == options { + return + } + let previousOptions = self.revealOptions + let wasEmpty = self.revealOptions.isEmpty + self.revealOptions = options + let isEmpty = options.isEmpty + if options.isEmpty { + if let _ = self.revealNode { + self.recognizer?.becomeCancelled() + self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring)) + } + } + if wasEmpty != isEmpty { + self.recognizer?.isEnabled = !isEmpty + } + } + + private func setRevealOptionsOpened(_ value: Bool, animated: Bool) { + if value != !self.revealOffset.isZero { + if !self.revealOffset.isZero { + self.recognizer?.becomeCancelled() + } + let transition: ContainedViewLayoutTransition + if animated { + transition = .animated(duration: 0.3, curve: .spring) + } else { + transition = .immediate + } + if value { + if self.revealNode == nil { + self.setupAndAddRevealNode() + if let revealNode = self.revealNode, revealNode.isNodeLoaded, let _ = self.validLayout { + revealNode.layout() + let revealSize = revealNode.bounds.size + self.updateRevealOffsetInternal(offset: -revealSize.width, transition: transition) + } + } + } else if !self.revealOffset.isZero { + self.updateRevealOffsetInternal(offset: 0.0, transition: transition) + } + } + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if let recognizer = self.recognizer, otherGestureRecognizer == recognizer { + return true + } else { + return false + } + } + + @objc func revealGesture(_ recognizer: ItemListRevealOptionsGestureRecognizer) { + guard let (size, _, _) = self.validLayout else { + return + } + switch recognizer.state { + case .began: + if let revealNode = self.revealNode { + let revealSize = revealNode.bounds.size + let location = recognizer.location(in: self.view) + if location.x > size.width - revealSize.width { + recognizer.becomeCancelled() + } else { + self.initialRevealOffset = self.revealOffset + } + } else { + if self.revealOptions.isEmpty { + recognizer.becomeCancelled() + } + self.initialRevealOffset = self.revealOffset + } + case .changed: + var translation = recognizer.translation(in: self.view) + translation.x += self.initialRevealOffset + if self.revealNode == nil && translation.x.isLess(than: 0.0) { + self.setupAndAddRevealNode() + self.revealOptionsInteractivelyOpened() + } + self.updateRevealOffsetInternal(offset: translation.x, transition: .immediate) + if self.revealNode == nil { + self.revealOptionsInteractivelyClosed() + } + case .ended, .cancelled: + guard let recognizer = self.recognizer else { + break + } + + if let revealNode = self.revealNode { + let velocity = recognizer.velocity(in: self.view) + let revealSize = revealNode.bounds.size + var reveal = false + if abs(velocity.x) < 100.0 { + if self.initialRevealOffset.isZero && self.revealOffset < 0.0 { + reveal = true + } else if self.revealOffset < -revealSize.width { + reveal = true + } else { + reveal = false + } + } else { + if velocity.x < 0.0 { + reveal = true + } else { + reveal = false + } + } + self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring)) + if !reveal { + self.revealOptionsInteractivelyClosed() + } + } + default: + break + } + } + + private func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { + guard let item = self.item else { + return + } + item.removeRequested(item.text) + } + + private func setupAndAddRevealNode() { + if !self.revealOptions.isEmpty { + let revealNode = ItemListRevealOptionsNode(optionSelected: { [weak self] option in + self?.revealOptionSelected(option, animated: false) + }, tapticAction: { [weak self] in + self?.hapticImpact() + }) + revealNode.setOptions(self.revealOptions, isLeft: false) + self.revealNode = revealNode + + if let (size, _, rightInset) = self.validLayout { + var revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height)) + revealSize.width += rightInset + + revealNode.frame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize) + revealNode.updateRevealOffset(offset: 0.0, sideInset: -rightInset, transition: .immediate) + } + + self.addSubnode(revealNode) + } + } + + private func updateRevealOffsetInternal(offset: CGFloat, transition: ContainedViewLayoutTransition) { + self.revealOffset = offset + guard let (size, leftInset, rightInset) = self.validLayout else { + return + } + + if let revealNode = self.revealNode { + let revealSize = revealNode.bounds.size + + let revealFrame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize) + let revealNodeOffset = -max(self.revealOffset, -revealSize.width) + revealNode.updateRevealOffset(offset: revealNodeOffset, sideInset: -rightInset, transition: transition) + + if CGFloat(0.0).isLessThanOrEqualTo(offset) { + self.revealNode = nil + transition.updateFrame(node: revealNode, frame: revealFrame, completion: { [weak revealNode] _ in + revealNode?.removeFromSupernode() + }) + } else { + transition.updateFrame(node: revealNode, frame: revealFrame) + } + } + self.updateRevealOffset(offset: offset, transition: transition) + } + + func revealOptionsInteractivelyOpened() { + if let item = self.item { + item.setHashtagRevealed(item.text) + } + } + + func revealOptionsInteractivelyClosed() { + if let item = self.item { + item.setHashtagRevealed(nil) + } + } + + private func hapticImpact() { + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + self.hapticFeedback?.impact(.medium) + } } diff --git a/submodules/TelegramUI/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift index 83cc958b92..d241bb0663 100644 --- a/submodules/TelegramUI/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -7,6 +7,7 @@ import SyncCore import Display import SwiftSignalKit import TelegramPresentationData +import TelegramUIPreferences import MergeLists import AccountContext import StickerPackPreviewUI @@ -86,7 +87,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont private var enqueuedTransitions: [(HorizontalListContextResultsChatInputContextPanelTransition, Bool)] = [] private var hasValidLayout = false - override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { + override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { self.strings = strings self.separatorNode = ASDisplayNode() @@ -100,7 +101,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont self.listView.transform = CATransform3DMakeRotation(-CGFloat(CGFloat.pi / 2.0), 0.0, 0.0, 1.0) self.listView.isHidden = true - super.init(context: context, theme: theme, strings: strings) + super.init(context: context, theme: theme, strings: strings, fontSize: fontSize) self.isOpaque = false self.clipsToBounds = true @@ -111,7 +112,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont self.listView.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in if let strongSelf = self, let state = opaqueTransactionState as? HorizontalListContextResultsOpaqueState { if let visible = displayedRange.visibleRange { - if state.hasMore && visible.lastIndex <= state.entryCount - 10 { + if state.hasMore && visible.lastIndex >= state.entryCount - 10 { strongSelf.loadMore() } } @@ -127,6 +128,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont super.didLoad() self.listView.view.disablesInteractiveTransitionGestureRecognizer = true + self.listView.view.disablesInteractiveKeyboardGestureRecognizer = true self.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in if let strongSelf = self { let convertedPoint = strongSelf.listView.view.convert(point, from: strongSelf.view) @@ -149,17 +151,16 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont } menuItems.append(PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: { _, _ in if let strongSelf = self { - let controller = StickerPackPreviewController(context: strongSelf.context, stickerPack: packReference, parentNavigationController: strongSelf.interfaceInteraction?.getNavigationController()) - controller.sendSticker = { file, sourceNode, sourceRect in - if let strongSelf = self { - return strongSelf.interfaceInteraction?.sendSticker(file, sourceNode, sourceRect) ?? false - } else { - return false - } - } + let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.interfaceInteraction?.getNavigationController(), sendSticker: { file, sourceNode, sourceRect in + if let strongSelf = self { + return strongSelf.interfaceInteraction?.sendSticker(file, sourceNode, sourceRect) ?? false + } else { + return false + } + }) - strongSelf.interfaceInteraction?.getNavigationController()?.view.window?.endEditing(true) - strongSelf.interfaceInteraction?.presentController(controller, nil) + strongSelf.interfaceInteraction?.getNavigationController()?.view.window?.endEditing(true) + strongSelf.interfaceInteraction?.presentController(controller, nil) } return true })) @@ -313,29 +314,9 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont var insets = UIEdgeInsets() insets.top = leftInset insets.bottom = rightInset - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: listHeight, height: size.width), insets: insets, duration: duration, curve: listViewCurve) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: listHeight, height: size.width), insets: insets, duration: duration, curve: curve) self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) diff --git a/submodules/TelegramUI/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift index 0a7ae4cc7d..132e096b62 100644 --- a/submodules/TelegramUI/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift @@ -310,7 +310,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode updateImageSignal = chatMessageSticker(account: item.account, file: stickerFile, small: false, fetched: true) } else { let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(CGSize(width: fittedImageDimensions.width * 2.0, height: fittedImageDimensions.height * 2.0)), resource: imageResource) - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) updateImageSignal = chatMessagePhoto(postbox: item.account.postbox, photoReference: .standalone(media: tmpImage)) } } else { diff --git a/submodules/TelegramUI/TelegramUI/HorizontalStickersChatContextPanelNode.swift b/submodules/TelegramUI/TelegramUI/HorizontalStickersChatContextPanelNode.swift index 45973f089f..6cfd6880a6 100755 --- a/submodules/TelegramUI/TelegramUI/HorizontalStickersChatContextPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/HorizontalStickersChatContextPanelNode.swift @@ -7,6 +7,7 @@ import SyncCore import Display import SwiftSignalKit import TelegramPresentationData +import TelegramUIPreferences import MergeLists import AccountContext import StickerPackPreviewUI @@ -109,7 +110,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { private var stickerPreviewController: StickerPreviewController? - override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { + override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { self.strings = strings self.backgroundNode = ASImageNode() @@ -136,7 +137,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { self.stickersInteraction = HorizontalStickersChatContextPanelInteraction() - super.init(context: context, theme: theme, strings: strings) + super.init(context: context, theme: theme, strings: strings, fontSize: fontSize) self.placement = .overTextInput self.isOpaque = false @@ -152,6 +153,9 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { override func didLoad() { super.didLoad() + self.gridNode.view.disablesInteractiveTransitionGestureRecognizer = true + self.gridNode.view.disablesInteractiveKeyboardGestureRecognizer = true + self.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in if let strongSelf = self { let convertedPoint = strongSelf.gridNode.view.convert(point, from: strongSelf.view) @@ -187,14 +191,13 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { switch attribute { case let .Sticker(_, packReference, _): if let packReference = packReference { - let controller = StickerPackPreviewController(context: strongSelf.context, stickerPack: packReference, parentNavigationController: controllerInteraction.navigationController()) - controller.sendSticker = { file, sourceNode, sourceRect in + let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { return controllerInteraction.sendSticker(file, true, sourceNode, sourceRect) } else { return false } - } + }) controllerInteraction.navigationController()?.view.window?.endEditing(true) controllerInteraction.presentController(controller, nil) @@ -208,7 +211,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { } return true }), - PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: { _, _ in return true }) + PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true }) ] return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) } else { @@ -315,6 +318,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { } override func animateOut(completion: @escaping () -> Void) { + self.layer.allowsGroupOpacity = true self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in completion() }) diff --git a/submodules/TelegramUI/TelegramUI/LegacyCamera.swift b/submodules/TelegramUI/TelegramUI/LegacyCamera.swift index d745057a2d..3a04cedcb3 100644 --- a/submodules/TelegramUI/TelegramUI/LegacyCamera.swift +++ b/submodules/TelegramUI/TelegramUI/LegacyCamera.swift @@ -20,7 +20,7 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, cameraView: TGAt legacyController.deferScreenEdgeGestures = [.top] let isSecretChat = peer.id.namespace == Namespaces.Peer.SecretChat - + let controller: TGCameraController if let cameraView = cameraView, let previewView = cameraView.previewView() { controller = TGCameraController(context: legacyController.context, saveEditedPhotos: saveCapturedPhotos && !isSecretChat, saveCapturedMedia: saveCapturedPhotos && !isSecretChat, camera: previewView.camera, previewView: previewView, intent: TGCameraControllerGenericIntent) @@ -73,6 +73,7 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, cameraView: TGAt controller.hasSilentPosting = !isSecretChat } controller.hasSchedule = hasSchedule + controller.reminder = peer.id == context.account.peerId let screenSize = parentController.view.bounds.size var startFrame = CGRect(x: 0, y: screenSize.height, width: screenSize.width, height: screenSize.height) diff --git a/submodules/TelegramUI/TelegramUI/LegacyInstantVideoController.swift b/submodules/TelegramUI/TelegramUI/LegacyInstantVideoController.swift index 1e901014e4..39da4e104f 100644 --- a/submodules/TelegramUI/TelegramUI/LegacyInstantVideoController.swift +++ b/submodules/TelegramUI/TelegramUI/LegacyInstantVideoController.swift @@ -99,7 +99,9 @@ func legacyInputMicPalette(from theme: PresentationTheme) -> TGModernConversatio return TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: inputPanelTheme.panelBackgroundColor, borderColor: inputPanelTheme.panelSeparatorColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor) } -func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, context: AccountContext, peerId: PeerId, slowmodeState: ChatSlowmodeState?, send: @escaping (EnqueueMessage) -> Void, displaySlowmodeTooltip: @escaping (ASDisplayNode, CGRect) -> Void) -> InstantVideoController { +func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, context: AccountContext, peerId: PeerId, slowmodeState: ChatSlowmodeState?, hasSchedule: Bool, send: @escaping (EnqueueMessage) -> Void, displaySlowmodeTooltip: @escaping (ASDisplayNode, CGRect) -> Void, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void) -> InstantVideoController { + let isSecretChat = peerId.namespace == Namespaces.Peer.SecretChat + let legacyController = InstantVideoController(presentation: .custom, theme: theme) legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all) legacyController.lockOrientation = true @@ -127,8 +129,13 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, let node = ChatSendButtonRadialStatusView(color: theme.chat.inputPanel.panelControlAccentColor) node.slowmodeState = slowmodeState return node - })! - controller.finishedWithVideo = { videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments in + }, canSendSilently: !isSecretChat, canSchedule: hasSchedule, reminder: peerId == context.account.peerId)! + controller.presentScheduleController = { done in + presentSchedulePicker { time in + done?(time) + } + } + controller.finishedWithVideo = { videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments, isSilent, scheduleTimestamp in guard let videoUrl = videoUrl else { return } @@ -179,8 +186,29 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, } let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: Int(finalDuration), size: PixelDimensions(finalDimensions), flags: [.instantRoundVideo])]) - let attributes: [MessageAttribute] = [] - send(.message(text: "", attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil)) + var message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil) + + let scheduleTime: Int32? = scheduleTimestamp > 0 ? scheduleTimestamp : nil + + message = message.withUpdatedAttributes { attributes in + var attributes = attributes + for i in (0 ..< attributes.count).reversed() { + if attributes[i] is NotificationInfoMessageAttribute { + attributes.remove(at: i) + } else if let _ = scheduleTime, attributes[i] is OutgoingScheduleInfoMessageAttribute { + attributes.remove(at: i) + } + } + if isSilent { + attributes.append(NotificationInfoMessageAttribute(flags: .muted)) + } + if let scheduleTime = scheduleTime { + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime)) + } + return attributes + } + + send(message) } controller.didDismiss = { [weak legacyController] in if let legacyController = legacyController { diff --git a/submodules/TelegramUI/TelegramUI/LegacyLiveUploadInterface.swift b/submodules/TelegramUI/TelegramUI/LegacyLiveUploadInterface.swift index 82c7b859f7..eaa93bf258 100644 --- a/submodules/TelegramUI/TelegramUI/LegacyLiveUploadInterface.swift +++ b/submodules/TelegramUI/TelegramUI/LegacyLiveUploadInterface.swift @@ -62,6 +62,7 @@ final class LegacyLiveUploadInterface: VideoConversionWatcher, TGLiveUploadInter override func fileUpdated(_ completed: Bool) -> Any! { let _ = super.fileUpdated(completed) + print("**fileUpdated \(completed)") if completed { let result = self.dataValue.modify { dataValue in if let dataValue = dataValue { diff --git a/submodules/TelegramUI/TelegramUI/ListMessageDateHeader.swift b/submodules/TelegramUI/TelegramUI/ListMessageDateHeader.swift index 406d37c5c9..44f4e1aa1b 100644 --- a/submodules/TelegramUI/TelegramUI/ListMessageDateHeader.swift +++ b/submodules/TelegramUI/TelegramUI/ListMessageDateHeader.swift @@ -3,6 +3,7 @@ import UIKit import Display import AsyncDisplayKit import TelegramPresentationData +import TelegramUIPreferences import TelegramStringFormatting private let timezoneOffset: Int32 = { @@ -13,6 +14,24 @@ private let timezoneOffset: Int32 = { return Int32(timeinfoNow.tm_gmtoff) }() +func listMessageDateHeaderId(timestamp: Int32) -> Int64 { + var time: time_t = time_t(timestamp + timezoneOffset) + var timeinfo: tm = tm() + localtime_r(&time, &timeinfo) + + let roundedTimestamp = timeinfo.tm_year * 100 + timeinfo.tm_mon + + return Int64(roundedTimestamp) +} + +func listMessageDateHeaderInfo(timestamp: Int32) -> (year: Int32, month: Int32) { + var time: time_t = time_t(timestamp + timezoneOffset) + var timeinfo: tm = tm() + localtime_r(&time, &timeinfo) + + return (timeinfo.tm_year, timeinfo.tm_mon) +} + final class ListMessageDateHeader: ListViewItemHeader { private let timestamp: Int32 private let roundedTimestamp: Int32 @@ -22,11 +41,13 @@ final class ListMessageDateHeader: ListViewItemHeader { let id: Int64 let theme: PresentationTheme let strings: PresentationStrings + let fontSize: PresentationFontSize - init(timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings) { + init(timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { self.timestamp = timestamp self.theme = theme self.strings = strings + self.fontSize = fontSize var time: time_t = time_t(timestamp + timezoneOffset) var timeinfo: tm = tm() @@ -44,21 +65,24 @@ final class ListMessageDateHeader: ListViewItemHeader { let height: CGFloat = 36.0 func node() -> ListViewItemHeaderNode { - return ListMessageDateHeaderNode(theme: self.theme, strings: self.strings, roundedTimestamp: self.roundedTimestamp, month: self.month, year: self.year) + return ListMessageDateHeaderNode(theme: self.theme, strings: self.strings, fontSize: self.fontSize, roundedTimestamp: self.roundedTimestamp, month: self.month, year: self.year) + } + + func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) { } } -private let sectionTitleFont = Font.regular(14.0) - final class ListMessageDateHeaderNode: ListViewItemHeaderNode { var theme: PresentationTheme var strings: PresentationStrings + var fontSize: PresentationFontSize let titleNode: ASTextNode let backgroundNode: ASDisplayNode - init(theme: PresentationTheme, strings: PresentationStrings, roundedTimestamp: Int32, month: Int32, year: Int32) { + init(theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, roundedTimestamp: Int32, month: Int32, year: Int32) { self.theme = theme self.strings = strings + self.fontSize = fontSize self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -71,6 +95,8 @@ final class ListMessageDateHeaderNode: ListViewItemHeaderNode { let dateText = stringForMonth(strings: strings, month: month, ofYear: year) + let sectionTitleFont = Font.regular(floor(fontSize.baseDisplaySize * 14.0 / 17.0)) + self.addSubnode(self.backgroundNode) self.addSubnode(self.titleNode) self.titleNode.attributedText = NSAttributedString(string: dateText, font: sectionTitleFont, textColor: theme.list.itemPrimaryTextColor) @@ -93,8 +119,7 @@ final class ListMessageDateHeaderNode: ListViewItemHeaderNode { override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { let titleSize = self.titleNode.measure(CGSize(width: size.width - leftInset - rightInset - 24.0, height: CGFloat.greatestFiniteMagnitude)) - self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + 12.0, y: 8.0), size: titleSize) + self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + 12.0, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize) self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) } } - diff --git a/submodules/TelegramUI/TelegramUI/ListMessageFileItemNode.swift b/submodules/TelegramUI/TelegramUI/ListMessageFileItemNode.swift index 5f8d3516d2..43ce473a32 100644 --- a/submodules/TelegramUI/TelegramUI/ListMessageFileItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ListMessageFileItemNode.swift @@ -15,6 +15,7 @@ import AccountContext import RadialStatusNode import PhotoResources import MusicAlbumArtResources +import UniversalMediaPlayer private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:]) @@ -114,10 +115,6 @@ private func extensionImage(fileExtension: String?) -> UIImage? { return nil } } - -private let titleFont = Font.medium(16.0) -private let audioTitleFont = Font.regular(16.0) -private let descriptionFont = Font.regular(13.0) private let extensionFont = Font.medium(13.0) private struct FetchControls { @@ -128,6 +125,7 @@ private struct FetchControls { private enum FileIconImage: Equatable { case imageRepresentation(TelegramMediaFile, TelegramMediaImageRepresentation) case albumArt(TelegramMediaFile, SharedMediaPlaybackAlbumArt) + case roundVideo(TelegramMediaFile) static func ==(lhs: FileIconImage, rhs: FileIconImage) -> Bool { switch lhs { @@ -143,6 +141,12 @@ private enum FileIconImage: Equatable { } else { return false } + case let .roundVideo(file): + if case .roundVideo(file) = rhs { + return true + } else { + return false + } } } } @@ -163,6 +167,10 @@ final class ListMessageFileItemNode: ListMessageNode { private let statusButtonNode: HighlightTrackingButtonNode private let statusNode: RadialStatusNode + private var waveformNode: AudioWaveformNode? + private var waveformForegroundNode: AudioWaveformNode? + private var waveformScrubbingNode: MediaPlayerScrubbingNode? + private var currentIconImage: FileIconImage? private var currentMedia: Media? @@ -171,6 +179,8 @@ final class ListMessageFileItemNode: ListMessageNode { private var fetchStatus: MediaResourceStatus? private var resourceStatus: FileMediaResourceMediaStatus? private let fetchDisposable = MetaDisposable() + private let playbackStatusDisposable = MetaDisposable() + private let playbackStatus = Promise() private var downloadStatusIconNode: ASImageNode private var linearProgressNode: ASDisplayNode @@ -230,7 +240,7 @@ final class ListMessageFileItemNode: ListMessageNode { self.downloadStatusIconNode.displayWithoutProcessing = true self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: .black, foregroundColor: .white, icon: nil)) - self.progressNode.isLayerBacked = true + //self.progressNode.isLayerBacked = true self.linearProgressNode = ASDisplayNode() self.linearProgressNode.isLayerBacked = true @@ -239,6 +249,7 @@ final class ListMessageFileItemNode: ListMessageNode { self.addSubnode(self.separatorNode) self.addSubnode(self.titleNode) + self.addSubnode(self.progressNode) self.addSubnode(self.descriptionNode) self.addSubnode(self.descriptionProgressNode) self.addSubnode(self.extensionIconNode) @@ -316,6 +327,10 @@ final class ListMessageFileItemNode: ListMessageNode { updatedTheme = item.theme } + let titleFont = Font.medium(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0)) + let audioTitleFont = Font.regular(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0)) + let descriptionFont = Font.regular(floor(item.fontSize.baseDisplaySize * 13.0 / 17.0)) + var leftInset: CGFloat = 65.0 + params.leftInset let rightInset: CGFloat = 8.0 + params.rightInset @@ -335,9 +350,13 @@ final class ListMessageFileItemNode: ListMessageNode { var iconImage: FileIconImage? var updateIconImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? var updatedStatusSignal: Signal? + var updatedPlaybackStatusSignal: Signal? var updatedFetchControls: FetchControls? + var waveform: AudioWaveform? var isAudio = false + var isVoice = false + var isInstantVideo = false let message = item.message @@ -346,9 +365,12 @@ final class ListMessageFileItemNode: ListMessageNode { if let file = media as? TelegramMediaFile { selectedMedia = file + isInstantVideo = file.isInstantVideo + for attribute in file.attributes { - if case let .Audio(voice, _, title, performer, _) = attribute { + if case let .Audio(voice, _, title, performer, waveformValue) = attribute { isAudio = true + isVoice = voice titleText = NSAttributedString(string: title ?? (file.fileName ?? "Unknown Track"), font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor) @@ -365,11 +387,21 @@ final class ListMessageFileItemNode: ListMessageNode { if !voice { iconImage = .albumArt(file, SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: false))) + } else { + titleText = NSAttributedString(string: " ", font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor) + descriptionText = NSAttributedString(string: item.message.author?.displayTitle(strings: item.strings, displayOrder: .firstLast) ?? " ", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor) + waveformValue?.withDataNoCopy { data in + waveform = AudioWaveform(bitstream: data, bitsPerSample: 5) + } } } } - if !isAudio { + if isInstantVideo { + titleText = NSAttributedString(string: item.strings.Message_VideoMessage, font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor) + descriptionText = NSAttributedString(string: item.message.author?.displayTitle(strings: item.strings, displayOrder: .firstLast) ?? " ", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor) + iconImage = .roundVideo(file) + } else if !isAudio { let fileName: String = file.fileName ?? "" titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) @@ -402,7 +434,7 @@ final class ListMessageFileItemNode: ListMessageNode { } } - if isAudio { + if isAudio && !isVoice { leftInset += 14.0 } @@ -435,9 +467,9 @@ final class ListMessageFileItemNode: ListMessageNode { } if statusUpdated { - updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: selectedMedia, message: message, isRecentActions: false) + updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: selectedMedia, message: message, isRecentActions: false, isSharedMedia: true) - if isAudio { + if isAudio || isInstantVideo { if let currentUpdatedStatusSignal = updatedStatusSignal { updatedStatusSignal = currentUpdatedStatusSignal |> map { status in @@ -450,6 +482,9 @@ final class ListMessageFileItemNode: ListMessageNode { } } } + if isVoice { + updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: selectedMedia, message: message, isRecentActions: false) + } } } @@ -472,6 +507,11 @@ final class ListMessageFileItemNode: ListMessageNode { let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0)) let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor) iconImageApply = iconImageLayout(arguments) + case let .roundVideo(file): + let iconSize = CGSize(width: 42.0, height: 42.0) + let imageCorners = ImageCorners(topLeft: .Corner(iconSize.width / 2.0), topRight: .Corner(iconSize.width / 2.0), bottomLeft: .Corner(iconSize.width / 2.0), bottomRight: .Corner(iconSize.width / 2.0)) + let arguments = TransformImageArguments(corners: imageCorners, imageSize: (file.dimensions ?? PixelDimensions(width: 320, height: 320)).cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor) + iconImageApply = iconImageLayout(arguments) } } @@ -482,7 +522,8 @@ final class ListMessageFileItemNode: ListMessageNode { updateIconImageSignal = chatWebpageSnippetFile(account: item.context.account, fileReference: .message(message: MessageReference(message), media: file), representation: representation) case let .albumArt(file, albumArt): updateIconImageSignal = playerAlbumArt(postbox: item.context.account.postbox, fileReference: .message(message: MessageReference(message), media: file), albumArt: albumArt, thumbnail: true) - + case let .roundVideo(file): + updateIconImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, videoReference: FileMediaReference.message(message: MessageReference(message), media: file), autoFetchFullSizeThumbnail: true) } } else { updateIconImageSignal = .complete() @@ -494,7 +535,7 @@ final class ListMessageFileItemNode: ListMessageNode { insets.top += header.height } - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 56.0), insets: insets) + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 8.0 * 2.0 + titleNodeLayout.size.height + 3.0 + descriptionNodeLayout.size.height), insets: insets) return (nodeLayout, { animation in if let strongSelf = self { @@ -562,7 +603,7 @@ final class ListMessageFileItemNode: ListMessageNode { } } - transition.updateFrame(node: strongSelf.descriptionNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + descriptionOffset, y: 32.0), size: descriptionNodeLayout.size)) + transition.updateFrame(node: strongSelf.descriptionNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + descriptionOffset, y: strongSelf.titleNode.frame.maxY + 3.0), size: descriptionNodeLayout.size)) let _ = descriptionNodeApply() let iconFrame: CGRect @@ -581,6 +622,48 @@ final class ListMessageFileItemNode: ListMessageNode { strongSelf.currentIconImage = iconImage + if isVoice { + let waveformNode: AudioWaveformNode + let waveformForegroundNode: AudioWaveformNode + let waveformScrubbingNode: MediaPlayerScrubbingNode + if let current = strongSelf.waveformNode { + waveformNode = current + } else { + waveformNode = AudioWaveformNode() + waveformNode.isLayerBacked = true + strongSelf.waveformNode = waveformNode + strongSelf.addSubnode(waveformNode) + } + if let current = strongSelf.waveformForegroundNode { + waveformForegroundNode = current + } else { + waveformForegroundNode = AudioWaveformNode() + waveformForegroundNode.isLayerBacked = true + strongSelf.waveformForegroundNode = waveformForegroundNode + strongSelf.addSubnode(waveformForegroundNode) + } + if let current = strongSelf.waveformScrubbingNode { + waveformScrubbingNode = current + } else { + waveformScrubbingNode = MediaPlayerScrubbingNode(content: .custom(backgroundNode: waveformNode, foregroundContentNode: waveformForegroundNode)) + waveformScrubbingNode.hitTestSlop = UIEdgeInsets(top: -10.0, left: 0.0, bottom: -10.0, right: 0.0) + waveformScrubbingNode.seek = { timestamp in + if let strongSelf = self, let context = strongSelf.context, let message = strongSelf.message, let type = peerMessageMediaPlayerType(message) { + context.sharedContext.mediaManager.playlistControl(.seek(timestamp), type: type) + } + } + waveformScrubbingNode.enableScrubbing = false + waveformScrubbingNode.status = strongSelf.playbackStatus.get() + strongSelf.waveformScrubbingNode = waveformScrubbingNode + strongSelf.addSubnode(waveformScrubbingNode) + } + + transition.updateFrame(node: waveformScrubbingNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 10.0), size: CGSize(width: params.width - leftInset - 16.0, height: 12.0))) + + waveformNode.setup(color: item.theme.list.controlSecondaryColor, waveform: waveform) + waveformForegroundNode.setup(color: item.theme.list.itemAccentColor, waveform: waveform) + } + if let iconImageApply = iconImageApply { if let updateImageSignal = updateIconImageSignal { strongSelf.iconImageNode.setSignal(updateImageSignal) @@ -630,12 +713,26 @@ final class ListMessageFileItemNode: ListMessageNode { })) } - transition.updateFrame(node: strongSelf.downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 34.0), size: CGSize(width: 11.0, height: 11.0))) + transition.updateFrame(node: strongSelf.downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: strongSelf.descriptionNode.frame.minY + floor((strongSelf.descriptionNode.frame.height - 11.0) / 2.0)), size: CGSize(width: 11.0, height: 11.0))) + + let progressSize: CGFloat = 40.0 + transition.updateFrame(node: strongSelf.progressNode, frame: CGRect(origin: CGPoint(x: leftOffset + params.leftInset + floor((leftInset - params.leftInset - progressSize) / 2.0), y: floor((nodeLayout.contentSize.height - progressSize) / 2.0)), size: CGSize(width: progressSize, height: progressSize))) if let updatedFetchControls = updatedFetchControls { let _ = strongSelf.fetchControls.swap(updatedFetchControls) } + if let updatedPlaybackStatusSignal = updatedPlaybackStatusSignal { + strongSelf.playbackStatus.set(updatedPlaybackStatusSignal) + /*strongSelf.playbackStatusDisposable.set((updatedPlaybackStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in + displayLinkDispatcher.dispatch { + if let strongSelf = strongSelf { + strongSelf.playerStatus = status + } + } + }))*/ + } + strongSelf.updateStatus(transition: transition) } }) @@ -648,25 +745,34 @@ final class ListMessageFileItemNode: ListMessageNode { } var isAudio = false + var isVoice = false + var isInstantVideo = false if let file = media as? TelegramMediaFile { isAudio = file.isMusic || file.isVoice + isVoice = file.isVoice + isInstantVideo = file.isInstantVideo } + self.progressNode.isHidden = !isVoice + + var enableScrubbing = false var musicIsPlaying: Bool? var statusState: RadialStatusNodeState = .none - if !isAudio { + if !isAudio && !isInstantVideo { self.updateProgressFrame(size: contentSize, leftInset: layoutParams.leftInset, rightInset: layoutParams.rightInset, transition: .immediate) } else { - switch fetchStatus { - case let .Fetching(_, progress): - let adjustedProgress = max(progress, 0.027) - statusState = .cloudProgress(color: item.theme.list.itemAccentColor, strokeBackgroundColor: item.theme.list.itemAccentColor.withAlphaComponent(0.5), lineWidth: 2.0, value: CGFloat(adjustedProgress)) - case .Local: - break - case .Remote: - if let image = PresentationResourcesItemList.cloudFetchIcon(item.theme) { - statusState = .customIcon(image) - } + if !isVoice && !isInstantVideo { + switch fetchStatus { + case let .Fetching(_, progress): + let adjustedProgress = max(progress, 0.027) + statusState = .cloudProgress(color: item.theme.list.itemAccentColor, strokeBackgroundColor: item.theme.list.itemAccentColor.withAlphaComponent(0.5), lineWidth: 2.0, value: CGFloat(adjustedProgress)) + case .Local: + break + case .Remote: + if let image = PresentationResourcesItemList.cloudFetchIcon(item.theme) { + statusState = .customIcon(image) + } + } } self.statusNode.transitionToState(statusState, completion: {}) self.statusButtonNode.isUserInteractionEnabled = statusState != .none @@ -691,6 +797,7 @@ final class ListMessageFileItemNode: ListMessageNode { } } case let .playbackStatus(playbackStatus): + enableScrubbing = true switch playbackStatus { case .playing: musicIsPlaying = true @@ -701,7 +808,8 @@ final class ListMessageFileItemNode: ListMessageNode { } } } - if let musicIsPlaying = musicIsPlaying { + self.waveformScrubbingNode?.enableScrubbing = enableScrubbing + if let musicIsPlaying = musicIsPlaying, !isVoice, !isInstantVideo { if self.playbackOverlayNode == nil { let playbackOverlayNode = ListMessagePlaybackOverlayNode() playbackOverlayNode.frame = self.iconImageNode.frame @@ -741,10 +849,10 @@ final class ListMessageFileItemNode: ListMessageNode { } } - override func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + override func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if let item = self.item, item.message.id == id, self.iconImageNode.supernode != nil { let iconImageNode = self.iconImageNode - return (self.iconImageNode, { [weak iconImageNode] in + return (self.iconImageNode, self.iconImageNode.bounds, { [weak iconImageNode] in return (iconImageNode?.view.snapshotContentTree(unhide: true), nil) }) } @@ -840,6 +948,7 @@ final class ListMessageFileItemNode: ListMessageNode { self.descriptionProgressNode.isHidden = true self.descriptionNode.isHidden = false } + let descriptionFont = Font.regular(floor(item.fontSize.baseDisplaySize * 13.0 / 17.0)) self.descriptionProgressNode.attributedText = NSAttributedString(string: downloadingString ?? "", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor) let descriptionSize = self.descriptionProgressNode.updateLayout(CGSize(width: size.width - 14.0, height: size.height)) transition.updateFrame(node: self.descriptionProgressNode, frame: CGRect(origin: self.descriptionNode.frame.origin, size: descriptionSize)) diff --git a/submodules/TelegramUI/TelegramUI/ListMessageItem.swift b/submodules/TelegramUI/TelegramUI/ListMessageItem.swift index fdeebefad1..b3264a7cad 100644 --- a/submodules/TelegramUI/TelegramUI/ListMessageItem.swift +++ b/submodules/TelegramUI/TelegramUI/ListMessageItem.swift @@ -8,10 +8,12 @@ import SwiftSignalKit import Postbox import TelegramPresentationData import AccountContext +import TelegramUIPreferences final class ListMessageItem: ListViewItem { let theme: PresentationTheme let strings: PresentationStrings + let fontSize: PresentationFontSize let dateTimeFormat: PresentationDateTimeFormat let context: AccountContext let chatLocation: ChatLocation @@ -23,16 +25,17 @@ final class ListMessageItem: ListViewItem { let selectable: Bool = true - public init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, context: AccountContext, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, message: Message, selection: ChatHistoryMessageSelection, displayHeader: Bool) { + public init(theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, dateTimeFormat: PresentationDateTimeFormat, context: AccountContext, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, message: Message, selection: ChatHistoryMessageSelection, displayHeader: Bool) { self.theme = theme self.strings = strings + self.fontSize = fontSize self.dateTimeFormat = dateTimeFormat self.context = context self.chatLocation = chatLocation self.controllerInteraction = controllerInteraction self.message = message if displayHeader { - self.header = ListMessageDateHeader(timestamp: message.timestamp, theme: theme, strings: strings) + self.header = ListMessageDateHeader(timestamp: message.timestamp, theme: theme, strings: strings, fontSize: fontSize) } else { self.header = nil } diff --git a/submodules/TelegramUI/TelegramUI/ListMessageNode.swift b/submodules/TelegramUI/TelegramUI/ListMessageNode.swift index ad9ff1b29b..f110ca203e 100644 --- a/submodules/TelegramUI/TelegramUI/ListMessageNode.swift +++ b/submodules/TelegramUI/TelegramUI/ListMessageNode.swift @@ -27,7 +27,7 @@ class ListMessageNode: ListViewItemNode { } } - func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil } diff --git a/submodules/TelegramUI/TelegramUI/ListMessageSnippetItemNode.swift b/submodules/TelegramUI/TelegramUI/ListMessageSnippetItemNode.swift index e3df40f7e7..d5154e082a 100644 --- a/submodules/TelegramUI/TelegramUI/ListMessageSnippetItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ListMessageSnippetItemNode.swift @@ -14,8 +14,6 @@ import PhotoResources import WebsiteType import UrlHandling -private let titleFont = Font.medium(16.0) -private let descriptionFont = Font.regular(14.0) private let iconFont = Font.medium(22.0) private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 2.0, color: UIColor(rgb: 0xdfdfdf)) @@ -157,6 +155,9 @@ final class ListMessageSnippetItemNode: ListMessageNode { updatedTheme = item.theme } + let titleFont = Font.medium(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0)) + let descriptionFont = Font.regular(floor(item.fontSize.baseDisplaySize * 14.0 / 17.0)) + let leftInset: CGFloat = 65.0 + params.leftInset var leftOffset: CGFloat = 0.0 @@ -327,7 +328,7 @@ final class ListMessageSnippetItemNode: ListMessageNode { } } - let contentHeight = 40.0 + descriptionNodeLayout.size.height + linkNodeLayout.size.height + let contentHeight = 9.0 + titleNodeLayout.size.height + 10.0 + descriptionNodeLayout.size.height + linkNodeLayout.size.height var insets = UIEdgeInsets() if dateHeaderAtBottom, let header = item.header { @@ -379,7 +380,7 @@ final class ListMessageSnippetItemNode: ListMessageNode { transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 9.0), size: titleNodeLayout.size)) let _ = titleNodeApply() - let descriptionFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset - 1.0, y: 32.0), size: descriptionNodeLayout.size) + let descriptionFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset - 1.0, y: strongSelf.titleNode.frame.maxY + 3.0), size: descriptionNodeLayout.size) transition.updateFrame(node: strongSelf.descriptionNode, frame: descriptionFrame) let _ = descriptionNodeApply() @@ -466,10 +467,10 @@ final class ListMessageSnippetItemNode: ListMessageNode { } } - override func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + override func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if let item = self.item, item.message.id == id, self.iconImageNode.supernode != nil { let iconImageNode = self.iconImageNode - return (self.iconImageNode, { [weak iconImageNode] in + return (self.iconImageNode, self.iconImageNode.bounds, { [weak iconImageNode] in return (iconImageNode?.view.snapshotContentTree(unhide: true), nil) }) } @@ -500,12 +501,12 @@ final class ListMessageSnippetItemNode: ListMessageNode { } } else { if isTelegramMeLink(content.url) || !item.controllerInteraction.openMessage(item.message, .link) { - item.controllerInteraction.openUrl(currentPrimaryUrl, false, false) + item.controllerInteraction.openUrl(currentPrimaryUrl, false, false, nil) } } } else { if !item.controllerInteraction.openMessage(item.message, .default) { - item.controllerInteraction.openUrl(currentPrimaryUrl, false, false) + item.controllerInteraction.openUrl(currentPrimaryUrl, false, false, nil) } } } @@ -555,10 +556,10 @@ final class ListMessageSnippetItemNode: ListMessageNode { item.controllerInteraction.longTap(ChatControllerInteractionLongTapAction.url(url), item.message) } else if url == self.currentPrimaryUrl { if !item.controllerInteraction.openMessage(item.message, .default) { - item.controllerInteraction.openUrl(url, false, false) + item.controllerInteraction.openUrl(url, false, false, nil) } } else { - item.controllerInteraction.openUrl(url, false, true) + item.controllerInteraction.openUrl(url, false, true, nil) } } case .hold, .doubleTap: diff --git a/submodules/TelegramUI/TelegramUI/MediaManager.swift b/submodules/TelegramUI/TelegramUI/MediaManager.swift index b2378ca932..2db3ce7136 100644 --- a/submodules/TelegramUI/TelegramUI/MediaManager.swift +++ b/submodules/TelegramUI/TelegramUI/MediaManager.swift @@ -8,6 +8,7 @@ import SyncCore import MediaPlayer import TelegramAudio import UniversalMediaPlayer +import TelegramPresentationData import TelegramUIPreferences import AccountContext import TelegramUniversalVideoContent @@ -61,6 +62,7 @@ public final class MediaManagerImpl: NSObject, MediaManager { private let accountManager: AccountManager private let inForeground: Signal + private let presentationData: Signal public let audioSession: ManagedAudioSession public let overlayMediaManager: OverlayMediaManager = OverlayMediaManager() @@ -173,9 +175,10 @@ public final class MediaManagerImpl: NSObject, MediaManager { public let galleryHiddenMediaManager: GalleryHiddenMediaManager = GalleryHiddenMediaManagerImpl() - init(accountManager: AccountManager, inForeground: Signal) { + init(accountManager: AccountManager, inForeground: Signal, presentationData: Signal) { self.accountManager = accountManager self.inForeground = inForeground + self.presentationData = presentationData self.audioSession = sharedAudioSession @@ -205,8 +208,8 @@ public final class MediaManagerImpl: NSObject, MediaManager { var currentGlobalControlsOptions = GlobalControlOptions() - self.globalControlsDisposable.set((self.globalMediaPlayerState - |> deliverOnMainQueue).start(next: { stateAndType in + self.globalControlsDisposable.set((combineLatest(self.globalMediaPlayerState, self.presentationData) + |> deliverOnMainQueue).start(next: { stateAndType, presentationData in var updatedGlobalControlOptions = GlobalControlOptions() if let (_, stateOrLoading, type) = stateAndType, case let .state(state) = stateOrLoading { if type == .music { @@ -234,8 +237,8 @@ public final class MediaManagerImpl: NSObject, MediaManager { case let .music(title, performer, artworkValue, _): artwork = artworkValue - let titleText: String = title ?? "Unknown Track" - let subtitleText: String = performer ?? "Unknown Artist" + let titleText: String = title ?? presentationData.strings.MediaPlayer_UnknownTrack + let subtitleText: String = performer ?? presentationData.strings.MediaPlayer_UnknownArtist nowPlayingInfo[MPMediaItemPropertyTitle] = titleText nowPlayingInfo[MPMediaItemPropertyArtist] = subtitleText @@ -392,11 +395,11 @@ public final class MediaManagerImpl: NSObject, MediaManager { let throttledSignal = self.globalMediaPlayerState |> mapToThrottled { next -> Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> in - return .single(next) |> then(.complete() |> delay(4.0, queue: Queue.concurrentDefaultQueue())) + return .single(next) |> then(.complete() |> delay(2.0, queue: Queue.concurrentDefaultQueue())) } self.mediaPlaybackStateDisposable.set(throttledSignal.start(next: { accountStateAndType in - if let (account, stateOrLoading, type) = accountStateAndType, type == .music, case let .state(state) = stateOrLoading, state.status.duration > 60.0 * 20.0, case .playing = state.status.status { + if let (account, stateOrLoading, type) = accountStateAndType, type == .music, case let .state(state) = stateOrLoading, state.status.duration >= 60.0 * 20.0, case .playing = state.status.status { if let item = state.item as? MessageMediaPlaylistItem { var storedState: MediaPlaybackStoredState? if state.status.timestamp > 5.0 && state.status.timestamp < state.status.duration - 5.0 { diff --git a/submodules/TelegramUI/TelegramUI/MentionChatInputContextPanelNode.swift b/submodules/TelegramUI/TelegramUI/MentionChatInputContextPanelNode.swift index a299957806..284e8bd01c 100644 --- a/submodules/TelegramUI/TelegramUI/MentionChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/MentionChatInputContextPanelNode.swift @@ -6,29 +6,32 @@ import TelegramCore import SyncCore import Display import TelegramPresentationData +import TelegramUIPreferences import MergeLists import TextFormat import AccountContext import LocalizedPeerData +import ItemListUI private struct MentionChatInputContextPanelEntry: Comparable, Identifiable { let index: Int let peer: Peer + let revealed: Bool var stableId: Int64 { return self.peer.id.toInt64() } static func ==(lhs: MentionChatInputContextPanelEntry, rhs: MentionChatInputContextPanelEntry) -> Bool { - return lhs.index == rhs.index && lhs.peer.isEqual(rhs.peer) + return lhs.index == rhs.index && lhs.peer.isEqual(rhs.peer) && lhs.revealed == rhs.revealed } static func <(lhs: MentionChatInputContextPanelEntry, rhs: MentionChatInputContextPanelEntry) -> Bool { return lhs.index < rhs.index } - func item(account: Account, theme: PresentationTheme, inverted: Bool, peerSelected: @escaping (Peer) -> Void) -> ListViewItem { - return MentionChatInputPanelItem(account: account, theme: theme, inverted: inverted, peer: self.peer, peerSelected: peerSelected) + func item(context: AccountContext, presentationData: PresentationData, inverted: Bool, setPeerIdRevealed: @escaping (PeerId?) -> Void, peerSelected: @escaping (Peer) -> Void, removeRequested: @escaping (PeerId) -> Void) -> ListViewItem { + return MentionChatInputPanelItem(context: context, presentationData: ItemListPresentationData(presentationData), inverted: inverted, peer: self.peer, revealed: self.revealed, setPeerIdRevealed: setPeerIdRevealed, peerSelected: peerSelected, removeRequested: removeRequested) } } @@ -38,12 +41,12 @@ private struct CommandChatInputContextPanelTransition { let updates: [ListViewUpdateItem] } -private func preparedTransition(from fromEntries: [MentionChatInputContextPanelEntry], to toEntries: [MentionChatInputContextPanelEntry], account: Account, theme: PresentationTheme, inverted: Bool, forceUpdate: Bool, peerSelected: @escaping (Peer) -> Void) -> CommandChatInputContextPanelTransition { +private func preparedTransition(from fromEntries: [MentionChatInputContextPanelEntry], to toEntries: [MentionChatInputContextPanelEntry], context: AccountContext, presentationData: PresentationData, inverted: Bool, forceUpdate: Bool, setPeerIdRevealed: @escaping (PeerId?) -> Void, peerSelected: @escaping (Peer) -> Void, removeRequested: @escaping (PeerId) -> Void) -> CommandChatInputContextPanelTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, inverted: inverted, peerSelected: peerSelected), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, inverted: inverted, peerSelected: peerSelected), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, inverted: inverted, setPeerIdRevealed: setPeerIdRevealed, peerSelected: peerSelected, removeRequested: removeRequested), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, inverted: inverted, setPeerIdRevealed: setPeerIdRevealed, peerSelected: peerSelected, removeRequested: removeRequested), directionHint: nil) } return CommandChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) } @@ -59,10 +62,13 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { private let listView: ListView private var currentEntries: [MentionChatInputContextPanelEntry]? + private var currentResults: [Peer] = [] + private var revealedPeerId: PeerId? + private var enqueuedTransitions: [(CommandChatInputContextPanelTransition, Bool)] = [] private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)? - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, mode: MentionChatInputContextPanelMode) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, mode: MentionChatInputContextPanelMode) { self.mode = mode self.listView = ListView() @@ -72,7 +78,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { self.listView.limitHitTestToNodes = true self.listView.view.disablesInteractiveTransitionGestureRecognizer = true - super.init(context: context, theme: theme, strings: strings) + super.init(context: context, theme: theme, strings: strings, fontSize: fontSize) self.isOpaque = false self.clipsToBounds = true @@ -85,6 +91,8 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { } func updateResults(_ results: [Peer]) { + self.currentResults = results + var entries: [MentionChatInputContextPanelEntry] = [] var index = 0 var peerIdSet = Set() @@ -94,7 +102,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { continue } peerIdSet.insert(peerId) - entries.append(MentionChatInputContextPanelEntry(index: index, peer: peer)) + entries.append(MentionChatInputContextPanelEntry(index: index, peer: peer, revealed: self.revealedPeerId == peer.id)) index += 1 } self.updateToEntries(entries: entries, forceUpdate: false) @@ -102,7 +110,13 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { private func updateToEntries(entries: [MentionChatInputContextPanelEntry], forceUpdate: Bool) { let firstTime = self.currentEntries == nil - let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, account: self.context.account, theme: self.theme, inverted: self.mode == .search, forceUpdate: forceUpdate, peerSelected: { [weak self] peer in + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, context: self.context, presentationData: presentationData, inverted: self.mode == .search, forceUpdate: forceUpdate, setPeerIdRevealed: { [weak self] peerId in + if let strongSelf = self { + strongSelf.revealedPeerId = peerId + strongSelf.updateResults(strongSelf.currentResults) + } + }, peerSelected: { [weak self] peer in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { switch strongSelf.mode { case .input: @@ -146,6 +160,14 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { interfaceInteraction.beginMessageSearch(.member(peer), "") } } + }, removeRequested: { [weak self] peerId in + if let strongSelf = self { + let _ = removeRecentlyUsedInlineBot(account: strongSelf.context.account, peerId: peerId).start() + + strongSelf.revealedPeerId = nil + strongSelf.currentResults = strongSelf.currentResults.filter { $0.id != peerId } + strongSelf.updateResults(strongSelf.currentResults) + } }) self.currentEntries = entries self.enqueueTransition(transition, firstTime: firstTime) @@ -225,29 +247,8 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: listViewCurve) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve) self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) diff --git a/submodules/TelegramUI/TelegramUI/MentionChatInputPanelItem.swift b/submodules/TelegramUI/TelegramUI/MentionChatInputPanelItem.swift index 7c65cb7dd8..4a6b0b748b 100644 --- a/submodules/TelegramUI/TelegramUI/MentionChatInputPanelItem.swift +++ b/submodules/TelegramUI/TelegramUI/MentionChatInputPanelItem.swift @@ -7,23 +7,32 @@ import SyncCore import SwiftSignalKit import Postbox import TelegramPresentationData +import TelegramUIPreferences import AvatarNode +import AccountContext +import ItemListUI final class MentionChatInputPanelItem: ListViewItem { - fileprivate let account: Account - fileprivate let theme: PresentationTheme + fileprivate let context: AccountContext + fileprivate let presentationData: ItemListPresentationData + fileprivate let revealed: Bool fileprivate let inverted: Bool fileprivate let peer: Peer private let peerSelected: (Peer) -> Void + fileprivate let setPeerIdRevealed: (PeerId?) -> Void + fileprivate let removeRequested: (PeerId) -> Void let selectable: Bool = true - public init(account: Account, theme: PresentationTheme, inverted: Bool, peer: Peer, peerSelected: @escaping (Peer) -> Void) { - self.account = account - self.theme = theme + public init(context: AccountContext, presentationData: ItemListPresentationData, inverted: Bool, peer: Peer, revealed: Bool, setPeerIdRevealed: @escaping (PeerId?) -> Void, peerSelected: @escaping (Peer) -> Void, removeRequested: @escaping (PeerId) -> Void) { + self.context = context + self.presentationData = presentationData self.inverted = inverted self.peer = peer + self.revealed = revealed + self.setPeerIdRevealed = setPeerIdRevealed self.peerSelected = peerSelected + self.removeRequested = removeRequested } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -74,25 +83,36 @@ final class MentionChatInputPanelItem: ListViewItem { } func selected(listView: ListView) { - self.peerSelected(self.peer) + if self.revealed { + self.setPeerIdRevealed(nil) + } else { + self.peerSelected(self.peer) + } } } private let avatarFont = avatarPlaceholderFont(size: 16.0) -private let primaryFont = Font.medium(14.0) -private let secondaryFont = Font.regular(14.0) final class MentionChatInputPanelItemNode: ListViewItemNode { static let itemHeight: CGFloat = 42.0 - - private var item: MentionChatInputPanelItem? - + private let avatarNode: AvatarNode private let textNode: TextNode private let topSeparatorNode: ASDisplayNode private let separatorNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode + private var revealNode: ItemListRevealOptionsNode? + private var revealOptions: [ItemListRevealOption] = [] + private var initialRevealOffset: CGFloat = 0.0 + public private(set) var revealOffset: CGFloat = 0.0 + private var recognizer: ItemListRevealOptionsGestureRecognizer? + private var hapticFeedback: HapticFeedback? + + private var item: MentionChatInputPanelItem? + + private var validLayout: (CGSize, CGFloat, CGFloat)? + init() { self.avatarNode = AvatarNode(font: avatarFont) self.textNode = TextNode() @@ -115,6 +135,15 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { self.addSubnode(self.textNode) } + override func didLoad() { + super.didLoad() + + let recognizer = ItemListRevealOptionsGestureRecognizer(target: self, action: #selector(self.revealGesture(_:))) + self.recognizer = recognizer + recognizer.allowAnyDirection = false + self.view.addGestureRecognizer(recognizer) + } + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? MentionChatInputPanelItem { let doLayout = self.asyncLayout() @@ -132,6 +161,9 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { let previousItem = self.item return { [weak self] item, params, mergedTop, mergedBottom in + let primaryFont = Font.medium(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)) + let secondaryFont = Font.regular(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)) + let leftInset: CGFloat = 55.0 + params.leftInset let rightInset: CGFloat = 10.0 + params.rightInset @@ -141,19 +173,23 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { } let string = NSMutableAttributedString() - string.append(NSAttributedString(string: item.peer.debugDisplayTitle, font: primaryFont, textColor: item.theme.list.itemPrimaryTextColor)) + string.append(NSAttributedString(string: item.peer.debugDisplayTitle, font: primaryFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)) if let addressName = item.peer.addressName, !addressName.isEmpty { - string.append(NSAttributedString(string: " @\(addressName)", font: secondaryFont, textColor: item.theme.list.itemSecondaryTextColor)) + string.append(NSAttributedString(string: " @\(addressName)", font: secondaryFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)) } let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) - return (nodeLayout, { _ in + return (nodeLayout, { animation in if let strongSelf = self { strongSelf.item = item + strongSelf.validLayout = (nodeLayout.contentSize, params.leftInset, params.rightInset) + + let revealOffset = strongSelf.revealOffset + if let updatedInverted = updatedInverted { if updatedInverted { strongSelf.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) @@ -162,12 +198,12 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { } } - strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.backgroundColor = item.theme.list.plainBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor + strongSelf.topSeparatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor + strongSelf.backgroundColor = item.presentationData.theme.list.plainBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor - strongSelf.avatarNode.setPeer(account: item.account, theme: item.theme, peer: item.peer, emptyColor: item.theme.list.mediaPlaceholderColor) + strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor) let _ = textApply() @@ -181,14 +217,33 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: !item.inverted ? (nodeLayout.contentSize.height - UIScreenPixel) : 0.0), size: CGSize(width: params.width - leftInset, height: UIScreenPixel)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel)) + + if let peer = item.peer as? TelegramUser, let _ = peer.botInfo { + strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]) + strongSelf.setRevealOptionsOpened(item.revealed, animated: animation.isAnimated) + } else { + strongSelf.setRevealOptions([]) + strongSelf.setRevealOptionsOpened(false, animated: animation.isAnimated) + } } }) } } + func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + if let (size, leftInset, rightInset) = self.validLayout { + transition.updateFrameAdditive(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: min(offset, 0.0) + 12.0 + leftInset, y: self.avatarNode.frame.minY), size: self.avatarNode.frame.size)) + transition.updateFrameAdditive(node: self.textNode, frame: CGRect(origin: CGPoint(x: min(offset, 0.0) + 55.0 + leftInset, y: self.textNode.frame.minY), size: self.textNode.frame.size)) + } + } + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) + if let revealNode = self.revealNode, self.revealOffset != 0 { + return + } + if highlighted { self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { @@ -211,4 +266,199 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { } } } + + func setRevealOptions(_ options: [ItemListRevealOption]) { + if self.revealOptions == options { + return + } + let previousOptions = self.revealOptions + let wasEmpty = self.revealOptions.isEmpty + self.revealOptions = options + let isEmpty = options.isEmpty + if options.isEmpty { + if let _ = self.revealNode { + self.recognizer?.becomeCancelled() + self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring)) + } + } + if wasEmpty != isEmpty { + self.recognizer?.isEnabled = !isEmpty + } + } + + private func setRevealOptionsOpened(_ value: Bool, animated: Bool) { + if value != !self.revealOffset.isZero { + if !self.revealOffset.isZero { + self.recognizer?.becomeCancelled() + } + let transition: ContainedViewLayoutTransition + if animated { + transition = .animated(duration: 0.3, curve: .spring) + } else { + transition = .immediate + } + if value { + if self.revealNode == nil { + self.setupAndAddRevealNode() + if let revealNode = self.revealNode, revealNode.isNodeLoaded, let _ = self.validLayout { + revealNode.layout() + let revealSize = revealNode.bounds.size + self.updateRevealOffsetInternal(offset: -revealSize.width, transition: transition) + } + } + } else if !self.revealOffset.isZero { + self.updateRevealOffsetInternal(offset: 0.0, transition: transition) + } + } + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if let recognizer = self.recognizer, otherGestureRecognizer == recognizer { + return true + } else { + return false + } + } + + @objc func revealGesture(_ recognizer: ItemListRevealOptionsGestureRecognizer) { + guard let (size, _, _) = self.validLayout else { + return + } + switch recognizer.state { + case .began: + if let revealNode = self.revealNode { + let revealSize = revealNode.bounds.size + let location = recognizer.location(in: self.view) + if location.x > size.width - revealSize.width { + recognizer.becomeCancelled() + } else { + self.initialRevealOffset = self.revealOffset + } + } else { + if self.revealOptions.isEmpty { + recognizer.becomeCancelled() + } + self.initialRevealOffset = self.revealOffset + } + case .changed: + var translation = recognizer.translation(in: self.view) + translation.x += self.initialRevealOffset + if self.revealNode == nil && translation.x.isLess(than: 0.0) { + self.setupAndAddRevealNode() + self.revealOptionsInteractivelyOpened() + } + self.updateRevealOffsetInternal(offset: translation.x, transition: .immediate) + if self.revealNode == nil { + self.revealOptionsInteractivelyClosed() + } + case .ended, .cancelled: + guard let recognizer = self.recognizer else { + break + } + + if let revealNode = self.revealNode { + let velocity = recognizer.velocity(in: self.view) + let revealSize = revealNode.bounds.size + var reveal = false + if abs(velocity.x) < 100.0 { + if self.initialRevealOffset.isZero && self.revealOffset < 0.0 { + reveal = true + } else if self.revealOffset < -revealSize.width { + reveal = true + } else { + reveal = false + } + } else { + if velocity.x < 0.0 { + reveal = true + } else { + reveal = false + } + } + self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring)) + if !reveal { + self.revealOptionsInteractivelyClosed() + } + } + default: + break + } + } + + private func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { + guard let item = self.item else { + return + } + item.removeRequested(item.peer.id) + } + + private func setupAndAddRevealNode() { + if !self.revealOptions.isEmpty { + let revealNode = ItemListRevealOptionsNode(optionSelected: { [weak self] option in + self?.revealOptionSelected(option, animated: false) + }, tapticAction: { [weak self] in + self?.hapticImpact() + }) + revealNode.setOptions(self.revealOptions, isLeft: false) + self.revealNode = revealNode + + if let (size, _, rightInset) = self.validLayout { + var revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height)) + revealSize.width += rightInset + + revealNode.frame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize) + revealNode.updateRevealOffset(offset: 0.0, sideInset: -rightInset, transition: .immediate) + } + + self.addSubnode(revealNode) + } + } + + private func updateRevealOffsetInternal(offset: CGFloat, transition: ContainedViewLayoutTransition) { + self.revealOffset = offset + guard let (size, leftInset, rightInset) = self.validLayout else { + return + } + + if let revealNode = self.revealNode { + let revealSize = revealNode.bounds.size + + let revealFrame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize) + let revealNodeOffset = -max(self.revealOffset, -revealSize.width) + revealNode.updateRevealOffset(offset: revealNodeOffset, sideInset: -rightInset, transition: transition) + + if CGFloat(0.0).isLessThanOrEqualTo(offset) { + self.revealNode = nil + transition.updateFrame(node: revealNode, frame: revealFrame, completion: { [weak revealNode] _ in + revealNode?.removeFromSupernode() + }) + } else { + transition.updateFrame(node: revealNode, frame: revealFrame) + } + } + self.updateRevealOffset(offset: offset, transition: transition) + } + + func revealOptionsInteractivelyOpened() { + if let item = self.item { + item.setPeerIdRevealed(item.peer.id) + } + } + + func revealOptionsInteractivelyClosed() { + if let item = self.item { + item.setPeerIdRevealed(nil) + } + } + + private func hapticImpact() { + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + self.hapticFeedback?.impact(.medium) + } } diff --git a/submodules/TelegramUI/TelegramUI/MultiScaleTextNode.swift b/submodules/TelegramUI/TelegramUI/MultiScaleTextNode.swift new file mode 100644 index 0000000000..5a5ae764e1 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/MultiScaleTextNode.swift @@ -0,0 +1,92 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display + +private final class MultiScaleTextStateNode: ASDisplayNode { + let textNode: ImmediateTextNode + + var currentLayout: MultiScaleTextLayout? + + override init() { + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.textNode) + } +} + +final class MultiScaleTextState { + let attributedText: NSAttributedString + let constrainedSize: CGSize + + init(attributedText: NSAttributedString, constrainedSize: CGSize) { + self.attributedText = attributedText + self.constrainedSize = constrainedSize + } +} + +struct MultiScaleTextLayout { + var size: CGSize +} + +final class MultiScaleTextNode: ASDisplayNode { + private let stateNodes: [AnyHashable: MultiScaleTextStateNode] + + init(stateKeys: [AnyHashable]) { + self.stateNodes = Dictionary(stateKeys.map { ($0, MultiScaleTextStateNode()) }, uniquingKeysWith: { lhs, _ in lhs }) + + super.init() + + for (_, node) in self.stateNodes { + self.addSubnode(node) + } + } + + func stateNode(forKey key: AnyHashable) -> ASDisplayNode? { + return self.stateNodes[key]?.textNode + } + + func updateLayout(states: [AnyHashable: MultiScaleTextState], mainState: AnyHashable) -> [AnyHashable: MultiScaleTextLayout] { + assert(Set(states.keys) == Set(self.stateNodes.keys)) + assert(states[mainState] != nil) + + var result: [AnyHashable: MultiScaleTextLayout] = [:] + var mainLayout: MultiScaleTextLayout? + for (key, state) in states { + if let node = self.stateNodes[key] { + node.textNode.attributedText = state.attributedText + let nodeSize = node.textNode.updateLayout(state.constrainedSize) + let nodeLayout = MultiScaleTextLayout(size: nodeSize) + if key == mainState { + mainLayout = nodeLayout + } + node.currentLayout = nodeLayout + result[key] = nodeLayout + } + } + if let mainLayout = mainLayout { + let mainBounds = CGRect(origin: CGPoint(x: -mainLayout.size.width / 2.0, y: -mainLayout.size.height / 2.0), size: mainLayout.size) + for (key, _) in states { + if let node = self.stateNodes[key], let nodeLayout = result[key] { + node.textNode.frame = CGRect(origin: CGPoint(x: mainBounds.minX, y: mainBounds.minY + floor((mainBounds.height - nodeLayout.size.height) / 2.0)), size: nodeLayout.size) + } + } + } + return result + } + + func update(stateFractions: [AnyHashable: CGFloat], transition: ContainedViewLayoutTransition) { + var fractionSum: CGFloat = 0.0 + for (_, fraction) in stateFractions { + fractionSum += fraction + } + for (key, fraction) in stateFractions { + if let node = self.stateNodes[key], let nodeLayout = node.currentLayout { + transition.updateAlpha(node: node, alpha: fraction / fractionSum) + } + } + } +} diff --git a/submodules/TelegramUI/TelegramUI/NavigateToChatController.swift b/submodules/TelegramUI/TelegramUI/NavigateToChatController.swift index 5b05789afd..9d7591e00c 100644 --- a/submodules/TelegramUI/TelegramUI/NavigateToChatController.swift +++ b/submodules/TelegramUI/TelegramUI/NavigateToChatController.swift @@ -33,6 +33,10 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam controller.scrollToEndOfHistory() let _ = params.navigationController.popToViewController(controller, animated: params.animated) params.completion() + } else if params.activateMessageSearch { + controller.activateSearch() + let _ = params.navigationController.popToViewController(controller, animated: params.animated) + params.completion() } else { let _ = params.navigationController.popToViewController(controller, animated: params.animated) params.completion() @@ -65,6 +69,9 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam controller = ChatControllerImpl(context: params.context, chatLocation: params.chatLocation, subject: params.subject, botStart: params.botStart) } controller.purposefulAction = params.purposefulAction + if params.activateMessageSearch { + controller.activateSearch() + } let resolvedKeepStack: Bool switch params.keepStack { case .default: diff --git a/submodules/TelegramUI/TelegramUI/NotificationContainerController.swift b/submodules/TelegramUI/TelegramUI/NotificationContainerController.swift index 0bbadaa6b2..1207c04486 100644 --- a/submodules/TelegramUI/TelegramUI/NotificationContainerController.swift +++ b/submodules/TelegramUI/TelegramUI/NotificationContainerController.swift @@ -101,4 +101,15 @@ public final class NotificationContainerController: ViewController { public func removeItems(_ f: (NotificationItem) -> Bool) { self.controllerNode.removeItems(f) } + + public func updateIsTemporaryHidden(_ value: Bool) { + if self.isNodeLoaded { + if value != (self.controllerNode.alpha == 0.0) { + let fromAlpha: CGFloat = value ? 1.0 : 0.0 + let toAlpha: CGFloat = value ? 0.0 : 1.0 + self.controllerNode.alpha = toAlpha + self.controllerNode.layer.animateAlpha(from: fromAlpha, to: toAlpha, duration: 0.2) + } + } + } } diff --git a/submodules/TelegramUI/TelegramUI/NotificationContentContext.swift b/submodules/TelegramUI/TelegramUI/NotificationContentContext.swift index 9182ea9df4..4950f319d8 100644 --- a/submodules/TelegramUI/TelegramUI/NotificationContentContext.swift +++ b/submodules/TelegramUI/TelegramUI/NotificationContentContext.swift @@ -55,14 +55,16 @@ private func parseFileLocationResource(_ dict: [AnyHashable: Any]) -> TelegramMe public struct NotificationViewControllerInitializationData { public let appGroupPath: String public let apiId: Int32 + public let apiHash: String public let languagesCategory: String public let encryptionParameters: (Data, Data) public let appVersion: String public let bundleData: Data? - public init(appGroupPath: String, apiId: Int32, languagesCategory: String, encryptionParameters: (Data, Data), appVersion: String, bundleData: Data?) { + public init(appGroupPath: String, apiId: Int32, apiHash: String, languagesCategory: String, encryptionParameters: (Data, Data), appVersion: String, bundleData: Data?) { self.appGroupPath = appGroupPath self.apiId = apiId + self.apiHash = apiHash self.languagesCategory = languagesCategory self.encryptionParameters = encryptionParameters self.appVersion = appVersion @@ -148,11 +150,11 @@ public final class NotificationViewControllerImpl { let presentationDataPromise = Promise() - let appLockContext = AppLockContextImpl(rootPath: rootPath, window: nil, applicationBindings: applicationBindings, accountManager: accountManager, presentationDataSignal: presentationDataPromise.get(), lockIconInitialFrame: { + let appLockContext = AppLockContextImpl(rootPath: rootPath, window: nil, rootController: nil, applicationBindings: applicationBindings, accountManager: accountManager, presentationDataSignal: presentationDataPromise.get(), lockIconInitialFrame: { return nil }) - sharedAccountContext = SharedAccountContextImpl(mainWindow: nil, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()), rootPath: rootPath, legacyBasePath: nil, legacyCache: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }) + sharedAccountContext = SharedAccountContextImpl(mainWindow: nil, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()), rootPath: rootPath, legacyBasePath: nil, legacyCache: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }) presentationDataPromise.set(sharedAccountContext!.presentationData) } diff --git a/submodules/TelegramUI/TelegramUI/OpenAddContact.swift b/submodules/TelegramUI/TelegramUI/OpenAddContact.swift index ee8e6c9ce1..dec7f34047 100644 --- a/submodules/TelegramUI/TelegramUI/OpenAddContact.swift +++ b/submodules/TelegramUI/TelegramUI/OpenAddContact.swift @@ -15,10 +15,10 @@ func openAddContactImpl(context: AccountContext, firstName: String = "", lastNam |> deliverOnMainQueue).start(next: { value in switch value { case .allowed: - let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: firstName, lastName: lastName, phoneNumbers: [DeviceContactPhoneNumberData(label: label, value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: []) + let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: firstName, lastName: lastName, phoneNumbers: [DeviceContactPhoneNumberData(label: label, value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") present(deviceContactInfoController(context: context, subject: .create(peer: nil, contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in if let peer = peer { - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { pushController(infoController) } } else { diff --git a/submodules/TelegramUI/TelegramUI/OpenChatMessage.swift b/submodules/TelegramUI/TelegramUI/OpenChatMessage.swift index 609b9479f3..268183aae1 100644 --- a/submodules/TelegramUI/TelegramUI/OpenChatMessage.swift +++ b/submodules/TelegramUI/TelegramUI/OpenChatMessage.swift @@ -20,6 +20,7 @@ import SettingsUI import AlertUI import PresentationDataUtils import ShareController +import UndoUI private enum ChatMessageGalleryControllerData { case url(String) @@ -45,7 +46,7 @@ private func chatMessageGalleryControllerData(context: AccountContext, message: switch action.action { case let .photoUpdated(image): if let peer = messageMainPeer(message), let image = image { - let promise: Promise<[AvatarGalleryEntry]> = Promise([AvatarGalleryEntry.image(image.reference, image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), peer, message.timestamp, nil, message.id)]) + let promise: Promise<[AvatarGalleryEntry]> = Promise([AvatarGalleryEntry.image(image.imageId, image.reference, image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), peer, message.timestamp, nil, message.id)]) let galleryController = AvatarGalleryController(context: context, peer: peer, remoteEntries: promise, replaceRootController: { controller, ready in }) @@ -270,7 +271,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { params.dismissInput() params.present(gallery, InstantPageGalleryControllerPresentationArguments(transitionArguments: { entry in - var selectedTransitionNode: (ASDisplayNode, () -> (UIView?, UIView?))? + var selectedTransitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? if entry.index == centralIndex { selectedTransitionNode = params.transitionNode(params.message.id, galleryMedia) } @@ -283,6 +284,15 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { case let .map(mapMedia): params.dismissInput() +// let controllerParams = LocationViewParams(sendLiveLocation: { location in +// let outMessage: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: nil, localGroupingKey: nil) +// params.enqueueMessage(outMessage) +// }, stopLiveLocation: { +// params.context.liveLocationManager?.cancelLiveLocation(peerId: params.message.id.peerId) +// }, openUrl: params.openUrl, openPeer: { peer in +// params.openPeer(peer, .info) +// }) +// let controller = LocationViewController(context: params.context, mapMedia: mapMedia, params: controllerParams) let controller = legacyLocationController(message: params.message, mapMedia: mapMedia, context: params.context, openPeer: { peer in params.openPeer(peer, .info) }, sendLiveLocation: { coordinate, period in @@ -295,12 +305,36 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { params.navigationController?.pushViewController(controller) return true case let .stickerPack(reference): - let controller = StickerPackPreviewController(context: params.context, stickerPack: reference, parentNavigationController: params.navigationController) - controller.sendSticker = params.sendSticker + let controller = StickerPackScreen(context: params.context, mainStickerPack: reference, stickerPacks: [reference], parentNavigationController: params.navigationController, sendSticker: params.sendSticker, actionPerformed: { info, items, action in + let presentationData = params.context.sharedContext.currentPresentationData.with { $0 } + var animateInAsReplacement = false + if let navigationController = params.navigationController { + for controller in navigationController.overlayControllers { + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitActionAndReplacementAnimation() + animateInAsReplacement = true + } + } + } + switch action { + case .add: + params.navigationController?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).0, undo: false, info: info, topItem: items.first, account: params.context.account), elevatedLayout: true, animateInAsReplacement: animateInAsReplacement, action: { _ in + return true + })) + case let .remove(positionInList): + params.navigationController?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_RemovedTitle, text: presentationData.strings.StickerPackActionInfo_RemovedText(info.title).0, undo: true, info: info, topItem: items.first, account: params.context.account), elevatedLayout: true, animateInAsReplacement: animateInAsReplacement, action: { action in + if case .undo = action { + let _ = addStickerPackInteractively(postbox: params.context.account.postbox, info: info, items: items, positionInList: positionInList).start() + } + return true + })) + } + }) params.dismissInput() params.present(controller, nil) return true case let .document(file, immediateShare): + params.dismissInput() let presentationData = params.context.sharedContext.currentPresentationData.with { $0 } if immediateShare { let controller = ShareController(context: params.context, subject: .media(.standalone(media: file)), immediateExternalShare: true) @@ -377,7 +411,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { if let vCard = contact.vCardData, let vCardData = vCard.data(using: .utf8), let parsed = DeviceContactExtendedData(vcard: vCardData) { contactData = parsed } else { - contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName, lastName: contact.lastName, phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: contact.phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: []) + contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName, lastName: contact.lastName, phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: contact.phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") } let controller = deviceContactInfoController(context: params.context, subject: .vcard(peer, nil, contactData), completed: nil, cancelled: nil) params.navigationController?.pushViewController(controller) @@ -478,10 +512,12 @@ func openChatWallpaper(context: AccountContext, message: Message, present: @esca if case let .wallpaper(parameter) = resolvedUrl { let source: WallpaperListSource switch parameter { - case let .slug(slug, options, color, intensity): - source = .slug(slug, content.file, options, color, intensity, message) + case let .slug(slug, options, firstColor, secondColor, intensity, rotation): + source = .slug(slug, content.file, options, firstColor, secondColor, intensity, rotation, message) case let .color(color): - source = .wallpaper(.color(Int32(color.rgb)), nil, nil, nil, message) + source = .wallpaper(.color(color.argb), nil, nil, nil, nil, nil, message) + case let .gradient(topColor, bottomColor, rotation): + source = .wallpaper(.gradient(topColor.argb, bottomColor.argb, WallpaperSettings(rotation: rotation)), nil, nil, nil, nil, rotation, message) } let controller = WallpaperGalleryController(context: context, source: source) @@ -498,20 +534,39 @@ func openChatTheme(context: AccountContext, message: Message, pushController: @e let _ = (context.sharedContext.resolveUrl(account: context.account, url: content.url) |> deliverOnMainQueue).start(next: { resolvedUrl in var file: TelegramMediaFile? - let mimeType = "application/x-tgtheme-ios" - if let contentFiles = content.files, let filteredFile = contentFiles.filter({ $0.mimeType == mimeType }).first { - file = filteredFile - } else if let contentFile = content.file, contentFile.mimeType == mimeType { + var settings: TelegramThemeSettings? + let themeMimeType = "application/x-tgtheme-ios" + + for attribute in content.attributes { + if case let .theme(attribute) = attribute { + if let attributeSettings = attribute.settings { + settings = attributeSettings + } else if let filteredFile = attribute.files.filter({ $0.mimeType == themeMimeType }).first { + file = filteredFile + } + } + } + + if file == nil && settings == nil, let contentFile = content.file, contentFile.mimeType == themeMimeType { file = contentFile } let displayUnsupportedAlert: () -> Void = { let presentationData = context.sharedContext.currentPresentationData.with { $0 } present(textAlertController(context: context, title: nil, text: presentationData.strings.Theme_Unsupported, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } - if case let .theme(slug) = resolvedUrl, let file = file { - if let path = context.sharedContext.accountManager.mediaBox.completedResourcePath(file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - if let theme = makePresentationTheme(data: data) { - let controller = ThemePreviewController(context: context, previewTheme: theme, source: .slug(slug, file)) + if case let .theme(slug) = resolvedUrl { + if let file = file { + if let path = context.sharedContext.accountManager.mediaBox.completedResourcePath(file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + if let theme = makePresentationTheme(data: data) { + let controller = ThemePreviewController(context: context, previewTheme: theme, source: .slug(slug, file)) + pushController(controller) + } else { + displayUnsupportedAlert() + } + } + } else if let settings = settings { + if let theme = makePresentationTheme(settings: settings, title: content.title) { + let controller = ThemePreviewController(context: context, previewTheme: theme, source: .themeSettings(slug, settings)) pushController(controller) } else { displayUnsupportedAlert() diff --git a/submodules/TelegramUI/TelegramUI/OpenResolvedUrl.swift b/submodules/TelegramUI/TelegramUI/OpenResolvedUrl.swift index 2ce3de8038..3ac3a2bf4f 100644 --- a/submodules/TelegramUI/TelegramUI/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/TelegramUI/OpenResolvedUrl.swift @@ -17,6 +17,7 @@ import StickerPackPreviewUI import JoinLinkPreviewUI import LanguageLinkPreviewUI import SettingsUI +import UrlHandling private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { if case .default = navigation { @@ -34,7 +35,7 @@ private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatContr } } -func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void) { +func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } switch resolvedUrl { case let .externalUrl(url): @@ -46,7 +47,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur present(textAlertController(context: context, title: nil, text: presentationData.strings.Resolve_ErrorNotFound, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } case .inaccessiblePeer: - present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.Conversation_ErrorInaccessibleMessage, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Conversation_ErrorInaccessibleMessage, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) case let .botStart(peerId, payload): openPeer(peerId, .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive))) case let .groupBotStart(botPeerId, payload): @@ -88,16 +89,51 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur openPeer(peerId, .chat(textInputState: nil, subject: .message(messageId))) case let .stickerPack(name): dismissInput() - let controller = StickerPackPreviewController(context: context, stickerPack: .name(name), parentNavigationController: navigationController) - controller.sendSticker = sendSticker - present(controller, nil) + if false { + var mainStickerPack: StickerPackReference? + var stickerPacks: [StickerPackReference] = [] + if let message = contentContext as? Message { + let dataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link]).rawValue) + if let matches = dataDetector?.matches(in: message.text, options: [], range: NSRange(message.text.startIndex ..< message.text.endIndex, in: message.text)) { + for match in matches { + guard let stringRange = Range(match.range, in: message.text) else { + continue + } + let urlText = String(message.text[stringRange]) + if let resultName = parseStickerPackUrl(urlText) { + stickerPacks.append(.name(resultName)) + if resultName == name { + mainStickerPack = .name(resultName) + } + } + } + if mainStickerPack == nil { + mainStickerPack = .name(name) + stickerPacks.insert(.name(name), at: 0) + } + } else { + mainStickerPack = .name(name) + stickerPacks = [.name(name)] + } + } else { + mainStickerPack = .name(name) + stickerPacks = [.name(name)] + } + if let mainStickerPack = mainStickerPack, !stickerPacks.isEmpty { + let controller = StickerPackScreen(context: context, mainStickerPack: mainStickerPack, stickerPacks: stickerPacks, sendSticker: sendSticker) + present(controller, nil) + } + } else { + let controller = StickerPackScreen(context: context, mainStickerPack: .name(name), stickerPacks: [.name(name)], parentNavigationController: navigationController, sendSticker: sendSticker) + present(controller, nil) + } case let .instantView(webpage, anchor): navigationController?.pushViewController(InstantPageController(context: context, webPage: webpage, sourcePeerType: .channel, anchor: anchor)) case let .join(link): dismissInput() present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peerId in openPeer(peerId, .chat(textInputState: nil, subject: nil)) - }), nil) + }, parentNavigationController: navigationController), nil) case let .localization(identifier): dismissInput() present(LanguageLinkPreviewController(context: context, identifier: identifier), nil) @@ -213,24 +249,30 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur let signal: Signal var options: WallpaperPresentationOptions? - var color: UIColor? + var topColor: UIColor? + var bottomColor: UIColor? var intensity: Int32? + var rotation: Int32? switch parameter { - case let .slug(slug, wallpaperOptions, patternColor, patternIntensity): - signal = getWallpaper(account: context.account, slug: slug) + case let .slug(slug, wallpaperOptions, firstColor, secondColor, intensityValue, rotationValue): + signal = getWallpaper(network: context.account.network, slug: slug) options = wallpaperOptions - color = patternColor - intensity = patternIntensity + topColor = firstColor + bottomColor = secondColor + intensity = intensityValue + rotation = rotationValue controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) present(controller!, nil) case let .color(color): - signal = .single(.color(Int32(color.rgb))) + signal = .single(.color(color.argb)) + case let .gradient(topColor, bottomColor, rotation): + signal = .single(.gradient(topColor.argb, bottomColor.argb, WallpaperSettings())) } let _ = (signal |> deliverOnMainQueue).start(next: { [weak controller] wallpaper in controller?.dismiss() - let galleryController = WallpaperGalleryController(context: context, source: .wallpaper(wallpaper, options, color, intensity, nil)) + let galleryController = WallpaperGalleryController(context: context, source: .wallpaper(wallpaper, options, topColor, bottomColor, intensity, rotation, nil)) present(galleryController, nil) }, error: { [weak controller] error in controller?.dismiss() @@ -239,12 +281,13 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur case let .theme(slug): let presentationData = context.sharedContext.currentPresentationData.with { $0 } let signal = getTheme(account: context.account, slug: slug) - |> mapToSignal { themeInfo -> Signal<(Data, TelegramTheme), GetThemeError> in - return Signal<(Data, TelegramTheme), GetThemeError> { subscriber in + |> mapToSignal { themeInfo -> Signal<(Data?, TelegramThemeSettings?, TelegramTheme), GetThemeError> in + return Signal<(Data?, TelegramThemeSettings?, TelegramTheme), GetThemeError> { subscriber in let disposables = DisposableSet() - let resource = themeInfo.file?.resource - - if let resource = resource { + if let settings = themeInfo.settings { + subscriber.putNext((nil, settings, themeInfo)) + subscriber.putCompletion() + } else if let resource = themeInfo.file?.resource { disposables.add(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: resource)).start()) let maybeFetched = context.sharedContext.accountManager.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: false) @@ -267,7 +310,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur disposables.add(maybeFetched.start(next: { data in if let data = data { - subscriber.putNext((data, themeInfo)) + subscriber.putNext((data, nil, themeInfo)) subscriber.putCompletion() } })) @@ -278,8 +321,6 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur return disposables } } - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) - present(controller, nil) var cancelImpl: (() -> Void)? let progressSignal = Signal { subscriber in @@ -307,14 +348,19 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur progressDisposable.dispose() } } - |> deliverOnMainQueue).start(next: { [weak controller] dataAndTheme in - controller?.dismiss() - - if let theme = makePresentationTheme(data: dataAndTheme.0) { - let previewController = ThemePreviewController(context: context, previewTheme: theme, source: .theme(dataAndTheme.1)) - navigationController?.pushViewController(previewController) + |> deliverOnMainQueue).start(next: { dataAndTheme in + if let data = dataAndTheme.0 { + if let theme = makePresentationTheme(data: data) { + let previewController = ThemePreviewController(context: context, previewTheme: theme, source: .theme(dataAndTheme.2)) + navigationController?.pushViewController(previewController) + } + } else if let settings = dataAndTheme.1 { + if let theme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: .builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)), accentColor: UIColor(argb: settings.accentColor), backgroundColors: nil, bubbleColors: settings.messageColors.flatMap { (UIColor(argb: $0.top), UIColor(argb: $0.bottom)) }, wallpaper: settings.wallpaper) { + let previewController = ThemePreviewController(context: context, previewTheme: theme, source: .theme(dataAndTheme.2)) + navigationController?.pushViewController(previewController) + } } - }, error: { [weak controller] error in + }, error: { error in let errorText: String switch error { case .generic, .slugInvalid: @@ -323,13 +369,60 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur errorText = presentationData.strings.Theme_Unsupported } present(textAlertController(context: context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) - controller?.dismiss() })) dismissInput() + #if ENABLE_WALLET case let .wallet(address, amount, comment): dismissInput() context.sharedContext.openWallet(context: context, walletContext: .send(address: address, amount: amount, comment: comment)) { c in navigationController?.pushViewController(c) } + #endif + case let .settings(section): + dismissInput() + switch section { + case .theme: + if let navigationController = navigationController { + let controller = themeSettingsController(context: context) + controller.navigationPresentation = .modal + + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is ThemeSettingsController) } + controllers.append(controller) + + navigationController.setViewControllers(controllers, animated: true) + } + case .devices: + if let navigationController = navigationController { + let activeSessions = deferred { () -> 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 _ = (activeSessions + |> take(1) + |> deliverOnMainQueue).start(next: { activeSessionsContext, count, webSessionsContext in + let controller = recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, websitesOnly: false) + controller.navigationPresentation = .modal + + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is RecentSessionsController) } + controllers.append(controller) + + navigationController.setViewControllers(controllers, animated: true) + }) + } + break + } } } diff --git a/submodules/TelegramUI/TelegramUI/OpenUrl.swift b/submodules/TelegramUI/TelegramUI/OpenUrl.swift index dd4ebfc627..36d0b974bc 100644 --- a/submodules/TelegramUI/TelegramUI/OpenUrl.swift +++ b/submodules/TelegramUI/TelegramUI/OpenUrl.swift @@ -16,8 +16,10 @@ import AccountContext import UrlEscaping import PassportUI import UrlHandling +#if ENABLE_WALLET import WalletUI import WalletUrl +#endif import OpenInExternalAppUI public struct ParsedSecureIdUrl { @@ -144,6 +146,7 @@ func formattedConfirmationCode(_ code: Int) -> String { } func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void) { + #if ENABLE_WALLET if url.hasPrefix("ton://") { if let url = URL(string: url), let parsedUrl = parseWalletUrl(url) { context.sharedContext.openWallet(context: context, walletContext: .send(address: parsedUrl.address, amount: parsedUrl.amount, comment: parsedUrl.comment)) { c in @@ -153,6 +156,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur return } + #endif if forceExternal || url.lowercased().hasPrefix("tel:") || url.lowercased().hasPrefix("calshow:") { context.sharedContext.applicationBindings.openUrl(url) @@ -200,7 +204,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } let continueHandling: () -> Void = { - let handleRevolvedUrl: (ResolvedUrl) -> Void = { resolved in + let handleResolvedUrl: (ResolvedUrl) -> Void = { resolved in if case let .externalUrl(value) = resolved { context.sharedContext.applicationBindings.openUrl(value) } else { @@ -209,7 +213,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur case .info: let _ = (context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { peer in - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { context.sharedContext.applicationBindings.dismissNativeController() navigationController?.pushViewController(infoController) } @@ -237,399 +241,441 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur context.sharedContext.applicationBindings.getWindowHost()?.present(c, on: .root, blockInteraction: false, completion: {}) }, dismissInput: { dismissInput() - }) + }, contentContext: nil) } } let handleInternalUrl: (String) -> Void = { url in let _ = (context.sharedContext.resolveUrl(account: context.account, url: url) - |> deliverOnMainQueue).start(next: handleRevolvedUrl) + |> deliverOnMainQueue).start(next: handleResolvedUrl) } - if let scheme = parsedUrl.scheme, (scheme == "tg" || scheme == context.sharedContext.applicationBindings.appSpecificScheme), let query = parsedUrl.query { + if let scheme = parsedUrl.scheme, (scheme == "tg" || scheme == context.sharedContext.applicationBindings.appSpecificScheme) { var convertedUrl: String? - if parsedUrl.host == "localpeer" { - if let components = URLComponents(string: "/?" + query) { - var peerId: PeerId? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "id", let intValue = Int64(value) { - peerId = PeerId(intValue) - } - } - } - } - if let peerId = peerId, let navigationController = navigationController { - context.sharedContext.applicationBindings.dismissNativeController() - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId))) - } - } - } else if parsedUrl.host == "join" { - if let components = URLComponents(string: "/?" + query) { - var invite: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "invite" { - invite = value - } - } - } - } - if let invite = invite { - convertedUrl = "https://t.me/joinchat/\(invite)" - } - } - } else if parsedUrl.host == "addstickers" { - if let components = URLComponents(string: "/?" + query) { - var set: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "set" { - set = value - } - } - } - } - if let set = set { - convertedUrl = "https://t.me/addstickers/\(set)" - } - } - } else if parsedUrl.host == "setlanguage" { - if let components = URLComponents(string: "/?" + query) { - var lang: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "lang" { - lang = value - } - } - } - } - if let lang = lang { - convertedUrl = "https://t.me/setlanguage/\(lang)" - } - } - } else if parsedUrl.host == "msg" { - if let components = URLComponents(string: "/?" + query) { - var sharePhoneNumber: String? - var shareText: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "to" { - sharePhoneNumber = value - } else if queryItem.name == "text" { - shareText = value - } - } - } - } - if sharePhoneNumber != nil || shareText != nil { - handleRevolvedUrl(.share(url: nil, text: shareText, to: sharePhoneNumber)) - return - } - } - } else if parsedUrl.host == "msg_url" { - if let components = URLComponents(string: "/?" + query) { - var shareUrl: String? - var shareText: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "url" { - shareUrl = value - } else if queryItem.name == "text" { - shareText = value - } - } - } - } - if let shareUrl = shareUrl { - var resultUrl = "https://t.me/share/url?url=\(urlEncodedStringFromString(shareUrl))" - if let shareText = shareText { - resultUrl += "&text=\(urlEncodedStringFromString(shareText))" - } - convertedUrl = resultUrl - } - } - } else if parsedUrl.host == "socks" || parsedUrl.host == "proxy" { - if let components = URLComponents(string: "/?" + query) { - var server: String? - var port: String? - var user: String? - var pass: String? - var secret: String? - var secretHost: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "server" || queryItem.name == "proxy" { - server = value - } else if queryItem.name == "port" { - port = value - } else if queryItem.name == "user" { - user = value - } else if queryItem.name == "pass" { - pass = value - } else if queryItem.name == "secret" { - secret = value - } else if queryItem.name == "host" { - secretHost = value - } - } - } - } - - if let server = server, !server.isEmpty, let port = port, let _ = Int32(port) { - var result = "https://t.me/proxy?proxy=\(server)&port=\(port)" - if let user = user { - result += "&user=\((user as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" - if let pass = pass { - result += "&pass=\((pass as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" - } - } - if let secret = secret { - result += "&secret=\((secret as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" - } - if let secretHost = secretHost?.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) { - result += "&host=\(secretHost)" - } - convertedUrl = result - } - } - } else if parsedUrl.host == "passport" || parsedUrl.host == "resolve" { - if let components = URLComponents(string: "/?" + query) { - var domain: String? - var botId: Int32? - var scope: String? - var publicKey: String? - var callbackUrl: String? - var opaquePayload = Data() - var opaqueNonce = Data() - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "domain" { - domain = value - } else if queryItem.name == "bot_id" { - botId = Int32(value) - } else if queryItem.name == "scope" { - scope = value - } else if queryItem.name == "public_key" { - publicKey = value - } else if queryItem.name == "callback_url" { - callbackUrl = value - } else if queryItem.name == "payload" { - if let data = value.data(using: .utf8) { - opaquePayload = data - } - } else if queryItem.name == "nonce" { - if let data = value.data(using: .utf8) { - opaqueNonce = data + if let query = parsedUrl.query { + if parsedUrl.host == "localpeer" { + if let components = URLComponents(string: "/?" + query) { + var peerId: PeerId? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "id", let intValue = Int64(value) { + peerId = PeerId(intValue) } } } } + if let peerId = peerId, let navigationController = navigationController { + context.sharedContext.applicationBindings.dismissNativeController() + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId))) + } } - - let valid: Bool - if parsedUrl.host == "resolve" { - if domain == "telegrampassport" { - valid = true + } else if parsedUrl.host == "join" { + if let components = URLComponents(string: "/?" + query) { + var invite: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "invite" { + invite = value + } + } + } + } + if let invite = invite { + convertedUrl = "https://t.me/joinchat/\(invite)" + } + } + } else if parsedUrl.host == "addstickers" { + if let components = URLComponents(string: "/?" + query) { + var set: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "set" { + set = value + } + } + } + } + if let set = set { + convertedUrl = "https://t.me/addstickers/\(set)" + } + } + } else if parsedUrl.host == "setlanguage" { + if let components = URLComponents(string: "/?" + query) { + var lang: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "lang" { + lang = value + } + } + } + } + if let lang = lang { + convertedUrl = "https://t.me/setlanguage/\(lang)" + } + } + } else if parsedUrl.host == "msg" { + if let components = URLComponents(string: "/?" + query) { + var sharePhoneNumber: String? + var shareText: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "to" { + sharePhoneNumber = value + } else if queryItem.name == "text" { + shareText = value + } + } + } + } + if sharePhoneNumber != nil || shareText != nil { + handleResolvedUrl(.share(url: nil, text: shareText, to: sharePhoneNumber)) + return + } + } + } else if parsedUrl.host == "msg_url" { + if let components = URLComponents(string: "/?" + query) { + var shareUrl: String? + var shareText: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "url" { + shareUrl = value + } else if queryItem.name == "text" { + shareText = value + } + } + } + } + if let shareUrl = shareUrl { + var resultUrl = "https://t.me/share/url?url=\(urlEncodedStringFromString(shareUrl))" + if let shareText = shareText { + resultUrl += "&text=\(urlEncodedStringFromString(shareText))" + } + convertedUrl = resultUrl + } + } + } else if parsedUrl.host == "socks" || parsedUrl.host == "proxy" { + if let components = URLComponents(string: "/?" + query) { + var server: String? + var port: String? + var user: String? + var pass: String? + var secret: String? + var secretHost: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "server" || queryItem.name == "proxy" { + server = value + } else if queryItem.name == "port" { + port = value + } else if queryItem.name == "user" { + user = value + } else if queryItem.name == "pass" { + pass = value + } else if queryItem.name == "secret" { + secret = value + } else if queryItem.name == "host" { + secretHost = value + } + } + } + } + + if let server = server, !server.isEmpty, let port = port, let _ = Int32(port) { + var result = "https://t.me/proxy?proxy=\(server)&port=\(port)" + if let user = user { + result += "&user=\((user as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" + if let pass = pass { + result += "&pass=\((pass as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" + } + } + if let secret = secret { + result += "&secret=\((secret as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" + } + if let secretHost = secretHost?.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) { + result += "&host=\(secretHost)" + } + convertedUrl = result + } + } + } else if parsedUrl.host == "passport" || parsedUrl.host == "resolve" { + if let components = URLComponents(string: "/?" + query) { + var domain: String? + var botId: Int32? + var scope: String? + var publicKey: String? + var callbackUrl: String? + var opaquePayload = Data() + var opaqueNonce = Data() + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "domain" { + domain = value + } else if queryItem.name == "bot_id" { + botId = Int32(value) + } else if queryItem.name == "scope" { + scope = value + } else if queryItem.name == "public_key" { + publicKey = value + } else if queryItem.name == "callback_url" { + callbackUrl = value + } else if queryItem.name == "payload" { + if let data = value.data(using: .utf8) { + opaquePayload = data + } + } else if queryItem.name == "nonce" { + if let data = value.data(using: .utf8) { + opaqueNonce = data + } + } + } + } + } + + let valid: Bool + if parsedUrl.host == "resolve" { + if domain == "telegrampassport" { + valid = true + } else { + valid = false + } } else { - valid = false + valid = true } - } else { - valid = true - } - - if valid { - if let botId = botId, let scope = scope, let publicKey = publicKey, let callbackUrl = callbackUrl { - if scope.hasPrefix("{") && scope.hasSuffix("}") { - opaquePayload = Data() - if opaqueNonce.isEmpty { + + if valid { + if let botId = botId, let scope = scope, let publicKey = publicKey, let callbackUrl = callbackUrl { + if scope.hasPrefix("{") && scope.hasSuffix("}") { + opaquePayload = Data() + if opaqueNonce.isEmpty { + return + } + } else if opaquePayload.isEmpty { return } - } else if opaquePayload.isEmpty { - return - } - if case .chat = urlContext { - return - } - let controller = SecureIdAuthController(context: context, mode: .form(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: botId), scope: scope, publicKey: publicKey, callbackUrl: callbackUrl, opaquePayload: opaquePayload, opaqueNonce: opaqueNonce)) - - if let navigationController = navigationController { - context.sharedContext.applicationBindings.dismissNativeController() + if case .chat = urlContext { + return + } + let controller = SecureIdAuthController(context: context, mode: .form(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: botId), scope: scope, publicKey: publicKey, callbackUrl: callbackUrl, opaquePayload: opaquePayload, opaqueNonce: opaqueNonce)) - navigationController.view.window?.endEditing(true) - context.sharedContext.applicationBindings.getWindowHost()?.present(controller, on: .root, blockInteraction: false, completion: {}) + if let navigationController = navigationController { + context.sharedContext.applicationBindings.dismissNativeController() + + navigationController.view.window?.endEditing(true) + context.sharedContext.applicationBindings.getWindowHost()?.present(controller, on: .root, blockInteraction: false, completion: {}) + } } + return } - return } - } - } else if parsedUrl.host == "user" { - if let components = URLComponents(string: "/?" + query) { - var id: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "id" { - id = value + } else if parsedUrl.host == "user" { + if let components = URLComponents(string: "/?" + query) { + var id: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "id" { + id = value + } } } } - } - - if let id = id, !id.isEmpty, let idValue = Int32(id), idValue > 0 { - let _ = (context.account.postbox.transaction { transaction -> Peer? in - return transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: idValue)) - } - |> deliverOnMainQueue).start(next: { peer in - if let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { - navigationController?.pushViewController(controller) + + if let id = id, !id.isEmpty, let idValue = Int32(id), idValue > 0 { + let _ = (context.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: idValue)) } - }) - return + |> deliverOnMainQueue).start(next: { peer in + if let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { + navigationController?.pushViewController(controller) + } + }) + return + } } - } - } else if parsedUrl.host == "login" { - if let components = URLComponents(string: "/?" + query) { - var code: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "code" { - code = value + } else if parsedUrl.host == "login" { + if let components = URLComponents(string: "/?" + query) { + var code: String? + var isToken: Bool = false + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "code" { + code = value + } + } + if queryItem.name == "token" { + isToken = true } } } + if isToken { + context.sharedContext.presentGlobalController(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.AuthSessions_AddDevice_UrlLoginHint, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { + }), + ], parseMarkdown: true), nil) + return + } + if let code = code { + convertedUrl = "https://t.me/login/\(code)" + } } - if let code = code { - convertedUrl = "https://t.me/login/\(code)" - } - } - } else if parsedUrl.host == "confirmphone" { - if let components = URLComponents(string: "/?" + query) { - var phone: String? - var hash: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "phone" { - phone = value - } else if queryItem.name == "hash" { - hash = value + } else if parsedUrl.host == "confirmphone" { + if let components = URLComponents(string: "/?" + query) { + var phone: String? + var hash: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "phone" { + phone = value + } else if queryItem.name == "hash" { + hash = value + } } } } + if let phone = phone, let hash = hash { + convertedUrl = "https://t.me/confirmphone?phone=\(phone)&hash=\(hash)" + } } - if let phone = phone, let hash = hash { - convertedUrl = "https://t.me/confirmphone?phone=\(phone)&hash=\(hash)" - } - } - } else if parsedUrl.host == "bg" { - if let components = URLComponents(string: "/?" + query) { - var parameter: String? - var mode = "" - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "slug" { - parameter = value - } else if queryItem.name == "color" { - parameter = value - } else if queryItem.name == "mode" { - mode = "?mode=\(value)" + } else if parsedUrl.host == "bg" { + if let components = URLComponents(string: "/?" + query) { + var parameter: String? + var query: [String] = [] + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "slug" { + parameter = value + } else if queryItem.name == "color" { + parameter = value + } else if queryItem.name == "gradient" { + parameter = value + } else if queryItem.name == "mode" { + query.append("mode=\(value)") + } else if queryItem.name == "bg_color" { + query.append("bg_color=\(value)") + } else if queryItem.name == "intensity" { + query.append("intensity=\(value)") + } else if queryItem.name == "rotation" { + query.append("rotation=\(value)") + } } } } + var queryString = "" + if !query.isEmpty { + queryString = "?\(query.joined(separator: "&"))" + } + if let parameter = parameter { + convertedUrl = "https://t.me/bg/\(parameter)\(queryString)" + } } - if let parameter = parameter { - convertedUrl = "https://t.me/bg/\(parameter)\(mode)" - } - } - } else if parsedUrl.host == "addtheme" { - if let components = URLComponents(string: "/?" + query) { - var parameter: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "slug" { - parameter = value + } else if parsedUrl.host == "addtheme" { + if let components = URLComponents(string: "/?" + query) { + var parameter: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "slug" { + parameter = value + } } } } - } - if let parameter = parameter { - convertedUrl = "https://t.me/addtheme/\(parameter)" + if let parameter = parameter { + convertedUrl = "https://t.me/addtheme/\(parameter)" + } } } - } - - if parsedUrl.host == "resolve" { - if let components = URLComponents(string: "/?" + query) { - var domain: String? - var start: String? - var startGroup: String? - var game: String? - var post: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "domain" { - domain = value - } else if queryItem.name == "start" { - start = value - } else if queryItem.name == "startgroup" { - startGroup = value - } else if queryItem.name == "game" { - game = value - } else if queryItem.name == "post" { - post = value + + if parsedUrl.host == "resolve" { + if let components = URLComponents(string: "/?" + query) { + var domain: String? + var start: String? + var startGroup: String? + var game: String? + var post: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "domain" { + domain = value + } else if queryItem.name == "start" { + start = value + } else if queryItem.name == "startgroup" { + startGroup = value + } else if queryItem.name == "game" { + game = value + } else if queryItem.name == "post" { + post = value + } } } } - } - - if let domain = domain { - var result = "https://t.me/\(domain)" - if let post = post, let postValue = Int(post) { - result += "/\(postValue)" + + if let domain = domain { + var result = "https://t.me/\(domain)" + if let post = post, let postValue = Int(post) { + result += "/\(postValue)" + } + if let start = start { + result += "?start=\(start)" + } else if let startGroup = startGroup { + result += "?startgroup=\(startGroup)" + } else if let game = game { + result += "?game=\(game)" + } + convertedUrl = result } - if let start = start { - result += "?start=\(start)" - } else if let startGroup = startGroup { - result += "?startgroup=\(startGroup)" - } else if let game = game { - result += "?game=\(game)" - } - convertedUrl = result } - } - } else if parsedUrl.host == "hostOverride" { - if let components = URLComponents(string: "/?" + query) { - var host: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "host" { - host = value + } else if parsedUrl.host == "hostOverride" { + if let components = URLComponents(string: "/?" + query) { + var host: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "host" { + host = value + } } } } + if let host = host { + let _ = updateNetworkSettingsInteractively(postbox: context.account.postbox, network: context.account.network, { settings in + var settings = settings + settings.backupHostOverride = host + return settings + }).start() + return + } } - if let host = host { - let _ = updateNetworkSettingsInteractively(postbox: context.account.postbox, network: context.account.network, { settings in - var settings = settings - settings.backupHostOverride = host - return settings - }).start() - return + } + } else { + if parsedUrl.host == "settings" { + if let path = parsedUrl.pathComponents.last { + var section: ResolvedUrlSettingsSection? + switch path { + case "theme": + section = .theme + case "devices": + section = .devices + default: + break + } + if let section = section { + handleResolvedUrl(.settings(section)) + } } } } diff --git a/submodules/TelegramUI/TelegramUI/OverlayMediaControllerNode.swift b/submodules/TelegramUI/TelegramUI/OverlayMediaControllerNode.swift index 3857ed9d39..2b2ac927aa 100644 --- a/submodules/TelegramUI/TelegramUI/OverlayMediaControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/OverlayMediaControllerNode.swift @@ -224,8 +224,6 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega } } - shouldHide = false - return (result, shouldHide) } @@ -323,13 +321,13 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega let translation = recognizer.translation(in: self.view) var nodeFrame = draggingNode.frame nodeFrame.origin = self.draggingStartPosition.offsetBy(dx: translation.x, dy: translation.y) -// if nodeFrame.midX < 0.0 { -// draggingNode.updateMinimizedEdge(.left, adjusting: true) -// } else if nodeFrame.midX > validLayout.size.width { -// draggingNode.updateMinimizedEdge(.right, adjusting: true) -// } else { + if nodeFrame.midX < 0.0 { + draggingNode.updateMinimizedEdge(.left, adjusting: true) + } else if nodeFrame.midX > validLayout.size.width { + draggingNode.updateMinimizedEdge(.right, adjusting: true) + } else { draggingNode.updateMinimizedEdge(nil, adjusting: true) -// } + } draggingNode.frame = nodeFrame } case .ended, .cancelled: @@ -339,13 +337,13 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega let (updatedLocation, shouldDismiss) = self.nodeLocationForPosition(layout: validLayout, position: CGPoint(x: previousFrame.midX, y: previousFrame.midY), velocity: recognizer.velocity(in: self.view), size: nodeSize, tempExtendedTopInset: draggingNode.tempExtendedTopInset) -// if shouldDismiss && draggingNode.isMinimizeable { -// draggingNode.updateMinimizedEdge(updatedLocation.x.isZero ? .left : .right, adjusting: false) -// self.videoNodes[index].isMinimized = true -// } else { + if shouldDismiss && draggingNode.isMinimizeable { + draggingNode.updateMinimizedEdge(updatedLocation.x.isZero ? .left : .right, adjusting: false) + self.videoNodes[index].isMinimized = true + } else { draggingNode.updateMinimizedEdge(nil, adjusting: true) self.videoNodes[index].isMinimized = false -// } + } if let group = draggingNode.group { self.locationByGroup[group] = updatedLocation diff --git a/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift b/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift index 0074f5a77d..330f8213b5 100644 --- a/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift @@ -66,7 +66,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in - }, clickThroughMessage: { + }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in @@ -77,7 +77,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in - }, openUrl: { _, _, _ in + }, openUrl: { _, _, _, _ in }, shareCurrentLocation: { }, shareAccountContact: { }, sendBotCommand: { _, _ in @@ -107,7 +107,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, requestRedeliveryOfFailedMessages: { _ in }, addContact: { _ in }, rateCall: { _, _ in - }, requestSelectMessagePollOption: { _, _ in + }, requestSelectMessagePollOptions: { _, _ in + }, requestOpenMessagePollResults: { _, _ in }, openAppStorePage: { }, displayMessageTooltip: { _, _, _, _ in }, seekToTimecode: { _, _, _ in @@ -118,6 +119,9 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, updateMessageReaction: { _, _ in }, openMessageReactions: { _ in }, displaySwipeToReplyHint: { + }, dismissReplyMarkupMessage: { _ in + }, openMessagePollResults: { _, _ in + }, openPollCreation: { _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false)) @@ -127,7 +131,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu self.contentNode = ASDisplayNode() - self.controlsNode = OverlayPlayerControlsNode(account: context.account, accountManager: context.sharedContext.accountManager, theme: self.presentationData.theme, status: context.sharedContext.mediaManager.musicMediaPlayerState) + self.controlsNode = OverlayPlayerControlsNode(account: context.account, accountManager: context.sharedContext.accountManager, presentationData: self.presentationData, status: context.sharedContext.mediaManager.musicMediaPlayerState) self.historyBackgroundNode = ASDisplayNode() self.historyBackgroundNode.isLayerBacked = true @@ -146,7 +150,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu tagMask = .voiceOrInstantVideo } - self.historyNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: tagMask, subject: .message(initialMessageId), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: currentIsReversed)) + self.historyNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: tagMask, subject: .message(initialMessageId), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: currentIsReversed, displayHeaders: .none)) super.init() @@ -161,6 +165,20 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu } } + self.historyNode.endedInteractiveDragging = { [weak self] in + guard let strongSelf = self else { + return + } + switch strongSelf.historyNode.visibleContentOffset() { + case let .known(value): + if value <= -10.0 { + strongSelf.requestDismiss() + } + default: + break + } + } + self.controlsNode.updateIsExpanded = { [weak self] in if let strongSelf = self, let validLayout = strongSelf.validLayout { strongSelf.containerLayoutUpdated(validLayout, transition: .animated(duration: 0.3, curve: .spring)) @@ -242,6 +260,17 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu panRecognizer.delegate = self panRecognizer.delaysTouchesBegan = false panRecognizer.cancelsTouchesInView = true + panRecognizer.shouldBegin = { [weak self] point in + guard let strongSelf = self else { + return false + } + if strongSelf.controlsNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.controlsNode.view)) { + if strongSelf.controlsNode.frame.maxY <= strongSelf.historyNode.frame.minY { + return true + } + } + return false + } self.view.addGestureRecognizer(panRecognizer) } @@ -249,7 +278,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu self.presentationData = presentationData self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor - self.controlsNode.updateTheme(self.presentationData.theme) + self.controlsNode.updatePresentationData(self.presentationData) } func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -281,29 +310,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu transition.updateFrame(node: self.historyNode, frame: CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize)) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: duration, curve: listViewCurve) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: duration, curve: curve) self.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) if let replacementHistoryNode = replacementHistoryNode { let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: 0.0, curve: .Default(duration: nil)) @@ -326,18 +334,23 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.controlsNode.bounds.contains(self.view.convert(point, to: self.controlsNode.view)) { + let controlsHitTest = self.controlsNode.view.hitTest(self.view.convert(point, to: self.controlsNode.view), with: event) + if controlsHitTest == nil { + if self.controlsNode.frame.maxY > self.historyNode.frame.minY { + return self.historyNode.view + } + } + } + + let result = super.hitTest(point, with: event) + if !self.bounds.contains(point) { return nil } if point.y < self.controlsNode.frame.minY { return self.dimNode.view } - let result = super.hitTest(point, with: event) - if self.controlsNode.frame.contains(point) { -// if result == self.historyNode.view { -// return self.view -// } - } return result } @@ -467,7 +480,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu tagMask = .voiceOrInstantVideo } - let historyNode = ChatHistoryListNode(context: self.context, chatLocation: .peer(self.peerId), tagMask: tagMask, subject: .message(messageId), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed)) + let historyNode = ChatHistoryListNode(context: self.context, chatLocation: .peer(self.peerId), tagMask: tagMask, subject: .message(messageId), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, displayHeaders: .none)) historyNode.preloadPages = true historyNode.stackFromBottom = true historyNode.updateFloatingHeaderOffset = { [weak self] offset, _ in @@ -553,6 +566,20 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu } } + self.historyNode.endedInteractiveDragging = { [weak self] in + guard let strongSelf = self else { + return + } + switch strongSelf.historyNode.visibleContentOffset() { + case let .known(value): + if value <= -10.0 { + strongSelf.requestDismiss() + } + default: + break + } + } + self.historyNode.beganInteractiveDragging = { [weak self] in self?.controlsNode.collapse() } diff --git a/submodules/TelegramUI/TelegramUI/OverlayPlayerControlsNode.swift b/submodules/TelegramUI/TelegramUI/OverlayPlayerControlsNode.swift index 95921c753e..e866e57ec7 100644 --- a/submodules/TelegramUI/TelegramUI/OverlayPlayerControlsNode.swift +++ b/submodules/TelegramUI/TelegramUI/OverlayPlayerControlsNode.swift @@ -60,7 +60,7 @@ private func timestampLabelWidthForDuration(_ timestamp: Double) -> CGFloat { private let titleFont = Font.semibold(17.0) private let descriptionFont = Font.regular(17.0) -private func stringsForDisplayData(_ data: SharedMediaPlaybackDisplayData?, theme: PresentationTheme) -> (NSAttributedString?, NSAttributedString?) { +private func stringsForDisplayData(_ data: SharedMediaPlaybackDisplayData?, presentationData: PresentationData) -> (NSAttributedString?, NSAttributedString?) { var titleString: NSAttributedString? var descriptionString: NSAttributedString? @@ -69,15 +69,15 @@ private func stringsForDisplayData(_ data: SharedMediaPlaybackDisplayData?, them let subtitleText: String switch data { case let .music(title, performer, _, _): - titleText = title ?? "Unknown Track" - subtitleText = performer ?? "Unknown Artist" + titleText = title ?? presentationData.strings.MediaPlayer_UnknownTrack + subtitleText = performer ?? presentationData.strings.MediaPlayer_UnknownArtist case .voice, .instantVideo: titleText = "" subtitleText = "" } - titleString = NSAttributedString(string: titleText, font: titleFont, textColor: theme.list.itemPrimaryTextColor) - descriptionString = NSAttributedString(string: subtitleText, font: descriptionFont, textColor: theme.list.itemSecondaryTextColor) + titleString = NSAttributedString(string: titleText, font: titleFont, textColor: presentationData.theme.list.itemPrimaryTextColor) + descriptionString = NSAttributedString(string: subtitleText, font: descriptionFont, textColor: presentationData.theme.list.itemSecondaryTextColor) } return (titleString, descriptionString) @@ -86,7 +86,7 @@ private func stringsForDisplayData(_ data: SharedMediaPlaybackDisplayData?, them final class OverlayPlayerControlsNode: ASDisplayNode { private let accountManager: AccountManager private let postbox: Postbox - private var theme: PresentationTheme + private var presentationData: PresentationData private let backgroundNode: ASImageNode @@ -142,20 +142,20 @@ final class OverlayPlayerControlsNode: ASDisplayNode { private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat)? - init(account: Account, accountManager: AccountManager, theme: PresentationTheme, status: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading)?, NoError>) { + init(account: Account, accountManager: AccountManager, presentationData: PresentationData, status: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading)?, NoError>) { self.accountManager = accountManager self.postbox = account.postbox - self.theme = theme + self.presentationData = presentationData self.backgroundNode = ASImageNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.image = generateBackground(theme: theme) + self.backgroundNode.image = generateBackground(theme: presentationData.theme) self.collapseNode = HighlightableButtonNode() self.collapseNode.displaysAsynchronously = false - self.collapseNode.setImage(generateCollapseIcon(theme: theme), for: []) + self.collapseNode.setImage(generateCollapseIcon(theme: presentationData.theme), for: []) self.albumArtNode = TransformImageNode() @@ -168,15 +168,17 @@ final class OverlayPlayerControlsNode: ASDisplayNode { self.descriptionNode.displaysAsynchronously = false self.shareNode = HighlightableButtonNode() - self.shareNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Share"), color: theme.list.itemAccentColor), for: []) + self.shareNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Share"), color: presentationData.theme.list.itemAccentColor), for: []) - self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .circle, backgroundColor: theme.list.controlSecondaryColor, foregroundColor: theme.list.itemAccentColor, bufferingColor: theme.list.itemAccentColor.withAlphaComponent(0.4))) - self.leftDurationLabel = MediaPlayerTimeTextNode(textColor: theme.list.itemSecondaryTextColor) + self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .circle, backgroundColor: presentationData.theme.list.controlSecondaryColor, foregroundColor: presentationData.theme.list.itemAccentColor, bufferingColor: presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4))) + self.leftDurationLabel = MediaPlayerTimeTextNode(textColor: presentationData.theme.list.itemSecondaryTextColor) self.leftDurationLabel.displaysAsynchronously = false - self.rightDurationLabel = MediaPlayerTimeTextNode(textColor: theme.list.itemSecondaryTextColor) + self.leftDurationLabel.keepPreviousValueOnEmptyState = true + self.rightDurationLabel = MediaPlayerTimeTextNode(textColor: presentationData.theme.list.itemSecondaryTextColor) self.rightDurationLabel.displaysAsynchronously = false self.rightDurationLabel.mode = .reversed self.rightDurationLabel.alignment = .right + self.rightDurationLabel.keepPreviousValueOnEmptyState = true self.rateButton = HighlightableButtonNode() self.rateButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -4.0, bottom: -8.0, right: -4.0) @@ -197,12 +199,12 @@ final class OverlayPlayerControlsNode: ASDisplayNode { self.playPauseButton = IconButtonNode() self.playPauseButton.displaysAsynchronously = false - self.backwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Previous"), color: theme.list.itemPrimaryTextColor) - self.forwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Next"), color: theme.list.itemPrimaryTextColor) + self.backwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Previous"), color: presentationData.theme.list.itemPrimaryTextColor) + self.forwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Next"), color: presentationData.theme.list.itemPrimaryTextColor) self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true - self.separatorNode.backgroundColor = theme.list.itemPlainSeparatorColor + self.separatorNode.backgroundColor = presentationData.theme.list.itemPlainSeparatorColor super.init() @@ -347,14 +349,14 @@ final class OverlayPlayerControlsNode: ASDisplayNode { } let duration = value.status.duration - if duration != strongSelf.currentDuration { + if duration != strongSelf.currentDuration && !duration.isZero { strongSelf.currentDuration = duration if let layout = strongSelf.validLayout { strongSelf.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, transition: .immediate) } } - strongSelf.rateButton.isHidden = rateButtonIsHidden || strongSelf.currentDuration.isZero + strongSelf.rateButton.isHidden = rateButtonIsHidden } else { strongSelf.playPauseButton.isEnabled = false strongSelf.backwardButton.isEnabled = false @@ -411,20 +413,20 @@ final class OverlayPlayerControlsNode: ASDisplayNode { self.albumArtNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.albumArtTap(_:)))) } - func updateTheme(_ theme: PresentationTheme) { - guard self.theme !== theme else { + func updatePresentationData(_ presentationData: PresentationData) { + guard self.presentationData.theme !== presentationData.theme else { return } - self.theme = theme + self.presentationData = presentationData - self.backgroundNode.image = generateBackground(theme: theme) - self.collapseNode.setImage(generateCollapseIcon(theme: theme), for: []) - self.shareNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Share"), color: theme.list.itemAccentColor), for: []) - self.scrubberNode.updateColors(backgroundColor: theme.list.controlSecondaryColor, foregroundColor: theme.list.itemAccentColor) - self.leftDurationLabel.textColor = theme.list.itemSecondaryTextColor - self.rightDurationLabel.textColor = theme.list.itemSecondaryTextColor - self.backwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Previous"), color: theme.list.itemPrimaryTextColor) - self.forwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Next"), color: theme.list.itemPrimaryTextColor) + self.backgroundNode.image = generateBackground(theme: presentationData.theme) + self.collapseNode.setImage(generateCollapseIcon(theme: presentationData.theme), for: []) + self.shareNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Share"), color: presentationData.theme.list.itemAccentColor), for: []) + self.scrubberNode.updateColors(backgroundColor: presentationData.theme.list.controlSecondaryColor, foregroundColor: presentationData.theme.list.itemAccentColor) + self.leftDurationLabel.textColor = presentationData.theme.list.itemSecondaryTextColor + self.rightDurationLabel.textColor = presentationData.theme.list.itemSecondaryTextColor + self.backwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Previous"), color: presentationData.theme.list.itemPrimaryTextColor) + self.forwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Next"), color: presentationData.theme.list.itemPrimaryTextColor) if let isPaused = self.currentIsPaused { self.updatePlayPauseButton(paused: isPaused) } @@ -437,7 +439,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { if let rate = self.currentRate { self.updateRateButton(rate) } - self.separatorNode.backgroundColor = theme.list.itemPlainSeparatorColor + self.separatorNode.backgroundColor = presentationData.theme.list.itemPlainSeparatorColor } private func updateLabels(transition: ContainedViewLayoutTransition) { @@ -454,7 +456,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { let infoVerticalOrigin: CGFloat = panelHeight - OverlayPlayerControlsNode.basePanelHeight + 36.0 - let (titleString, descriptionString) = stringsForDisplayData(self.displayData, theme: self.theme) + let (titleString, descriptionString) = stringsForDisplayData(self.displayData, presentationData: self.presentationData) let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - sideInset * 2.0 - leftInset - rightInset - infoLabelsLeftInset - infoLabelsRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode) @@ -487,42 +489,42 @@ final class OverlayPlayerControlsNode: ASDisplayNode { private func updatePlayPauseButton(paused: Bool) { if paused { - self.playPauseButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Play"), color: self.theme.list.itemPrimaryTextColor) + self.playPauseButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Play"), color: self.presentationData.theme.list.itemPrimaryTextColor) } else { - self.playPauseButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Pause"), color: self.theme.list.itemPrimaryTextColor) + self.playPauseButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Pause"), color: self.presentationData.theme.list.itemPrimaryTextColor) } } private func updateOrderButton(_ order: MusicPlaybackSettingsOrder) { - let baseColor = self.theme.list.itemSecondaryTextColor + let baseColor = self.presentationData.theme.list.itemSecondaryTextColor switch order { case .regular: self.orderButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/OrderReverse"), color: baseColor) case .reversed: - self.orderButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/OrderReverse"), color: self.theme.list.itemAccentColor) + self.orderButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/OrderReverse"), color: self.presentationData.theme.list.itemAccentColor) case .random: - self.orderButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/OrderRandom"), color: self.theme.list.itemAccentColor) + self.orderButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/OrderRandom"), color: self.presentationData.theme.list.itemAccentColor) } } private func updateLoopButton(_ looping: MusicPlaybackSettingsLooping) { - let baseColor = self.theme.list.itemSecondaryTextColor + let baseColor = self.presentationData.theme.list.itemSecondaryTextColor switch looping { case .none: self.loopingButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Repeat"), color: baseColor) case .item: - self.loopingButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/RepeatOne"), color: self.theme.list.itemAccentColor) + self.loopingButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/RepeatOne"), color: self.presentationData.theme.list.itemAccentColor) case .all: - self.loopingButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Repeat"), color: self.theme.list.itemAccentColor) + self.loopingButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Repeat"), color: self.presentationData.theme.list.itemAccentColor) } } private func updateRateButton(_ baseRate: AudioPlaybackRate) { switch baseRate { case .x2: - self.rateButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedRateActiveIcon(self.theme), for: []) + self.rateButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedRateActiveIcon(self.presentationData.theme), for: []) default: - self.rateButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedRateInactiveIcon(self.theme), for: []) + self.rateButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedRateInactiveIcon(self.presentationData.theme), for: []) } } diff --git a/submodules/TelegramUI/TelegramUI/PaneSearchContainerNode.swift b/submodules/TelegramUI/TelegramUI/PaneSearchContainerNode.swift index 21acd7a3e7..d56bb6e555 100644 --- a/submodules/TelegramUI/TelegramUI/PaneSearchContainerNode.swift +++ b/submodules/TelegramUI/TelegramUI/PaneSearchContainerNode.swift @@ -49,10 +49,10 @@ final class PaneSearchContainerNode: ASDisplayNode { self.controllerInteraction = controllerInteraction self.inputNodeInteraction = inputNodeInteraction switch mode { - case .gif: - self.contentNode = GifPaneSearchContentNode(context: context, theme: theme, strings: strings, controllerInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction, trendingPromise: trendingGifsPromise) - case .sticker: - self.contentNode = StickerPaneSearchContentNode(context: context, theme: theme, strings: strings, controllerInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction) + case .gif: + self.contentNode = GifPaneSearchContentNode(context: context, theme: theme, strings: strings, controllerInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction, trendingPromise: trendingGifsPromise) + case .sticker, .trending: + self.contentNode = StickerPaneSearchContentNode(context: context, theme: theme, strings: strings, controllerInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction) } self.backgroundNode = ASDisplayNode() @@ -92,10 +92,10 @@ final class PaneSearchContainerNode: ASDisplayNode { let placeholder: String switch mode { - case .gif: - placeholder = strings.Gif_Search - case .sticker: - placeholder = strings.Stickers_Search + case .gif: + placeholder = strings.Gif_Search + case .sticker, .trending: + placeholder = strings.Stickers_Search } self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor) } diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenActionItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenActionItem.swift new file mode 100644 index 0000000000..c7b9fb361f --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenActionItem.swift @@ -0,0 +1,98 @@ +import AsyncDisplayKit +import Display +import TelegramPresentationData + +enum PeerInfoScreenActionColor { + case accent + case destructive +} + +final class PeerInfoScreenActionItem: PeerInfoScreenItem { + let id: AnyHashable + let text: String + let color: PeerInfoScreenActionColor + let action: (() -> Void)? + + init(id: AnyHashable, text: String, color: PeerInfoScreenActionColor = .accent, action: (() -> Void)?) { + self.id = id + self.text = text + self.color = color + self.action = action + } + + func node() -> PeerInfoScreenItemNode { + return PeerInfoScreenActionItemNode() + } +} + +private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode { + private let selectionNode: PeerInfoScreenSelectableBackgroundNode + private let textNode: ImmediateTextNode + private let bottomSeparatorNode: ASDisplayNode + + private var item: PeerInfoScreenActionItem? + + override init() { + var bringToFrontForHighlightImpl: (() -> Void)? + self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() }) + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.isUserInteractionEnabled = false + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + + super.init() + + bringToFrontForHighlightImpl = { [weak self] in + self?.bringToFrontForHighlight?() + } + + self.addSubnode(self.bottomSeparatorNode) + self.addSubnode(self.selectionNode) + self.addSubnode(self.textNode) + } + + override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat { + guard let item = item as? PeerInfoScreenActionItem else { + return 10.0 + } + + self.item = item + + self.selectionNode.pressed = item.action + + let sideInset: CGFloat = 16.0 + + self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + let textColorValue: UIColor + switch item.color { + case .accent: + textColorValue = presentationData.theme.list.itemAccentColor + case .destructive: + textColorValue = presentationData.theme.list.itemDestructiveColor + } + + self.textNode.maximumNumberOfLines = 1 + self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue) + + let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude)) + + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: textSize) + + let height = textSize.height + 22.0 + + transition.updateFrame(node: self.textNode, frame: textFrame) + + let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel + self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition) + transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset))) + + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel))) + transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0) + + return height + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenAddressItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenAddressItem.swift new file mode 100644 index 0000000000..c9391e8c5a --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenAddressItem.swift @@ -0,0 +1,122 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import ItemListAddressItem +import SwiftSignalKit +import AccountContext + +final class PeerInfoScreenAddressItem: PeerInfoScreenItem { + let id: AnyHashable + let label: String + let text: String + let imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + let action: (() -> Void)? + let longTapAction: (() -> Void)? + let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? + + init( + id: AnyHashable, + label: String, + text: String, + imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?, + action: (() -> Void)?, + longTapAction: (() -> Void)? = nil, + linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil + ) { + self.id = id + self.label = label + self.text = text + self.imageSignal = imageSignal + self.action = action + self.longTapAction = longTapAction + self.linkItemAction = linkItemAction + } + + func node() -> PeerInfoScreenItemNode { + return PeerInfoScreenAddressItemNode() + } +} + +private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode { + private let selectionNode: PeerInfoScreenSelectableBackgroundNode + private let bottomSeparatorNode: ASDisplayNode + + private var item: PeerInfoScreenAddressItem? + private var itemNode: ItemListAddressItemNode? + + override init() { + var bringToFrontForHighlightImpl: (() -> Void)? + self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() }) + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + + super.init() + + bringToFrontForHighlightImpl = { [weak self] in + self?.bringToFrontForHighlight?() + } + + self.addSubnode(self.bottomSeparatorNode) + self.addSubnode(self.selectionNode) + } + + override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat { + guard let item = item as? PeerInfoScreenAddressItem else { + return 10.0 + } + + self.item = item + + self.selectionNode.pressed = item.action + + let sideInset: CGFloat = 16.0 + + self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + let addressItem = ItemListAddressItem(theme: presentationData.theme, label: item.label, text: item.text, imageSignal: item.imageSignal, sectionId: 0, style: .blocks, displayDecorations: false, action: nil, longTapAction: item.longTapAction, linkItemAction: item.linkItemAction) + + let params = ListViewItemLayoutParams(width: width, leftInset: 0.0, rightInset: 0.0, availableHeight: 1000.0) + + let itemNode: ItemListAddressItemNode + if let current = self.itemNode { + itemNode = current + addressItem.updateNode(async: { $0() }, node: { + return itemNode + }, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: layout.size.height)) + + itemNode.contentSize = layout.contentSize + itemNode.insets = layout.insets + itemNode.frame = nodeFrame + + apply(ListViewItemApply(isOnScreen: true)) + }) + } else { + var itemNodeValue: ListViewItemNode? + addressItem.nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { node, apply in + itemNodeValue = node + apply().1(ListViewItemApply(isOnScreen: true)) + }) + itemNode = itemNodeValue as! ItemListAddressItemNode + itemNode.isUserInteractionEnabled = false + self.itemNode = itemNode + self.addSubnode(itemNode) + } + + let height = itemNode.contentSize.height + + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: itemNode.bounds.size)) + + let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel + self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition) + transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset))) + + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel))) + transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0) + + return height + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenCallListItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenCallListItem.swift new file mode 100644 index 0000000000..25b847138a --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenCallListItem.swift @@ -0,0 +1,111 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import ItemListAddressItem +import SwiftSignalKit +import AccountContext +import Postbox +import PeerInfoUI +import ItemListUI + +final class PeerInfoScreenCallListItem: PeerInfoScreenItem { + let id: AnyHashable + let messages: [Message] + + init( + id: AnyHashable, + messages: [Message] + ) { + self.id = id + self.messages = messages + } + + func node() -> PeerInfoScreenItemNode { + return PeerInfoScreenCallListItemNode() + } +} + +private final class PeerInfoScreenCallListItemNode: PeerInfoScreenItemNode { + private let selectionNode: PeerInfoScreenSelectableBackgroundNode + private let bottomSeparatorNode: ASDisplayNode + + private var item: PeerInfoScreenCallListItem? + private var itemNode: ItemListCallListItemNode? + + override init() { + var bringToFrontForHighlightImpl: (() -> Void)? + self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() }) + self.selectionNode.isUserInteractionEnabled = false + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + + super.init() + + bringToFrontForHighlightImpl = { [weak self] in + self?.bringToFrontForHighlight?() + } + + self.addSubnode(self.bottomSeparatorNode) + self.addSubnode(self.selectionNode) + } + + override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat { + guard let item = item as? PeerInfoScreenCallListItem else { + return 10.0 + } + + self.item = item + + self.selectionNode.pressed = nil + + let sideInset: CGFloat = 16.0 + + self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + let addressItem = ItemListCallListItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, messages: item.messages, sectionId: 0, style: .blocks, displayDecorations: false) + + let params = ListViewItemLayoutParams(width: width, leftInset: 0.0, rightInset: 0.0, availableHeight: 1000.0) + + let itemNode: ItemListCallListItemNode + if let current = self.itemNode { + itemNode = current + addressItem.updateNode(async: { $0() }, node: { + return itemNode + }, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: layout.size.height)) + + itemNode.contentSize = layout.contentSize + itemNode.insets = layout.insets + itemNode.frame = nodeFrame + + apply(ListViewItemApply(isOnScreen: true)) + }) + } else { + var itemNodeValue: ListViewItemNode? + addressItem.nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { node, apply in + itemNodeValue = node + apply().1(ListViewItemApply(isOnScreen: true)) + }) + itemNode = itemNodeValue as! ItemListCallListItemNode + itemNode.isUserInteractionEnabled = false + self.itemNode = itemNode + self.addSubnode(itemNode) + } + + let height = itemNode.contentSize.height + + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: itemNode.bounds.size)) + + let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel + self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition) + transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset))) + + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel))) + transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0) + + return height + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenCommentItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenCommentItem.swift new file mode 100644 index 0000000000..19937b5c92 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenCommentItem.swift @@ -0,0 +1,57 @@ +import AsyncDisplayKit +import Display +import TelegramPresentationData + +final class PeerInfoScreenCommentItem: PeerInfoScreenItem { + let id: AnyHashable + let text: String + + init(id: AnyHashable, text: String) { + self.id = id + self.text = text + } + + func node() -> PeerInfoScreenItemNode { + return PeerInfoScreenCommentItemNode() + } +} + +private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode { + private let textNode: ImmediateTextNode + + private var item: PeerInfoScreenCommentItem? + + override init() { + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.isUserInteractionEnabled = false + + super.init() + + self.addSubnode(self.textNode) + } + + override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat { + guard let item = item as? PeerInfoScreenCommentItem else { + return 10.0 + } + + self.item = item + + let sideInset: CGFloat = 16.0 + let verticalInset: CGFloat = 7.0 + + self.textNode.maximumNumberOfLines = 0 + self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(14.0), textColor: presentationData.theme.list.freeTextColor) + + let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude)) + + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: textSize) + + let height = textSize.height + verticalInset * 2.0 + + transition.updateFrame(node: self.textNode, frame: textFrame) + + return height + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift new file mode 100644 index 0000000000..69c2fd2669 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift @@ -0,0 +1,118 @@ +import AsyncDisplayKit +import Display +import TelegramPresentationData +import SyncCore +import EncryptionKeyVisualization + +final class PeerInfoScreenDisclosureEncryptionKeyItem: PeerInfoScreenItem { + let id: AnyHashable + let text: String + let fingerprint: SecretChatKeyFingerprint + let action: (() -> Void)? + + init(id: AnyHashable, text: String, fingerprint: SecretChatKeyFingerprint, action: (() -> Void)?) { + self.id = id + self.text = text + self.fingerprint = fingerprint + self.action = action + } + + func node() -> PeerInfoScreenItemNode { + return PeerInfoScreenDisclosureEncryptionKeyItemNode() + } +} + +private final class PeerInfoScreenDisclosureEncryptionKeyItemNode: PeerInfoScreenItemNode { + private let selectionNode: PeerInfoScreenSelectableBackgroundNode + private let textNode: ImmediateTextNode + private let keyNode: ASImageNode + private let arrowNode: ASImageNode + private let bottomSeparatorNode: ASDisplayNode + + private var item: PeerInfoScreenDisclosureEncryptionKeyItem? + + override init() { + var bringToFrontForHighlightImpl: (() -> Void)? + self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() }) + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.isUserInteractionEnabled = false + + self.keyNode = ASImageNode() + self.keyNode.displaysAsynchronously = false + self.keyNode.displayWithoutProcessing = true + self.keyNode.isUserInteractionEnabled = false + + self.arrowNode = ASImageNode() + self.arrowNode.isLayerBacked = true + self.arrowNode.displaysAsynchronously = false + self.arrowNode.displayWithoutProcessing = true + self.arrowNode.isUserInteractionEnabled = false + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + + super.init() + + bringToFrontForHighlightImpl = { [weak self] in + self?.bringToFrontForHighlight?() + } + + self.addSubnode(self.bottomSeparatorNode) + self.addSubnode(self.selectionNode) + self.addSubnode(self.textNode) + self.addSubnode(self.keyNode) + self.addSubnode(self.arrowNode) + } + + override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat { + guard let item = item as? PeerInfoScreenDisclosureEncryptionKeyItem else { + return 10.0 + } + + if self.item?.fingerprint != item.fingerprint { + self.keyNode.image = secretChatKeyImage(item.fingerprint, size: CGSize(width: 24.0, height: 24.0)) + } + + self.item = item + + self.selectionNode.pressed = item.action + + let sideInset: CGFloat = 16.0 + + self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + self.textNode.maximumNumberOfLines = 1 + self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: presentationData.theme.list.itemPrimaryTextColor) + + let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0 - 44.0, height: .greatestFiniteMagnitude)) + + let arrowInset: CGFloat = 18.0 + + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: textSize) + + let height = textSize.height + 22.0 + + if let arrowImage = PresentationResourcesItemList.disclosureArrowImage(presentationData.theme) { + self.arrowNode.image = arrowImage + let arrowFrame = CGRect(origin: CGPoint(x: width - 7.0 - arrowImage.size.width, y: floorToScreenPixels((height - arrowImage.size.height) / 2.0)), size: arrowImage.size) + transition.updateFrame(node: self.arrowNode, frame: arrowFrame) + } + + if let image = self.keyNode.image { + self.keyNode.frame = CGRect(origin: CGPoint(x: width - sideInset - arrowInset - image.size.width, y: floor((height - image.size.height) / 2.0)), size: image.size) + } + + transition.updateFrame(node: self.textNode, frame: textFrame) + + let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel + self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition) + transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset))) + + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel))) + transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0) + + return height + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenDisclosureItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenDisclosureItem.swift new file mode 100644 index 0000000000..ca27a09e75 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenDisclosureItem.swift @@ -0,0 +1,115 @@ +import AsyncDisplayKit +import Display +import TelegramPresentationData + +final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { + let id: AnyHashable + let label: String + let text: String + let action: (() -> Void)? + + init(id: AnyHashable, label: String, text: String, action: (() -> Void)?) { + self.id = id + self.label = label + self.text = text + self.action = action + } + + func node() -> PeerInfoScreenItemNode { + return PeerInfoScreenDisclosureItemNode() + } +} + +private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { + private let selectionNode: PeerInfoScreenSelectableBackgroundNode + private let labelNode: ImmediateTextNode + private let textNode: ImmediateTextNode + private let arrowNode: ASImageNode + private let bottomSeparatorNode: ASDisplayNode + + private var item: PeerInfoScreenDisclosureItem? + + override init() { + var bringToFrontForHighlightImpl: (() -> Void)? + self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() }) + + self.labelNode = ImmediateTextNode() + self.labelNode.displaysAsynchronously = false + self.labelNode.isUserInteractionEnabled = false + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.isUserInteractionEnabled = false + + self.arrowNode = ASImageNode() + self.arrowNode.isLayerBacked = true + self.arrowNode.displaysAsynchronously = false + self.arrowNode.displayWithoutProcessing = true + self.arrowNode.isUserInteractionEnabled = false + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + + super.init() + + bringToFrontForHighlightImpl = { [weak self] in + self?.bringToFrontForHighlight?() + } + + self.addSubnode(self.bottomSeparatorNode) + self.addSubnode(self.selectionNode) + self.addSubnode(self.labelNode) + self.addSubnode(self.textNode) + self.addSubnode(self.arrowNode) + } + + override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat { + guard let item = item as? PeerInfoScreenDisclosureItem else { + return 10.0 + } + + self.item = item + + self.selectionNode.pressed = item.action + + let sideInset: CGFloat = 16.0 + + self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + let textColorValue: UIColor = presentationData.theme.list.itemPrimaryTextColor + let labelColorValue: UIColor = presentationData.theme.list.itemSecondaryTextColor + + self.labelNode.attributedText = NSAttributedString(string: item.label, font: Font.regular(17.0), textColor: labelColorValue) + + self.textNode.maximumNumberOfLines = 1 + self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue) + + let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude)) + let labelSize = self.labelNode.updateLayout(CGSize(width: width - textSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude)) + + let arrowInset: CGFloat = 18.0 + + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: textSize) + let labelFrame = CGRect(origin: CGPoint(x: width - sideInset - arrowInset - labelSize.width, y: 11.0), size: labelSize) + + let height = textSize.height + 22.0 + + if let arrowImage = PresentationResourcesItemList.disclosureArrowImage(presentationData.theme) { + self.arrowNode.image = arrowImage + let arrowFrame = CGRect(origin: CGPoint(x: width - 7.0 - arrowImage.size.width, y: floorToScreenPixels((height - arrowImage.size.height) / 2.0)), size: arrowImage.size) + transition.updateFrame(node: self.arrowNode, frame: arrowFrame) + } + + transition.updateFrame(node: self.labelNode, frame: labelFrame) + transition.updateFrame(node: self.textNode, frame: textFrame) + + let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel + self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition) + transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset))) + + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel))) + transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0) + + return height + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenHeaderItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenHeaderItem.swift new file mode 100644 index 0000000000..6ab8aab21e --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenHeaderItem.swift @@ -0,0 +1,57 @@ +import AsyncDisplayKit +import Display +import TelegramPresentationData + +final class PeerInfoScreenHeaderItem: PeerInfoScreenItem { + let id: AnyHashable + let text: String + + init(id: AnyHashable, text: String) { + self.id = id + self.text = text + } + + func node() -> PeerInfoScreenItemNode { + return PeerInfoScreenHeaderItemNode() + } +} + +private final class PeerInfoScreenHeaderItemNode: PeerInfoScreenItemNode { + private let textNode: ImmediateTextNode + + private var item: PeerInfoScreenHeaderItem? + + override init() { + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.isUserInteractionEnabled = false + + super.init() + + self.addSubnode(self.textNode) + } + + override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat { + guard let item = item as? PeerInfoScreenHeaderItem else { + return 10.0 + } + + self.item = item + + let sideInset: CGFloat = 16.0 + let verticalInset: CGFloat = 7.0 + + self.textNode.maximumNumberOfLines = 0 + self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(13.0), textColor: presentationData.theme.list.freeTextColor) + + let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude)) + + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: textSize) + + let height = textSize.height + verticalInset * 2.0 + + transition.updateFrame(node: self.textNode, frame: textFrame) + + return height + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift new file mode 100644 index 0000000000..6455c35496 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift @@ -0,0 +1,331 @@ +import AsyncDisplayKit +import Display +import TelegramPresentationData +import AccountContext +import TextFormat + +enum PeerInfoScreenLabeledValueTextColor { + case primary + case accent +} + +enum PeerInfoScreenLabeledValueTextBehavior: Equatable { + case singleLine + case multiLine(maxLines: Int, enabledEntities: EnabledEntityTypes) +} + +final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { + let id: AnyHashable + let label: String + let text: String + let textColor: PeerInfoScreenLabeledValueTextColor + let textBehavior: PeerInfoScreenLabeledValueTextBehavior + let action: (() -> Void)? + let longTapAction: ((ASDisplayNode) -> Void)? + let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? + let requestLayout: () -> Void + + init( + id: AnyHashable, + label: String, + text: String, + textColor: PeerInfoScreenLabeledValueTextColor = .primary, + textBehavior: PeerInfoScreenLabeledValueTextBehavior = .singleLine, + action: (() -> Void)?, + longTapAction: ((ASDisplayNode) -> Void)? = nil, + linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, + requestLayout: @escaping () -> Void + ) { + self.id = id + self.label = label + self.text = text + self.textColor = textColor + self.textBehavior = textBehavior + self.action = action + self.longTapAction = longTapAction + self.linkItemAction = linkItemAction + self.requestLayout = requestLayout + } + + func node() -> PeerInfoScreenItemNode { + return PeerInfoScreenLabeledValueItemNode() + } +} + +private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { + private let selectionNode: PeerInfoScreenSelectableBackgroundNode + private let labelNode: ImmediateTextNode + private let textNode: ImmediateTextNode + private let bottomSeparatorNode: ASDisplayNode + + private let expandNode: ImmediateTextNode + private let expandButonNode: HighlightTrackingButtonNode + + private var linkHighlightingNode: LinkHighlightingNode? + + private var item: PeerInfoScreenLabeledValueItem? + private var theme: PresentationTheme? + + private var isExpanded: Bool = false + + override init() { + var bringToFrontForHighlightImpl: (() -> Void)? + self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() }) + self.selectionNode.isUserInteractionEnabled = false + + self.labelNode = ImmediateTextNode() + self.labelNode.displaysAsynchronously = false + self.labelNode.isUserInteractionEnabled = false + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.isUserInteractionEnabled = false + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + + self.expandNode = ImmediateTextNode() + self.expandNode.displaysAsynchronously = false + self.expandNode.isUserInteractionEnabled = false + + self.expandButonNode = HighlightTrackingButtonNode() + + super.init() + + bringToFrontForHighlightImpl = { [weak self] in + self?.bringToFrontForHighlight?() + } + + self.addSubnode(self.bottomSeparatorNode) + self.addSubnode(self.selectionNode) + self.addSubnode(self.labelNode) + self.addSubnode(self.textNode) + + self.addSubnode(self.expandNode) + self.addSubnode(self.expandButonNode) + + self.expandButonNode.addTarget(self, action: #selector(self.expandPressed), forControlEvents: .touchUpInside) + self.expandButonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.expandNode.layer.removeAnimation(forKey: "opacity") + strongSelf.expandNode.alpha = 0.4 + } else { + strongSelf.expandNode.alpha = 1.0 + strongSelf.expandNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + @objc private func expandPressed() { + self.isExpanded = true + self.item?.requestLayout() + } + + override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { [weak self] point in + guard let strongSelf = self, let item = strongSelf.item else { + return .keepWithSingleTap + } + if !strongSelf.expandButonNode.isHidden, strongSelf.expandButonNode.view.hitTest(strongSelf.view.convert(point, to: strongSelf.expandButonNode.view), with: nil) != nil { + return .fail + } + if let _ = strongSelf.linkItemAtPoint(point) { + return .waitForSingleTap + } + if item.longTapAction != nil { + return .waitForSingleTap + } + if item.action != nil { + return .keepWithSingleTap + } + return .fail + } + recognizer.highlight = { [weak self] point in + guard let strongSelf = self else { + return + } + strongSelf.updateTouchesAtPoint(point) + } + self.view.addGestureRecognizer(recognizer) + } + + @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap, .longTap: + if let item = self.item { + if let linkItem = self.linkItemAtPoint(location) { + item.linkItemAction?(gesture == .tap ? .tap : .longTap, linkItem) + } else if case .longTap = gesture { + item.longTapAction?(self) + } else if case .tap = gesture { + item.action?() + } + } + default: + break + } + } + default: + break + } + } + + override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat { + guard let item = item as? PeerInfoScreenLabeledValueItem else { + return 10.0 + } + + self.item = item + self.theme = presentationData.theme + + self.selectionNode.pressed = item.action + + let sideInset: CGFloat = 16.0 + + self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + let textColorValue: UIColor + switch item.textColor { + case .primary: + textColorValue = presentationData.theme.list.itemPrimaryTextColor + case .accent: + textColorValue = presentationData.theme.list.itemAccentColor + } + + self.expandNode.attributedText = NSAttributedString(string: presentationData.strings.PeerInfo_BioExpand, font: Font.regular(17.0), textColor: presentationData.theme.list.itemAccentColor) + let expandSize = self.expandNode.updateLayout(CGSize(width: width, height: 100.0)) + + self.labelNode.attributedText = NSAttributedString(string: item.label, font: Font.regular(14.0), textColor: presentationData.theme.list.itemPrimaryTextColor) + + switch item.textBehavior { + case .singleLine: + self.textNode.cutout = nil + self.textNode.maximumNumberOfLines = 1 + self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue) + case let .multiLine(maxLines, enabledEntities): + self.textNode.maximumNumberOfLines = self.isExpanded ? maxLines : 3 + self.textNode.cutout = self.isExpanded ? nil : TextNodeCutout(bottomRight: CGSize(width: expandSize.width + 4.0, height: expandSize.height)) + if enabledEntities.isEmpty { + self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue) + } else { + let fontSize: CGFloat = 17.0 + + var baseFont = Font.regular(fontSize) + var linkFont = baseFont + var boldFont = Font.medium(fontSize) + var italicFont = Font.italic(fontSize) + var boldItalicFont = Font.semiboldItalic(fontSize) + let titleFixedFont = Font.monospace(fontSize) + + let entities = generateTextEntities(item.text, enabledTypes: enabledEntities) + self.textNode.attributedText = stringWithAppliedEntities(item.text, entities: entities, baseColor: textColorValue, linkColor: presentationData.theme.list.itemAccentColor, baseFont: baseFont, linkFont: linkFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: titleFixedFont, blockQuoteFont: baseFont) + } + } + + let labelSize = self.labelNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude)) + let textLayout = self.textNode.updateLayoutInfo(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude)) + let textSize = textLayout.size + + if case .multiLine = item.textBehavior, textLayout.truncated, !self.isExpanded { + self.expandNode.isHidden = false + self.expandButonNode.isHidden = false + } else { + self.expandNode.isHidden = true + self.expandButonNode.isHidden = true + } + + let labelFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: labelSize) + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: labelFrame.maxY + 3.0), size: textSize) + + let expandFrame = CGRect(origin: CGPoint(x: textFrame.minX + max(self.textNode.trailingLineWidth ?? 0.0, textFrame.width) - expandSize.width, y: textFrame.maxY - expandSize.height), size: expandSize) + self.expandNode.frame = expandFrame + self.expandButonNode.frame = expandFrame.insetBy(dx: -8.0, dy: -8.0) + + transition.updateFrame(node: self.labelNode, frame: labelFrame) + transition.updateFrame(node: self.textNode, frame: textFrame) + + let height = labelSize.height + 3.0 + textSize.height + 22.0 + + let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel + self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition) + transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset))) + + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel))) + transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0) + + return height + } + + private func linkItemAtPoint(_ point: CGPoint) -> TextLinkItem? { + let textNodeFrame = self.textNode.frame + if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + return .url(url) + } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { + return .mention(peerName) + } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { + return .hashtag(hashtag.peerName, hashtag.hashtag) + } else { + return nil + } + } + return nil + } + + private func updateTouchesAtPoint(_ point: CGPoint?) { + guard let item = self.item, let theme = self.theme else { + return + } + var rects: [CGRect]? + if let point = point { + let textNodeFrame = self.textNode.frame + if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + let possibleNames: [String] = [ + TelegramTextAttributes.URL, + TelegramTextAttributes.PeerMention, + TelegramTextAttributes.PeerTextMention, + TelegramTextAttributes.BotCommand, + TelegramTextAttributes.Hashtag + ] + for name in possibleNames { + if let _ = attributes[NSAttributedString.Key(rawValue: name)] { + rects = self.textNode.attributeRects(name: name, at: index) + break + } + } + } + } + + if let rects = rects { + let linkHighlightingNode: LinkHighlightingNode + if let current = self.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: theme.list.itemAccentColor.withAlphaComponent(0.5)) + self.linkHighlightingNode = linkHighlightingNode + self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode) + } + linkHighlightingNode.frame = self.textNode.frame + linkHighlightingNode.updateRects(rects) + } else if let linkHighlightingNode = self.linkHighlightingNode { + self.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + + if point != nil && rects == nil && item.action != nil { + self.selectionNode.updateIsHighlighted(true) + } else { + self.selectionNode.updateIsHighlighted(false) + } + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift new file mode 100644 index 0000000000..e593df1e4e --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift @@ -0,0 +1,213 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import ItemListPeerItem +import SwiftSignalKit +import AccountContext +import Postbox +import SyncCore +import TelegramCore +import ItemListUI + +enum PeerInfoScreenMemberItemAction { + case open + case promote + case restrict + case remove +} + +final class PeerInfoScreenMemberItem: PeerInfoScreenItem { + let id: AnyHashable + let context: AccountContext + let enclosingPeer: Peer + let member: PeerInfoMember + let action: ((PeerInfoScreenMemberItemAction) -> Void)? + + init( + id: AnyHashable, + context: AccountContext, + enclosingPeer: Peer, + member: PeerInfoMember, + action: ((PeerInfoScreenMemberItemAction) -> Void)? + ) { + self.id = id + self.context = context + self.enclosingPeer = enclosingPeer + self.member = member + self.action = action + } + + func node() -> PeerInfoScreenItemNode { + return PeerInfoScreenMemberItemNode() + } +} + +private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { + private let selectionNode: PeerInfoScreenSelectableBackgroundNode + private let bottomSeparatorNode: ASDisplayNode + + private var item: PeerInfoScreenMemberItem? + private var itemNode: ItemListPeerItemNode? + + override init() { + var bringToFrontForHighlightImpl: (() -> Void)? + self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() }) + self.selectionNode.isUserInteractionEnabled = false + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + + super.init() + + bringToFrontForHighlightImpl = { [weak self] in + self?.bringToFrontForHighlight?() + } + + self.addSubnode(self.bottomSeparatorNode) + self.addSubnode(self.selectionNode) + } + + override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { [weak self] point in + return .keepWithSingleTap + } + recognizer.highlight = { [weak self] point in + guard let strongSelf = self else { + return + } + strongSelf.updateTouchesAtPoint(point) + } + self.view.addGestureRecognizer(recognizer) + } + + @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + if let item = self.item { + item.action?(.open) + } + default: + break + } + } + default: + break + } + } + + override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat { + guard let item = item as? PeerInfoScreenMemberItem else { + return 10.0 + } + + self.item = item + + self.selectionNode.pressed = item.action.flatMap { action in + return { + action(.open) + } + } + + let sideInset: CGFloat = 16.0 + + self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + let label: String? + if let rank = item.member.rank { + label = rank + } else { + switch item.member.role { + case .creator: + label = presentationData.strings.GroupInfo_LabelOwner + case .admin: + label = presentationData.strings.GroupInfo_LabelAdmin + case .member: + label = nil + } + } + + let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.account.peerId, peer: item.enclosingPeer, member: item.member) + + var options: [ItemListPeerItemRevealOption] = [] + if actions.contains(.promote) && item.enclosingPeer is TelegramChannel { + options.append(ItemListPeerItemRevealOption(type: .neutral, title: presentationData.strings.GroupInfo_ActionPromote, action: { + item.action?(.promote) + })) + } + if actions.contains(.restrict) { + if item.enclosingPeer is TelegramChannel { + options.append(ItemListPeerItemRevealOption(type: .warning, title: presentationData.strings.GroupInfo_ActionRestrict, action: { + item.action?(.restrict) + })) + } + options.append(ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Common_Delete, action: { + item.action?(.remove) + })) + } + + let peerItem = ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: item.context, peer: item.member.peer, height: .peerList, presence: item.member.presence, text: .presence, label: label == nil ? .none : .text(label!, .standard), editing: ItemListPeerItemEditing(editable: !options.isEmpty, editing: false, revealed: nil), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, selectable: false, sectionId: 0, action: nil, setPeerIdWithRevealedOptions: { lhs, rhs in + + }, removePeer: { _ in + + }, contextAction: nil, hasTopStripe: false, hasTopGroupInset: false, noInsets: true, displayDecorations: false) + + let params = ListViewItemLayoutParams(width: width, leftInset: 0.0, rightInset: 0.0, availableHeight: 1000.0) + + let itemNode: ItemListPeerItemNode + if let current = self.itemNode { + itemNode = current + peerItem.updateNode(async: { $0() }, node: { + return itemNode + }, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: layout.size.height)) + + itemNode.contentSize = layout.contentSize + itemNode.insets = layout.insets + itemNode.frame = nodeFrame + + apply(ListViewItemApply(isOnScreen: true)) + }) + } else { + var itemNodeValue: ListViewItemNode? + peerItem.nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { node, apply in + itemNodeValue = node + apply().1(ListViewItemApply(isOnScreen: true)) + }) + itemNode = itemNodeValue as! ItemListPeerItemNode + self.itemNode = itemNode + self.addSubnode(itemNode) + } + + let height = itemNode.contentSize.height + + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: itemNode.bounds.size)) + + let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel + self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition) + transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset))) + + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel))) + transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0) + + return height + } + + private func updateTouchesAtPoint(_ point: CGPoint?) { + guard let item = self.item else { + return + } + if point != nil && item.context.account.peerId != item.member.id { + self.selectionNode.updateIsHighlighted(true) + } else { + self.selectionNode.updateIsHighlighted(false) + } + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenSelectableBackgroundNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenSelectableBackgroundNode.swift new file mode 100644 index 0000000000..a630c37287 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenSelectableBackgroundNode.swift @@ -0,0 +1,62 @@ +import AsyncDisplayKit +import Display +import TelegramPresentationData + +final class PeerInfoScreenSelectableBackgroundNode: ASDisplayNode { + private let backgroundNode: ASDisplayNode + private let buttonNode: HighlightTrackingButtonNode + + let bringToFrontForHighlight: () -> Void + + private var isHighlighted: Bool = false + + var pressed: (() -> Void)? { + didSet { + self.buttonNode.isUserInteractionEnabled = self.pressed != nil + } + } + + init(bringToFrontForHighlight: @escaping () -> Void) { + self.bringToFrontForHighlight = bringToFrontForHighlight + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.alpha = 0.0 + + self.buttonNode = HighlightTrackingButtonNode() + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.buttonNode.highligthedChanged = { [weak self] highlighted in + self?.updateIsHighlighted(highlighted) + } + } + + @objc private func buttonPressed() { + self.pressed?() + } + + func updateIsHighlighted(_ isHighlighted: Bool) { + if self.isHighlighted != isHighlighted { + self.isHighlighted = isHighlighted + if isHighlighted { + self.bringToFrontForHighlight() + self.backgroundNode.layer.removeAnimation(forKey: "opacity") + self.backgroundNode.alpha = 1.0 + } else { + self.backgroundNode.alpha = 0.0 + self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + } + } + } + + func update(size: CGSize, theme: PresentationTheme, transition: ContainedViewLayoutTransition) { + self.backgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(), size: size)) + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenSwitchItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenSwitchItem.swift new file mode 100644 index 0000000000..8a798af589 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenSwitchItem.swift @@ -0,0 +1,121 @@ +import AsyncDisplayKit +import Display +import TelegramPresentationData + +final class PeerInfoScreenSwitchItem: PeerInfoScreenItem { + let id: AnyHashable + let text: String + let value: Bool + let toggled: ((Bool) -> Void)? + + init(id: AnyHashable, text: String, value: Bool, toggled: ((Bool) -> Void)?) { + self.id = id + self.text = text + self.value = value + self.toggled = toggled + } + + func node() -> PeerInfoScreenItemNode { + return PeerInfoScreenSwitchItemNode() + } +} + +private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode { + private let selectionNode: PeerInfoScreenSelectableBackgroundNode + private let textNode: ImmediateTextNode + private let switchNode: SwitchNode + private let bottomSeparatorNode: ASDisplayNode + + private var item: PeerInfoScreenSwitchItem? + + private var theme: PresentationTheme? + + override init() { + var bringToFrontForHighlightImpl: (() -> Void)? + self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() }) + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.isUserInteractionEnabled = false + + self.switchNode = SwitchNode() + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + + super.init() + + bringToFrontForHighlightImpl = { [weak self] in + self?.bringToFrontForHighlight?() + } + + self.addSubnode(self.bottomSeparatorNode) + self.addSubnode(self.selectionNode) + self.addSubnode(self.textNode) + self.addSubnode(self.switchNode) + + self.switchNode.valueUpdated = { [weak self] value in + self?.item?.toggled?(value) + } + } + + override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat { + guard let item = item as? PeerInfoScreenSwitchItem else { + return 10.0 + } + + let firstTime = self.item == nil + + if self.theme !== presentationData.theme { + self.theme = presentationData.theme + + self.switchNode.frameColor = presentationData.theme.list.itemSwitchColors.frameColor + self.switchNode.contentColor = presentationData.theme.list.itemSwitchColors.contentColor + self.switchNode.handleColor = presentationData.theme.list.itemSwitchColors.handleColor + } + + self.item = item + + self.selectionNode.pressed = nil + + let sideInset: CGFloat = 16.0 + + self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + let textColorValue: UIColor = presentationData.theme.list.itemPrimaryTextColor + + self.textNode.maximumNumberOfLines = 1 + self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue) + + let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0 - 56.0, height: .greatestFiniteMagnitude)) + + let arrowInset: CGFloat = 18.0 + + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: textSize) + + let height = textSize.height + 22.0 + + transition.updateFrame(node: self.textNode, frame: textFrame) + + if let switchView = self.switchNode.view as? UISwitch { + if self.switchNode.bounds.size.width.isZero { + switchView.sizeToFit() + } + let switchSize = switchView.bounds.size + + self.switchNode.frame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0, y: floor((height - switchSize.height) / 2.0)), size: switchSize) + if switchView.isOn != item.value { + switchView.setOn(item.value, animated: !firstTime) + } + } + + let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel + self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition) + transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset))) + + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel))) + transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0) + + return height + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift new file mode 100644 index 0000000000..c0408f0821 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift @@ -0,0 +1,219 @@ +import AsyncDisplayKit +import Display +import TelegramCore +import SyncCore +import SwiftSignalKit +import Postbox +import TelegramPresentationData +import AccountContext +import ContextUI +import PhotoResources +import TelegramUIPreferences +import ItemListPeerItem +import MergeLists +import ItemListUI + +private struct GroupsInCommonListTransaction { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] +} + +private struct GroupsInCommonListEntry: Comparable, Identifiable { + var index: Int + var peer: Peer + + var stableId: PeerId { + return self.peer.id + } + + static func ==(lhs: GroupsInCommonListEntry, rhs: GroupsInCommonListEntry) -> Bool { + return lhs.peer.isEqual(rhs.peer) + } + + static func <(lhs: GroupsInCommonListEntry, rhs: GroupsInCommonListEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem { + let peer = self.peer + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: self.peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: { + openPeer(peer) + }, setPeerIdWithRevealedOptions: { _, _ in + }, removePeer: { _ in + }, contextAction: { node, gesture in + openPeerContextAction(peer, node, gesture) + }, hasTopStripe: false, noInsets: true) + } +} + +private func preparedTransition(from fromEntries: [GroupsInCommonListEntry], to toEntries: [GroupsInCommonListEntry], context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> GroupsInCommonListTransaction { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, openPeer: openPeer, openPeerContextAction: openPeerContextAction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, openPeer: openPeer, openPeerContextAction: openPeerContextAction), directionHint: nil) } + + return GroupsInCommonListTransaction(deletions: deletions, insertions: insertions, updates: updates) +} + +final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { + private let context: AccountContext + private let peerId: PeerId + private let chatControllerInteraction: ChatControllerInteraction + private let openPeerContextAction: (Peer, ASDisplayNode, ContextGesture?) -> Void + private let groupsInCommonContext: GroupsInCommonContext + + private let listNode: ListView + private var state: GroupsInCommonState? + private var currentEntries: [GroupsInCommonListEntry] = [] + private var enqueuedTransactions: [GroupsInCommonListTransaction] = [] + + private var currentParams: (size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData)? + + private let ready = Promise() + private var didSetReady: Bool = false + var isReady: Signal { + return self.ready.get() + } + + private var disposable: Disposable? + + init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, groupsInCommonContext: GroupsInCommonContext) { + self.context = context + self.peerId = peerId + self.chatControllerInteraction = chatControllerInteraction + self.openPeerContextAction = openPeerContextAction + self.groupsInCommonContext = groupsInCommonContext + + self.listNode = ListView() + + super.init() + + self.listNode.preloadPages = true + self.addSubnode(self.listNode) + + self.disposable = (groupsInCommonContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let strongSelf = self else { + return + } + strongSelf.state = state + if let (_, _, presentationData) = strongSelf.currentParams { + strongSelf.updatePeers(state: state, presentationData: presentationData) + } + }) + + self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in + guard let strongSelf = self, let state = strongSelf.state, case .ready(true) = state.dataState else { + return + } + if case let .known(value) = offset, value < 100.0, case .ready(true) = state.dataState { + strongSelf.groupsInCommonContext.loadMore() + } + } + } + + deinit { + self.disposable?.dispose() + } + + func scrollToTop() -> Bool { + if !self.listNode.scrollToOffsetFromTop(0.0) { + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: 0.4), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + return true + } else { + return false + } + } + + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + let isFirstLayout = self.currentParams == nil + self.currentParams = (size, isScrollingLockedAtTop, presentationData) + + transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + + var scrollToItem: ListViewScrollToItem? + if isScrollingLockedAtTop { + switch self.listNode.visibleContentOffset() { + case .known(0.0): + break + default: + scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: duration), directionHint: .Up) + } + } + + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + self.listNode.scrollEnabled = !isScrollingLockedAtTop + + if isFirstLayout, let state = self.state { + self.updatePeers(state: state, presentationData: presentationData) + } + } + + private func updatePeers(state: GroupsInCommonState, presentationData: PresentationData) { + var entries: [GroupsInCommonListEntry] = [] + for peer in state.peers { + if let peer = peer.peer { + entries.append(GroupsInCommonListEntry(index: entries.count, peer: peer)) + } + } + let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, openPeer: { [weak self] peer in + self?.chatControllerInteraction.openPeer(peer.id, .default, nil) + }, openPeerContextAction: { [weak self] peer, node, gesture in + self?.openPeerContextAction(peer, node, gesture) + }) + self.currentEntries = entries + self.enqueuedTransactions.append(transaction) + self.dequeueTransaction() + } + + private func dequeueTransaction() { + guard let (layout, _, _) = self.currentParams, let transaction = self.enqueuedTransactions.first else { + return + } + + self.enqueuedTransactions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + options.insert(.Synchronous) + + self.listNode.transaction(deleteIndices: transaction.deletions, insertIndicesAndItems: transaction.insertions, updateIndicesAndItems: transaction.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf.ready.set(.single(true)) + } + }) + } + + func findLoadedMessage(id: MessageId) -> Message? { + return nil + } + + func updateHiddenMedia() { + } + + func transferVelocity(_ velocity: CGFloat) { + if velocity > 0.0 { + self.listNode.transferVelocity(velocity) + } + } + + func cancelPreviewGestures() { + } + + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + return nil + } + + func addToTransitionSurface(view: UIView) { + } + + func updateSelectedMessages(animated: Bool) { + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoListPaneNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoListPaneNode.swift new file mode 100644 index 0000000000..2a2e4d2a76 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoListPaneNode.swift @@ -0,0 +1,136 @@ +import AsyncDisplayKit +import Display +import TelegramCore +import SyncCore +import SwiftSignalKit +import Postbox +import TelegramPresentationData +import AccountContext +import ContextUI +import PhotoResources +import TelegramUIPreferences + +final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { + private let context: AccountContext + private let peerId: PeerId + private let chatControllerInteraction: ChatControllerInteraction + + private let listNode: ChatHistoryListNode + + private var currentParams: (size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData)? + + private let ready = Promise() + private var didSetReady: Bool = false + var isReady: Signal { + return self.ready.get() + } + + private let selectedMessagesPromise = Promise?>(nil) + private var selectedMessages: Set? { + didSet { + if self.selectedMessages != oldValue { + self.selectedMessagesPromise.set(.single(self.selectedMessages)) + } + } + } + + private var hiddenMediaDisposable: Disposable? + + init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, tagMask: MessageTags) { + self.context = context + self.peerId = peerId + self.chatControllerInteraction = chatControllerInteraction + + self.selectedMessages = chatControllerInteraction.selectionState.flatMap { $0.selectedIds } + self.selectedMessagesPromise.set(.single(self.selectedMessages)) + + self.listNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: tagMask, subject: nil, controllerInteraction: chatControllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), mode: .list(search: false, reversed: false, displayHeaders: .allButLast)) + + super.init() + + self.listNode.preloadPages = true + self.addSubnode(self.listNode) + + self.ready.set(self.listNode.historyState.get() + |> take(1) + |> map { _ -> Bool in true }) + } + + deinit { + self.hiddenMediaDisposable?.dispose() + } + + func scrollToTop() -> Bool { + let offset = self.listNode.visibleContentOffset() + switch offset { + case let .known(value) where value <= CGFloat.ulpOfOne: + return false + default: + self.listNode.scrollToEndOfHistory() + return true + } + } + + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + self.currentParams = (size, isScrollingLockedAtTop, presentationData) + + transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + self.listNode.updateLayout(transition: transition, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve)) + if isScrollingLockedAtTop { + switch self.listNode.visibleContentOffset() { + case .known(0.0), .none: + break + default: + self.listNode.scrollToEndOfHistory() + } + } + self.listNode.scrollEnabled = !isScrollingLockedAtTop + } + + func findLoadedMessage(id: MessageId) -> Message? { + self.listNode.messageInCurrentHistoryView(id) + } + + func updateHiddenMedia() { + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ListMessageNode { + itemNode.updateHiddenMedia() + } + } + } + + func transferVelocity(_ velocity: CGFloat) { + if velocity > 0.0 { + self.listNode.transferVelocity(velocity) + } + } + + func cancelPreviewGestures() { + } + + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ListMessageNode { + if let result = itemNode.transitionNode(id: messageId, media: media) { + transitionNode = result + } + } + } + return transitionNode + } + + func addToTransitionSurface(view: UIView) { + self.view.addSubview(view) + } + + func updateSelectedMessages(animated: Bool) { + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + itemNode.updateSelectionState(animated: animated) + } + } + self.selectedMessages = self.chatControllerInteraction.selectionState.flatMap { $0.selectedIds } + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoMembersPane.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoMembersPane.swift new file mode 100644 index 0000000000..5bc764ed19 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoMembersPane.swift @@ -0,0 +1,262 @@ +import AsyncDisplayKit +import Display +import TelegramCore +import SyncCore +import SwiftSignalKit +import Postbox +import TelegramPresentationData +import AccountContext +import ContextUI +import PhotoResources +import TelegramUIPreferences +import ItemListPeerItem +import MergeLists +import ItemListUI + +private struct PeerMembersListTransaction { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let animated: Bool +} + +enum PeerMembersListAction { + case open + case promote + case restrict + case remove +} + +private struct PeerMembersListEntry: Comparable, Identifiable { + var index: Int + var member: PeerInfoMember + + var stableId: PeerId { + return self.member.id + } + + static func ==(lhs: PeerMembersListEntry, rhs: PeerMembersListEntry) -> Bool { + return lhs.member == rhs.member + } + + static func <(lhs: PeerMembersListEntry, rhs: PeerMembersListEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) -> ListViewItem { + let member = self.member + let label: String? + if let rank = member.rank { + label = rank + } else { + switch member.role { + case .creator: + label = presentationData.strings.GroupInfo_LabelOwner + case .admin: + label = presentationData.strings.GroupInfo_LabelAdmin + case .member: + label = nil + } + } + + let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: enclosingPeer, member: member) + + var options: [ItemListPeerItemRevealOption] = [] + if actions.contains(.promote) && enclosingPeer is TelegramChannel{ + options.append(ItemListPeerItemRevealOption(type: .neutral, title: presentationData.strings.GroupInfo_ActionPromote, action: { + action(member, .promote) + })) + } + if actions.contains(.restrict) { + if enclosingPeer is TelegramChannel { + options.append(ItemListPeerItemRevealOption(type: .warning, title: presentationData.strings.GroupInfo_ActionRestrict, action: { + action(member, .restrict) + })) + } + options.append(ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Common_Delete, action: { + action(member, .remove) + })) + } + + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: member.peer, presence: member.presence, text: .presence, label: label == nil ? .none : .text(label!, .standard), editing: ItemListPeerItemEditing(editable: !options.isEmpty, editing: false, revealed: false), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, selectable: member.id != context.account.peerId, sectionId: 0, action: { + action(member, .open) + }, setPeerIdWithRevealedOptions: { _, _ in + }, removePeer: { _ in + }, contextAction: nil/*{ node, gesture in + openPeerContextAction(peer, node, gesture) + }*/, hasTopStripe: false, noInsets: true) + } +} + +private func preparedTransition(from fromEntries: [PeerMembersListEntry], to toEntries: [PeerMembersListEntry], context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) -> PeerMembersListTransaction { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enclosingPeer: enclosingPeer, action: action), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enclosingPeer: enclosingPeer, action: action), directionHint: nil) } + + return PeerMembersListTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: toEntries.count < fromEntries.count) +} + +final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { + private let context: AccountContext + private let membersContext: PeerInfoMembersContext + private let action: (PeerInfoMember, PeerMembersListAction) -> Void + + private let listNode: ListView + private var currentEntries: [PeerMembersListEntry] = [] + private var enclosingPeer: Peer? + private var currentState: PeerInfoMembersState? + private var canLoadMore: Bool = false + private var enqueuedTransactions: [PeerMembersListTransaction] = [] + + private var currentParams: (size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData)? + + private let ready = Promise() + private var didSetReady: Bool = false + var isReady: Signal { + return self.ready.get() + } + + private var disposable: Disposable? + + init(context: AccountContext, peerId: PeerId, membersContext: PeerInfoMembersContext, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) { + self.context = context + self.membersContext = membersContext + self.action = action + + self.listNode = ListView() + + super.init() + + self.listNode.preloadPages = true + self.addSubnode(self.listNode) + + self.disposable = (combineLatest(queue: .mainQueue(), + membersContext.state, + context.account.postbox.combinedView(keys: [.basicPeer(peerId)]) + ) + |> deliverOnMainQueue).start(next: { [weak self] state, combinedView in + guard let strongSelf = self, let basicPeerView = combinedView.views[.basicPeer(peerId)] as? BasicPeerView, let enclosingPeer = basicPeerView.peer else { + return + } + + strongSelf.enclosingPeer = enclosingPeer + strongSelf.currentState = state + if let (_, _, presentationData) = strongSelf.currentParams { + strongSelf.updateState(enclosingPeer: enclosingPeer, state: state, presentationData: presentationData) + } + }) + + self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in + guard let strongSelf = self, let state = strongSelf.currentState, case .ready(true) = state.dataState else { + return + } + if case let .known(value) = offset, value < 100.0 { + strongSelf.membersContext.loadMore() + } + } + } + + deinit { + } + + func scrollToTop() -> Bool { + if !self.listNode.scrollToOffsetFromTop(0.0) { + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: 0.4), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + return true + } else { + return false + } + } + + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + let isFirstLayout = self.currentParams == nil + self.currentParams = (size, isScrollingLockedAtTop, presentationData) + + transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + + var scrollToItem: ListViewScrollToItem? + if isScrollingLockedAtTop { + switch self.listNode.visibleContentOffset() { + case .known(0.0): + break + default: + scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: duration), directionHint: .Up) + } + } + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + self.listNode.scrollEnabled = !isScrollingLockedAtTop + + if isFirstLayout, let enclosingPeer = self.enclosingPeer, let state = self.currentState { + self.updateState(enclosingPeer: enclosingPeer, state: state, presentationData: presentationData) + } + } + + private func updateState(enclosingPeer: Peer, state: PeerInfoMembersState, presentationData: PresentationData) { + var entries: [PeerMembersListEntry] = [] + for member in state.members { + entries.append(PeerMembersListEntry(index: entries.count, member: member)) + } + let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, enclosingPeer: enclosingPeer, action: { [weak self] member, action in + self?.action(member, action) + }) + self.enclosingPeer = enclosingPeer + self.currentEntries = entries + self.enqueuedTransactions.append(transaction) + self.dequeueTransaction() + } + + private func dequeueTransaction() { + guard let (layout, _, _) = self.currentParams, let transaction = self.enqueuedTransactions.first else { + return + } + + self.enqueuedTransactions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + if transaction.animated { + options.insert(.AnimateInsertion) + } else { + options.insert(.Synchronous) + } + + self.listNode.transaction(deleteIndices: transaction.deletions, insertIndicesAndItems: transaction.insertions, updateIndicesAndItems: transaction.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf.ready.set(.single(true)) + } + }) + } + + func findLoadedMessage(id: MessageId) -> Message? { + return nil + } + + func updateHiddenMedia() { + } + + func transferVelocity(_ velocity: CGFloat) { + if velocity > 0.0 { + self.listNode.transferVelocity(velocity) + } + } + + func cancelPreviewGestures() { + } + + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + return nil + } + + func addToTransitionSurface(view: UIView) { + } + + func updateSelectedMessages(animated: Bool) { + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift new file mode 100644 index 0000000000..cc2ff6f4ab --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift @@ -0,0 +1,872 @@ +import AsyncDisplayKit +import Display +import TelegramCore +import SyncCore +import SwiftSignalKit +import Postbox +import TelegramPresentationData +import AccountContext +import ContextUI +import PhotoResources +import RadialStatusNode +import TelegramStringFormatting +import GridMessageSelectionNode + +private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6) +private let mediaBadgeTextColor = UIColor.white + +private final class VisualMediaItemInteraction { + let openMessage: (Message) -> Void + let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void + let toggleSelection: (MessageId, Bool) -> Void + + var hiddenMedia: [MessageId: [Media]] = [:] + var selectedMessageIds: Set? + + init( + openMessage: @escaping (Message) -> Void, + openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, + toggleSelection: @escaping (MessageId, Bool) -> Void + ) { + self.openMessage = openMessage + self.openMessageContextActions = openMessageContextActions + self.toggleSelection = toggleSelection + } +} + +private final class VisualMediaItemNode: ASDisplayNode { + private let context: AccountContext + private let interaction: VisualMediaItemInteraction + + private let containerNode: ContextControllerSourceNode + private let imageNode: TransformImageNode + private var statusNode: RadialStatusNode + private let mediaBadgeNode: ChatMessageInteractiveMediaBadge + private var selectionNode: GridMessageSelectionNode? + + private let fetchStatusDisposable = MetaDisposable() + private let fetchDisposable = MetaDisposable() + private var resourceStatus: MediaResourceStatus? + + private var item: (VisualMediaItem, Media?, CGSize, CGSize?)? + private var theme: PresentationTheme? + + init(context: AccountContext, interaction: VisualMediaItemInteraction) { + self.context = context + self.interaction = interaction + + self.containerNode = ContextControllerSourceNode() + self.imageNode = TransformImageNode() + self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6)) + let progressDiameter: CGFloat = 40.0 + self.statusNode.frame = CGRect(x: 0.0, y: 0.0, width: progressDiameter, height: progressDiameter) + self.statusNode.isUserInteractionEnabled = false + + self.mediaBadgeNode = ChatMessageInteractiveMediaBadge() + self.mediaBadgeNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 50.0, height: 50.0)) + + super.init() + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.imageNode) + self.containerNode.addSubnode(self.mediaBadgeNode) + + self.containerNode.activated = { [weak self] gesture in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + strongSelf.interaction.openMessageContextActions(item.0.message, strongSelf.containerNode, strongSelf.containerNode.bounds, gesture) + } + } + + deinit { + self.fetchStatusDisposable.dispose() + self.fetchDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + recognizer.tapActionAtPoint = { _ in + return .waitForSingleTap + } + self.imageNode.view.addGestureRecognizer(recognizer) + + self.mediaBadgeNode.pressed = { [weak self] in + self?.progressPressed() + } + } + + @objc func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + if case .ended = recognizer.state { + if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { + if case .tap = gesture { + if let (item, _, _, _) = self.item { + var media: Media? + for value in item.message.media { + if let image = value as? TelegramMediaImage { + media = image + break + } else if let file = value as? TelegramMediaFile { + media = file + break + } + } + + if let media = media { + if let file = media as? TelegramMediaFile { + if isMediaStreamable(message: item.message, media: file) { + self.interaction.openMessage(item.message) + } else { + self.progressPressed() + } + } else { + self.interaction.openMessage(item.message) + } + } + } + } + } + } + } + + private func progressPressed() { + guard let message = self.item?.0.message else { + return + } + + var media: Media? + for value in message.media { + if let image = value as? TelegramMediaImage { + media = image + break + } else if let file = value as? TelegramMediaFile { + media = file + break + } + } + + if let resourceStatus = self.resourceStatus, let file = media as? TelegramMediaFile { + switch resourceStatus { + case .Fetching: + messageMediaFileCancelInteractiveFetch(context: self.context, messageId: message.id, file: file) + case .Local: + self.interaction.openMessage(message) + case .Remote: + self.fetchDisposable.set(messageMediaFileInteractiveFetched(context: self.context, message: message, file: file, userInitiated: true).start()) + } + } + } + + func cancelPreviewGesture() { + self.containerNode.cancelGesture() + } + + func update(size: CGSize, item: VisualMediaItem, theme: PresentationTheme, synchronousLoad: Bool) { + if item === self.item?.0 && size == self.item?.2 { + return + } + self.theme = theme + var media: Media? + for value in item.message.media { + if let image = value as? TelegramMediaImage { + media = image + break + } else if let file = value as? TelegramMediaFile { + media = file + break + } + } + + if let media = media, (self.item?.1 == nil || !media.isEqual(to: self.item!.1!)) { + var mediaDimensions: CGSize? + if let image = media as? TelegramMediaImage, let largestSize = largestImageRepresentation(image.representations)?.dimensions { + mediaDimensions = largestSize.cgSize + + self.imageNode.setSignal(mediaGridMessagePhoto(account: context.account, photoReference: .message(message: MessageReference(item.message), media: image), fullRepresentationSize: CGSize(width: 300.0, height: 300.0), synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad, dispatchOnDisplayLink: true) + + self.fetchStatusDisposable.set(nil) + self.statusNode.transitionToState(.none, completion: { [weak self] in + self?.statusNode.isHidden = true + }) + self.mediaBadgeNode.isHidden = true + self.resourceStatus = nil + } else if let file = media as? TelegramMediaFile, file.isVideo { + mediaDimensions = file.dimensions?.cgSize + self.imageNode.setSignal(mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .message(message: MessageReference(item.message), media: file), synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: true), attemptSynchronously: synchronousLoad) + + self.mediaBadgeNode.isHidden = false + + self.resourceStatus = nil + + self.item = (item, media, size, mediaDimensions) + + self.fetchStatusDisposable.set((messageMediaFileStatus(context: context, messageId: item.message.id, file: file) + |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self, let (item, _, _, _) = strongSelf.item { + strongSelf.resourceStatus = status + + let isStreamable = isMediaStreamable(message: item.message, media: file) + + var statusState: RadialStatusNodeState = .none + if isStreamable { + statusState = .none + } else { + switch status { + case let .Fetching(_, progress): + let adjustedProgress = max(progress, 0.027) + statusState = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true) + case .Local: + statusState = .none + case .Remote: + statusState = .download(.white) + } + } + + switch statusState { + case .none: + break + default: + strongSelf.statusNode.isHidden = false + } + + strongSelf.statusNode.transitionToState(statusState, animated: true, completion: { + if let strongSelf = self { + if case .none = statusState { + strongSelf.statusNode.isHidden = true + } + } + }) + + if let duration = file.duration { + let durationString = stringForDuration(duration) + + var badgeContent: ChatMessageInteractiveMediaBadgeContent? + var mediaDownloadState: ChatMessageInteractiveMediaDownloadState? + + if isStreamable { + switch status { + case let .Fetching(_, progress): + let progressString = String(format: "%d%%", Int(progress * 100.0)) + badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString)) + mediaDownloadState = .compactFetching(progress: 0.0) + case .Local: + badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString)) + case .Remote: + badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString)) + mediaDownloadState = .compactRemote + } + } else { + badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString)) + } + + strongSelf.mediaBadgeNode.update(theme: nil, content: badgeContent, mediaDownloadState: mediaDownloadState, alignment: .right, animated: false, badgeAnimated: false) + } + } + })) + if self.statusNode.supernode == nil { + self.imageNode.addSubnode(self.statusNode) + } + } else { + self.mediaBadgeNode.isHidden = true + } + self.item = (item, media, size, mediaDimensions) + + let progressDiameter: CGFloat = 40.0 + self.statusNode.frame = CGRect(origin: CGPoint(x: floor((size.width - progressDiameter) / 2.0), y: floor((size.height - progressDiameter) / 2.0)), size: CGSize(width: progressDiameter, height: progressDiameter)) + + self.mediaBadgeNode.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 18.0 - 3.0), size: CGSize(width: 50.0, height: 50.0)) + + self.selectionNode?.frame = CGRect(origin: CGPoint(), size: size) + + self.updateHiddenMedia() + } + + if let (item, media, _, mediaDimensions) = self.item { + self.item = (item, media, size, mediaDimensions) + + let imageFrame = CGRect(origin: CGPoint(), size: size) + + self.containerNode.frame = imageFrame + self.imageNode.frame = imageFrame + + if let mediaDimensions = mediaDimensions { + let imageSize = mediaDimensions.aspectFilled(imageFrame.size) + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets(), emptyColor: theme.list.mediaPlaceholderColor))() + } + + self.updateSelectionState(animated: false) + } + } + + func updateSelectionState(animated: Bool) { + if let (item, media, _, mediaDimensions) = self.item, let theme = self.theme { + self.containerNode.isGestureEnabled = self.interaction.selectedMessageIds == nil + + if let selectedIds = self.interaction.selectedMessageIds { + let selected = selectedIds.contains(item.message.id) + + if let selectionNode = self.selectionNode { + selectionNode.updateSelected(selected, animated: animated) + selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) + } else { + let selectionNode = GridMessageSelectionNode(theme: theme, toggle: { [weak self] value in + if let strongSelf = self, let messageId = strongSelf.item?.0.message.id { + var toggledValue = true + if let selectedMessageIds = strongSelf.interaction.selectedMessageIds, selectedMessageIds.contains(messageId) { + toggledValue = false + } + strongSelf.interaction.toggleSelection(messageId, toggledValue) + } + }) + + selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) + self.containerNode.addSubnode(selectionNode) + self.selectionNode = selectionNode + selectionNode.updateSelected(selected, animated: false) + if animated { + selectionNode.animateIn() + } + } + } else { + if let selectionNode = self.selectionNode { + self.selectionNode = nil + if animated { + selectionNode.animateOut { [weak selectionNode] in + selectionNode?.removeFromSupernode() + } + } else { + selectionNode.removeFromSupernode() + } + } + } + } + } + + func transitionNode() -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + let imageNode = self.imageNode + return (self.imageNode, self.imageNode.bounds, { [weak self, weak imageNode] in + var statusNodeHidden = false + var accessoryHidden = false + if let strongSelf = self { + statusNodeHidden = strongSelf.statusNode.isHidden + accessoryHidden = strongSelf.mediaBadgeNode.isHidden + strongSelf.statusNode.isHidden = true + strongSelf.mediaBadgeNode.isHidden = true + } + let view = imageNode?.view.snapshotContentTree(unhide: true) + if let strongSelf = self { + strongSelf.statusNode.isHidden = statusNodeHidden + strongSelf.mediaBadgeNode.isHidden = accessoryHidden + } + return (view, nil) + }) + } + + func updateHiddenMedia() { + if let (item, _, _, _) = self.item { + if let _ = self.interaction.hiddenMedia[item.message.id] { + self.isHidden = true + } else { + self.isHidden = false + } + } else { + self.isHidden = false + } + } +} + +private final class VisualMediaItem { + let message: Message + + init(message: Message) { + self.message = message + } +} + +private final class FloatingHeaderNode: ASDisplayNode { + private let backgroundNode: ASImageNode + private let labelNode: ImmediateTextNode + + private var currentParams: (constrainedWidth: CGFloat, year: Int32, month: Int32, theme: PresentationTheme)? + private var currentSize: CGSize? + + override init() { + self.backgroundNode = ASImageNode() + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + + self.labelNode = ImmediateTextNode() + self.labelNode.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.labelNode) + } + + func update(constrainedWidth: CGFloat, year: Int32, month: Int32, theme: PresentationTheme, strings: PresentationStrings) -> CGSize { + if let currentParams = self.currentParams, let currentSize = self.currentSize { + if currentParams.constrainedWidth == constrainedWidth && + currentParams.year == year && + currentParams.month == month && + currentParams.theme === theme { + return currentSize + } + } + + if self.currentParams?.theme !== theme { + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 27.0, color: mediaBadgeBackgroundColor) + } + + self.currentParams = (constrainedWidth, year, month, theme) + + self.labelNode.attributedText = NSAttributedString(string: stringForMonth(strings: strings, month: month, ofYear: year), font: Font.regular(14.0), textColor: .white) + let labelSize = self.labelNode.updateLayout(CGSize(width: constrainedWidth, height: .greatestFiniteMagnitude)) + + let sideInset: CGFloat = 10.0 + self.labelNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floor((27.0 - labelSize.height) / 2.0)), size: labelSize) + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: labelSize.width + sideInset * 2.0, height: 27.0)) + + let size = CGSize(width: labelSize.width + sideInset * 2.0, height: 27.0) + return size + } +} + +final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate { + private let context: AccountContext + private let peerId: PeerId + private let chatControllerInteraction: ChatControllerInteraction + + private let scrollNode: ASScrollNode + private let floatingHeaderNode: FloatingHeaderNode + private var flashHeaderDelayTimer: Foundation.Timer? + private var isDeceleratingAfterTracking = false + + private var _itemInteraction: VisualMediaItemInteraction? + private var itemInteraction: VisualMediaItemInteraction { + return self._itemInteraction! + } + + private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData)? + + private let ready = Promise() + private var didSetReady: Bool = false + var isReady: Signal { + return self.ready.get() + } + + private let listDisposable = MetaDisposable() + private var hiddenMediaDisposable: Disposable? + private var mediaItems: [VisualMediaItem] = [] + private var visibleMediaItems: [UInt32: VisualMediaItemNode] = [:] + + private var numberOfItemsToRequest: Int = 50 + private var currentView: MessageHistoryView? + private var isRequestingView: Bool = false + private var isFirstHistoryView: Bool = true + + private var decelerationAnimator: ConstantDisplayLinkAnimator? + + init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId) { + self.context = context + self.peerId = peerId + self.chatControllerInteraction = chatControllerInteraction + + self.scrollNode = ASScrollNode() + self.floatingHeaderNode = FloatingHeaderNode() + self.floatingHeaderNode.alpha = 0.0 + + super.init() + + self._itemInteraction = VisualMediaItemInteraction( + openMessage: { [weak self] message in + self?.chatControllerInteraction.openMessage(message, .default) + }, + openMessageContextActions: { [weak self] message, sourceNode, sourceRect, gesture in + self?.chatControllerInteraction.openMessageContextActions(message, sourceNode, sourceRect, gesture) + }, + toggleSelection: { [weak self] id, value in + self?.chatControllerInteraction.toggleMessagesSelection([id], value) + } + ) + self.itemInteraction.selectedMessageIds = chatControllerInteraction.selectionState.flatMap { $0.selectedIds } + + self.scrollNode.view.delaysContentTouches = false + self.scrollNode.view.canCancelContentTouches = true + self.scrollNode.view.showsVerticalScrollIndicator = false + if #available(iOS 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + self.scrollNode.view.scrollsToTop = false + self.scrollNode.view.delegate = self + + self.addSubnode(self.scrollNode) + self.addSubnode(self.floatingHeaderNode) + + self.requestHistoryAroundVisiblePosition() + + self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in + guard let strongSelf = self else { + return + } + var hiddenMedia: [MessageId: [Media]] = [:] + for id in ids { + if case let .chat(accountId, messageId, media) = id, accountId == strongSelf.context.account.id { + hiddenMedia[messageId] = [media] + } + } + strongSelf.itemInteraction.hiddenMedia = hiddenMedia + for (_, itemNode) in strongSelf.visibleMediaItems { + itemNode.updateHiddenMedia() + } + }) + } + + deinit { + self.listDisposable.dispose() + self.hiddenMediaDisposable?.dispose() + } + + private func requestHistoryAroundVisiblePosition() { + if self.isRequestingView { + return + } + self.isRequestingView = true + self.listDisposable.set((self.context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(self.peerId), index: .upperBound, anchorIndex: .upperBound, count: self.numberOfItemsToRequest, fixedCombinedReadStates: nil, tagMask: .photoOrVideo) + |> deliverOnMainQueue).start(next: { [weak self] (view, updateType, _) in + guard let strongSelf = self else { + return + } + strongSelf.updateHistory(view: view, updateType: updateType) + strongSelf.isRequestingView = false + })) + } + + private func updateHistory(view: MessageHistoryView, updateType: ViewUpdateType) { + self.currentView = view + + switch updateType { + case .FillHole: + self.requestHistoryAroundVisiblePosition() + default: + self.mediaItems.removeAll() + for entry in view.entries.reversed() { + self.mediaItems.append(VisualMediaItem(message: entry.message)) + } + + let wasFirstHistoryView = self.isFirstHistoryView + self.isFirstHistoryView = false + + if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData) = self.currentParams { + self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: wasFirstHistoryView, transition: .immediate) + if !self.didSetReady { + self.didSetReady = true + self.ready.set(.single(true)) + } + } + } + } + + func scrollToTop() -> Bool { + if self.scrollNode.view.contentOffset.y > 0.0 { + self.scrollNode.view.setContentOffset(CGPoint(), animated: true) + return true + } else { + return false + } + } + + func findLoadedMessage(id: MessageId) -> Message? { + for item in self.mediaItems { + if item.message.id == id { + return item.message + } + } + return nil + } + + func updateHiddenMedia() { + for (_, itemNode) in self.visibleMediaItems { + itemNode.updateHiddenMedia() + } + } + + func transferVelocity(_ velocity: CGFloat) { + if velocity > 0.0 { + //print("transferVelocity \(velocity)") + self.decelerationAnimator?.isPaused = true + let startTime = CACurrentMediaTime() + var currentOffset = self.scrollNode.view.contentOffset + let decelerationRate: CGFloat = 0.998 + self.scrollViewDidEndDragging(self.scrollNode.view, willDecelerate: true) + self.decelerationAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in + guard let strongSelf = self else { + return + } + let t = CACurrentMediaTime() - startTime + var currentVelocity = velocity * 15.0 * CGFloat(pow(Double(decelerationRate), 1000.0 * t)) + //print("value at \(t) = \(currentVelocity)") + currentOffset.y += currentVelocity + let maxOffset = strongSelf.scrollNode.view.contentSize.height - strongSelf.scrollNode.bounds.height + if currentOffset.y >= maxOffset { + currentOffset.y = maxOffset + currentVelocity = 0.0 + } + if currentOffset.y < 0.0 { + currentOffset.y = 0.0 + currentVelocity = 0.0 + } + + var didEnd = false + if abs(currentVelocity) < 0.1 { + strongSelf.decelerationAnimator?.isPaused = true + strongSelf.decelerationAnimator = nil + didEnd = true + } + var contentOffset = strongSelf.scrollNode.view.contentOffset + contentOffset.y = floorToScreenPixels(currentOffset.y) + strongSelf.scrollNode.view.setContentOffset(contentOffset, animated: false) + strongSelf.scrollViewDidScroll(strongSelf.scrollNode.view) + if didEnd { + strongSelf.scrollViewDidEndDecelerating(strongSelf.scrollNode.view) + } + }) + self.decelerationAnimator?.isPaused = false + } + } + + func cancelPreviewGestures() { + for (_, itemNode) in self.visibleMediaItems { + itemNode.cancelPreviewGesture() + } + } + + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + for item in self.mediaItems { + if item.message.id == messageId { + if let itemNode = self.visibleMediaItems[item.message.stableId] { + return itemNode.transitionNode() + } + break + } + } + return nil + } + + func addToTransitionSurface(view: UIView) { + self.scrollNode.view.addSubview(view) + } + + func updateSelectedMessages(animated: Bool) { + self.itemInteraction.selectedMessageIds = self.chatControllerInteraction.selectionState.flatMap { $0.selectedIds } + for (_, itemNode) in self.visibleMediaItems { + itemNode.updateSelectionState(animated: animated) + } + } + + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + self.currentParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData) + + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size)) + + let availableWidth = size.width - sideInset * 2.0 + + let itemSpacing: CGFloat = 1.0 + let itemsInRow: Int = max(3, min(6, Int(availableWidth / 140.0))) + let itemSize: CGFloat = floor(availableWidth / CGFloat(itemsInRow)) + + let rowCount: Int = self.mediaItems.count / itemsInRow + (self.mediaItems.count % itemsInRow == 0 ? 0 : 1) + + let contentHeight = CGFloat(rowCount + 1) * itemSpacing + CGFloat(rowCount) * itemSize + bottomInset + + self.scrollNode.view.contentSize = CGSize(width: size.width, height: contentHeight) + self.updateVisibleItems(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, theme: presentationData.theme, strings: presentationData.strings, synchronousLoad: synchronous) + + if isScrollingLockedAtTop { + if self.scrollNode.view.contentOffset.y > .ulpOfOne { + transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)) + } + } + self.scrollNode.view.isScrollEnabled = !isScrollingLockedAtTop + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.decelerationAnimator?.isPaused = true + self.decelerationAnimator = nil + + for (_, itemNode) in self.visibleMediaItems { + itemNode.cancelPreviewGesture() + } + + self.updateHeaderFlashing(animated: true) + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if let (size, sideInset, bottomInset, visibleHeight, _, presentationData) = self.currentParams { + self.updateVisibleItems(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, theme: presentationData.theme, strings: presentationData.strings, synchronousLoad: false) + + if scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.bounds.height * 2.0, let currentView = self.currentView, currentView.earlierId != nil { + if !self.isRequestingView { + self.numberOfItemsToRequest += 50 + self.requestHistoryAroundVisiblePosition() + } + } + } + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if decelerate { + self.isDeceleratingAfterTracking = true + self.updateHeaderFlashing(animated: true) + } else { + self.isDeceleratingAfterTracking = false + self.resetHeaderFlashTimer(start: true) + self.updateHeaderFlashing(animated: true) + } + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.isDeceleratingAfterTracking = false + self.resetHeaderFlashTimer(start: true) + self.updateHeaderFlashing(animated: true) + } + + private func updateVisibleItems(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, theme: PresentationTheme, strings: PresentationStrings, synchronousLoad: Bool) { + let availableWidth = size.width - sideInset * 2.0 + + let itemSpacing: CGFloat = 1.0 + let itemsInRow: Int = max(3, min(6, Int(availableWidth / 140.0))) + let itemSize: CGFloat = floor(availableWidth / CGFloat(itemsInRow)) + + let rowCount: Int = self.mediaItems.count / itemsInRow + (self.mediaItems.count % itemsInRow == 0 ? 0 : 1) + + let headerItemMinY = self.scrollNode.view.bounds.minY + 20.0 + let visibleRect = self.scrollNode.view.bounds.insetBy(dx: 0.0, dy: -400.0) + var minVisibleRow = Int(floor((visibleRect.minY - itemSpacing) / (itemSize + itemSpacing))) + minVisibleRow = max(0, minVisibleRow) + var maxVisibleRow = Int(ceil((visibleRect.maxY - itemSpacing) / (itemSize + itemSpacing))) + maxVisibleRow = min(rowCount - 1, maxVisibleRow) + + let minVisibleIndex = minVisibleRow * itemsInRow + let maxVisibleIndex = min(self.mediaItems.count - 1, (maxVisibleRow + 1) * itemsInRow - 1) + + var headerItem: Message? + + var validIds = Set() + if minVisibleIndex <= maxVisibleIndex { + for i in minVisibleIndex ... maxVisibleIndex { + let stableId = self.mediaItems[i].message.stableId + validIds.insert(stableId) + let rowIndex = i / Int(itemsInRow) + let columnIndex = i % Int(itemsInRow) + let itemOrigin = CGPoint(x: sideInset + CGFloat(columnIndex) * (itemSize + itemSpacing), y: itemSpacing + CGFloat(rowIndex) * (itemSize + itemSpacing)) + let itemFrame = CGRect(origin: itemOrigin, size: CGSize(width: columnIndex == itemsInRow ? (availableWidth - itemOrigin.x) : itemSize, height: itemSize)) + let itemNode: VisualMediaItemNode + if let current = self.visibleMediaItems[stableId] { + itemNode = current + } else { + itemNode = VisualMediaItemNode(context: self.context, interaction: self.itemInteraction) + self.visibleMediaItems[stableId] = itemNode + self.scrollNode.addSubnode(itemNode) + } + itemNode.frame = itemFrame + if headerItem == nil && itemFrame.maxY > headerItemMinY { + headerItem = self.mediaItems[i].message + } + var itemSynchronousLoad = false + if itemFrame.maxY <= visibleHeight { + itemSynchronousLoad = synchronousLoad + } + itemNode.update(size: itemFrame.size, item: self.mediaItems[i], theme: theme, synchronousLoad: itemSynchronousLoad) + } + } + var removeKeys: [UInt32] = [] + for (id, _) in self.visibleMediaItems { + if !validIds.contains(id) { + removeKeys.append(id) + } + } + for id in removeKeys { + if let itemNode = self.visibleMediaItems.removeValue(forKey: id) { + itemNode.removeFromSupernode() + } + } + + if let headerItem = headerItem { + let (year, month) = listMessageDateHeaderInfo(timestamp: headerItem.timestamp) + let headerSize = self.floatingHeaderNode.update(constrainedWidth: size.width, year: year, month: month, theme: theme, strings: strings) + self.floatingHeaderNode.frame = CGRect(origin: CGPoint(x: floor((size.width - headerSize.width) / 2.0), y: 7.0), size: headerSize) + self.floatingHeaderNode.isHidden = false + } else { + self.floatingHeaderNode.isHidden = true + } + } + + private func resetHeaderFlashTimer(start: Bool, duration: Double = 0.3) { + if let flashHeaderDelayTimer = self.flashHeaderDelayTimer { + flashHeaderDelayTimer.invalidate() + self.flashHeaderDelayTimer = nil + } + + if start { + final class TimerProxy: NSObject { + private let action: () -> () + + init(_ action: @escaping () -> ()) { + self.action = action + super.init() + } + + @objc func timerEvent() { + self.action() + } + } + + let timer = Timer(timeInterval: duration, target: TimerProxy { [weak self] in + if let strongSelf = self { + if let flashHeaderDelayTimer = strongSelf.flashHeaderDelayTimer { + flashHeaderDelayTimer.invalidate() + strongSelf.flashHeaderDelayTimer = nil + strongSelf.updateHeaderFlashing(animated: true) + } + } + }, selector: #selector(TimerProxy.timerEvent), userInfo: nil, repeats: false) + self.flashHeaderDelayTimer = timer + RunLoop.main.add(timer, forMode: RunLoop.Mode.common) + self.updateHeaderFlashing(animated: true) + } + } + + private func headerIsFlashing() -> Bool { + return self.scrollNode.view.isDragging || self.isDeceleratingAfterTracking || self.flashHeaderDelayTimer != nil + } + + private func updateHeaderFlashing(animated: Bool) { + let flashing = self.headerIsFlashing() + let alpha: CGFloat = flashing ? 1.0 : 0.0 + let previousAlpha = self.floatingHeaderNode.alpha + + if !previousAlpha.isEqual(to: alpha) { + self.floatingHeaderNode.alpha = alpha + if animated { + let duration: Double = flashing ? 0.3 : 0.4 + self.floatingHeaderNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration) + } + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + if self.decelerationAnimator != nil { + self.decelerationAnimator?.isPaused = true + self.decelerationAnimator = nil + + return self.scrollNode.view + } + return result + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoData.swift new file mode 100644 index 0000000000..88d2d9c5a7 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoData.swift @@ -0,0 +1,791 @@ +import Foundation +import UIKit +import Postbox +import SyncCore +import TelegramCore +import SwiftSignalKit +import AccountContext +import PeerPresenceStatusManager +import TelegramStringFormatting +import TelegramPresentationData +import PeerAvatarGalleryUI + +enum PeerInfoUpdatingAvatar { + case none + case image(TelegramMediaImageRepresentation) +} + +final class PeerInfoState { + let isEditing: Bool + let selectedMessageIds: Set? + let updatingAvatar: PeerInfoUpdatingAvatar? + + init( + isEditing: Bool, + selectedMessageIds: Set?, + updatingAvatar: PeerInfoUpdatingAvatar? + ) { + self.isEditing = isEditing + self.selectedMessageIds = selectedMessageIds + self.updatingAvatar = updatingAvatar + } + + func withIsEditing(_ isEditing: Bool) -> PeerInfoState { + return PeerInfoState( + isEditing: isEditing, + selectedMessageIds: self.selectedMessageIds, + updatingAvatar: self.updatingAvatar + ) + } + + func withSelectedMessageIds(_ selectedMessageIds: Set?) -> PeerInfoState { + return PeerInfoState( + isEditing: self.isEditing, + selectedMessageIds: selectedMessageIds, + updatingAvatar: self.updatingAvatar + ) + } + + func withUpdatingAvatar(_ updatingAvatar: PeerInfoUpdatingAvatar?) -> PeerInfoState { + return PeerInfoState( + isEditing: self.isEditing, + selectedMessageIds: self.selectedMessageIds, + updatingAvatar: updatingAvatar + ) + } +} + +final class PeerInfoScreenData { + let peer: Peer? + let cachedData: CachedPeerData? + let status: PeerInfoStatusData? + let notificationSettings: TelegramPeerNotificationSettings? + let globalNotificationSettings: GlobalNotificationSettings? + let isContact: Bool + let availablePanes: [PeerInfoPaneKey] + let groupsInCommon: GroupsInCommonContext? + let linkedDiscussionPeer: Peer? + let members: PeerInfoMembersData? + let encryptionKeyFingerprint: SecretChatKeyFingerprint? + + init( + peer: Peer?, + cachedData: CachedPeerData?, + status: PeerInfoStatusData?, + notificationSettings: TelegramPeerNotificationSettings?, + globalNotificationSettings: GlobalNotificationSettings?, + isContact: Bool, + availablePanes: [PeerInfoPaneKey], + groupsInCommon: GroupsInCommonContext?, + linkedDiscussionPeer: Peer?, + members: PeerInfoMembersData?, + encryptionKeyFingerprint: SecretChatKeyFingerprint? + ) { + self.peer = peer + self.cachedData = cachedData + self.status = status + self.notificationSettings = notificationSettings + self.globalNotificationSettings = globalNotificationSettings + self.isContact = isContact + self.availablePanes = availablePanes + self.groupsInCommon = groupsInCommon + self.linkedDiscussionPeer = linkedDiscussionPeer + self.members = members + self.encryptionKeyFingerprint = encryptionKeyFingerprint + } +} + +private enum PeerInfoScreenInputUserKind { + case user + case bot + case support +} + +private enum PeerInfoScreenInputData: Equatable { + case none + case user(userId: PeerId, secretChatId: PeerId?, kind: PeerInfoScreenInputUserKind) + case channel + case group(groupId: PeerId) +} + +private func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId) -> Signal<[PeerInfoPaneKey]?, NoError> { + let tags: [(MessageTags, PeerInfoPaneKey)] = [ + (.photoOrVideo, .media), + (.file, .files), + (.music, .music), + //(.voiceOrInstantVideo, .voice), + (.webPage, .links) + ] + enum PaneState { + case loading + case empty + case present + } + let loadedOnce = Atomic(value: false) + return combineLatest(queue: .mainQueue(), tags.map { tagAndKey -> Signal<(PeerInfoPaneKey, PaneState), NoError> in + let (tag, key) = tagAndKey + return context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId), index: .upperBound, anchorIndex: .upperBound, count: 20, clipHoles: false, fixedCombinedReadStates: nil, tagMask: tag) + |> map { (view, _, _) -> (PeerInfoPaneKey, PaneState) in + if view.entries.isEmpty { + if view.isLoading { + return (key, .loading) + } else { + return (key, .empty) + } + } else { + return (key, .present) + } + } + }) + |> map { keysAndStates -> [PeerInfoPaneKey]? in + let loadedOnceValue = loadedOnce.with { $0 } + var result: [PeerInfoPaneKey] = [] + var hasNonLoaded = false + for (key, state) in keysAndStates { + switch state { + case .present: + result.append(key) + case .empty: + break + case .loading: + hasNonLoaded = true + } + } + if !hasNonLoaded || loadedOnceValue { + if !loadedOnceValue { + let _ = loadedOnce.swap(true) + } + return result + } else { + return nil + } + } + |> distinctUntilChanged +} + +struct PeerInfoStatusData: Equatable { + var text: String + var isActivity: Bool +} + +enum PeerInfoMembersData: Equatable { + case shortList(membersContext: PeerInfoMembersContext, members: [PeerInfoMember]) + case longList(PeerInfoMembersContext) + + var membersContext: PeerInfoMembersContext { + switch self { + case let .shortList(shortList): + return shortList.membersContext + case let .longList(membersContext): + return membersContext + } + } +} + +private func peerInfoScreenInputData(context: AccountContext, peerId: PeerId) -> Signal { + return context.account.postbox.combinedView(keys: [.basicPeer(peerId)]) + |> map { view -> PeerInfoScreenInputData in + guard let peer = (view.views[.basicPeer(peerId)] as? BasicPeerView)?.peer else { + return .none + } + if let user = peer as? TelegramUser { + let kind: PeerInfoScreenInputUserKind + if user.flags.contains(.isSupport) { + kind = .support + } else if user.botInfo != nil { + kind = .bot + } else { + kind = .user + } + return .user(userId: user.id, secretChatId: nil, kind: kind) + } else if let channel = peer as? TelegramChannel { + if case .group = channel.info { + return .group(groupId: channel.id) + } else { + return .channel + } + } else if let group = peer as? TelegramGroup { + return .group(groupId: group.id) + } else if let secretChat = peer as? TelegramSecretChat { + return .user(userId: secretChat.regularPeerId, secretChatId: peer.id, kind: .user) + } else { + return .none + } + } + |> distinctUntilChanged +} + +private func peerInfoProfilePhotos(context: AccountContext, peerId: PeerId) -> Signal { + return context.account.postbox.combinedView(keys: [.basicPeer(peerId)]) + |> map { view -> AvatarGalleryEntry? in + guard let peer = (view.views[.basicPeer(peerId)] as? BasicPeerView)?.peer else { + return nil + } + return initialAvatarGalleryEntries(peer: peer).first + } + |> distinctUntilChanged + |> mapToSignal { firstEntry -> Signal<[AvatarGalleryEntry], NoError> in + if let firstEntry = firstEntry { + return context.account.postbox.loadedPeerWithId(peerId) + |> mapToSignal { peer -> Signal<[AvatarGalleryEntry], NoError>in + return fetchedAvatarGalleryEntries(account: context.account, peer: peer, firstEntry: firstEntry) + } + } else { + return .single([]) + } + } + |> map { items -> Any in + return items + } +} + +func peerInfoProfilePhotosWithCache(context: AccountContext, peerId: PeerId) -> Signal<[AvatarGalleryEntry], NoError> { + return context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: context.account.postbox, network: context.account.network, peerId: peerId, fetch: peerInfoProfilePhotos(context: context, peerId: peerId)) + |> map { items -> [AvatarGalleryEntry] in + return items as? [AvatarGalleryEntry] ?? [] + } +} + +func keepPeerInfoScreenDataHot(context: AccountContext, peerId: PeerId) -> Signal { + return peerInfoScreenInputData(context: context, peerId: peerId) + |> mapToSignal { inputData -> Signal in + switch inputData { + case .none: + return .complete() + case .user, .channel, .group: + return combineLatest( + context.peerChannelMemberCategoriesContextsManager.profileData(postbox: context.account.postbox, network: context.account.network, peerId: peerId, customData: peerInfoAvailableMediaPanes(context: context, peerId: peerId) |> ignoreValues), + context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: context.account.postbox, network: context.account.network, peerId: peerId, fetch: peerInfoProfilePhotos(context: context, peerId: peerId)) |> ignoreValues + ) + |> ignoreValues + } + } +} + +func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> Signal { + return peerInfoScreenInputData(context: context, peerId: peerId) + |> mapToSignal { inputData -> Signal in + switch inputData { + case .none: + return .single(PeerInfoScreenData( + peer: nil, + cachedData: nil, + status: nil, + notificationSettings: nil, + globalNotificationSettings: nil, + isContact: false, + availablePanes: [], + groupsInCommon: nil, + linkedDiscussionPeer: nil, + members: nil, + encryptionKeyFingerprint: nil + )) + case let .user(userPeerId, secretChatId, kind): + let groupsInCommon: GroupsInCommonContext? + if case .user = kind { + groupsInCommon = GroupsInCommonContext(account: context.account, peerId: userPeerId) + } else { + groupsInCommon = nil + } + + enum StatusInputData: Equatable { + case none + case presence(TelegramUserPresence) + case bot + case support + } + let status = Signal { subscriber in + class Manager { + var currentValue: TelegramUserPresence? = nil + var updateManager: QueueLocalObject? = nil + } + let manager = Atomic(value: Manager()) + let notify: () -> Void = { + let data = manager.with { manager -> PeerInfoStatusData? in + if let presence = manager.currentValue { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + let (text, isActivity) = stringAndActivityForUserPresence(strings: strings, dateTimeFormat: dateTimeFormat, presence: presence, relativeTo: Int32(timestamp), expanded: true) + return PeerInfoStatusData(text: text, isActivity: isActivity) + } else { + return nil + } + } + subscriber.putNext(data) + } + let disposable = (context.account.viewTracker.peerView(userPeerId, updateData: false) + |> map { view -> StatusInputData in + guard let user = view.peers[userPeerId] as? TelegramUser else { + return .none + } + if user.id == context.account.peerId { + return .none + } + if user.isDeleted { + return .none + } + if user.flags.contains(.isSupport) { + return .support + } + if user.botInfo != nil { + return .bot + } + guard let presence = view.peerPresences[userPeerId] as? TelegramUserPresence else { + return .none + } + return .presence(presence) + } + |> distinctUntilChanged).start(next: { inputData in + switch inputData { + case .bot: + subscriber.putNext(PeerInfoStatusData(text: strings.Bot_GenericBotStatus, isActivity: false)) + case .support: + subscriber.putNext(PeerInfoStatusData(text: strings.Bot_GenericSupportStatus, isActivity: false)) + default: + var presence: TelegramUserPresence? + if case let .presence(value) = inputData { + presence = value + } + let _ = manager.with { manager -> Void in + manager.currentValue = presence + if let presence = presence { + let updateManager: QueueLocalObject + if let current = manager.updateManager { + updateManager = current + } else { + updateManager = QueueLocalObject(queue: .mainQueue(), generate: { + return PeerPresenceStatusManager(update: { + notify() + }) + }) + } + updateManager.with { updateManager in + updateManager.reset(presence: presence) + } + } else if let _ = manager.updateManager { + manager.updateManager = nil + } + } + notify() + } + }) + return disposable + } + |> distinctUntilChanged + let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.globalNotifications])) + var combinedKeys: [PostboxViewKey] = [] + combinedKeys.append(globalNotificationsKey) + if let secretChatId = secretChatId { + combinedKeys.append(.peerChatState(peerId: secretChatId)) + } + return combineLatest( + context.account.viewTracker.peerView(userPeerId, updateData: true), + peerInfoAvailableMediaPanes(context: context, peerId: peerId), + context.account.postbox.combinedView(keys: combinedKeys), + status + ) + |> map { peerView, availablePanes, combinedView, status -> PeerInfoScreenData in + var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings + if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView { + if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings { + globalNotificationSettings = settings + } + } + + var encryptionKeyFingerprint: SecretChatKeyFingerprint? + if let secretChatId = secretChatId, let peerChatStateView = combinedView.views[.peerChatState(peerId: secretChatId)] as? PeerChatStateView { + if let peerChatState = peerChatStateView.chatState as? SecretChatKeyState { + encryptionKeyFingerprint = peerChatState.keyFingerprint + } + } + + var availablePanes = availablePanes + if availablePanes != nil, groupsInCommon != nil, let cachedData = peerView.cachedData as? CachedUserData { + if cachedData.commonGroupCount != 0 { + availablePanes?.append(.groupsInCommon) + } + } + + return PeerInfoScreenData( + peer: peerView.peers[userPeerId], + cachedData: peerView.cachedData, + status: status, + notificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings, + globalNotificationSettings: globalNotificationSettings, + isContact: peerView.peerIsContact, + availablePanes: availablePanes ?? [], + groupsInCommon: groupsInCommon, + linkedDiscussionPeer: nil, + members: nil, + encryptionKeyFingerprint: encryptionKeyFingerprint + ) + } + case .channel: + let status = context.account.viewTracker.peerView(peerId, updateData: false) + |> map { peerView -> PeerInfoStatusData? in + guard let channel = peerView.peers[peerId] as? TelegramChannel else { + return PeerInfoStatusData(text: strings.Channel_Status, isActivity: false) + } + if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount, memberCount != 0 { + return PeerInfoStatusData(text: strings.Conversation_StatusSubscribers(memberCount), isActivity: false) + } else { + return PeerInfoStatusData(text: strings.Channel_Status, isActivity: false) + } + } + |> distinctUntilChanged + + let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.globalNotifications])) + var combinedKeys: [PostboxViewKey] = [] + combinedKeys.append(globalNotificationsKey) + return combineLatest( + context.account.viewTracker.peerView(peerId, updateData: true), + peerInfoAvailableMediaPanes(context: context, peerId: peerId), + context.account.postbox.combinedView(keys: combinedKeys), + status + ) + |> map { peerView, availablePanes, combinedView, status -> PeerInfoScreenData in + var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings + if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView { + if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings { + globalNotificationSettings = settings + } + } + + var discussionPeer: Peer? + if let linkedDiscussionPeerId = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] { + discussionPeer = peer + } + + return PeerInfoScreenData( + peer: peerView.peers[peerId], + cachedData: peerView.cachedData, + status: status, + notificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings, + globalNotificationSettings: globalNotificationSettings, + isContact: peerView.peerIsContact, + availablePanes: availablePanes ?? [], + groupsInCommon: nil, + linkedDiscussionPeer: discussionPeer, + members: nil, + encryptionKeyFingerprint: nil + ) + } + case let .group(groupId): + var onlineMemberCount: Signal = .single(nil) + if peerId.namespace == Namespaces.Peer.CloudChannel { + onlineMemberCount = context.account.viewTracker.peerView(groupId, updateData: false) + |> map { view -> Bool? in + if let cachedData = view.cachedData as? CachedChannelData, let peer = peerViewMainPeer(view) as? TelegramChannel { + if case .broadcast = peer.info { + return nil + } else if let memberCount = cachedData.participantsSummary.memberCount, memberCount > 50 { + return true + } else { + return false + } + } else { + return false + } + } + |> distinctUntilChanged + |> mapToSignal { isLarge -> Signal in + if let isLarge = isLarge { + if isLarge { + return context.peerChannelMemberCategoriesContextsManager.recentOnline(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) + |> map(Optional.init) + } else { + return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) + |> map(Optional.init) + } + } else { + return .single(nil) + } + } + } + + let status = combineLatest(queue: .mainQueue(), + context.account.viewTracker.peerView(groupId, updateData: false), + onlineMemberCount + ) + |> map { peerView, onlineMemberCount -> PeerInfoStatusData? in + if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { + if let onlineMemberCount = onlineMemberCount, onlineMemberCount > 1 { + var string = "" + + string.append("\(strings.Conversation_StatusMembers(Int32(memberCount))), ") + string.append(strings.Conversation_StatusOnline(Int32(onlineMemberCount))) + return PeerInfoStatusData(text: string, isActivity: false) + } else if memberCount > 0 { + return PeerInfoStatusData(text: strings.Conversation_StatusMembers(Int32(memberCount)), isActivity: false) + } + } else if let group = peerView.peers[groupId] as? TelegramGroup, let cachedGroupData = peerView.cachedData as? CachedGroupData { + var onlineCount = 0 + if let participants = cachedGroupData.participants { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + for participant in participants.participants { + if let presence = peerView.peerPresences[participant.peerId] as? TelegramUserPresence { + let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: Int32(timestamp)) + switch relativeStatus { + case .online: + onlineCount += 1 + default: + break + } + } + } + } + if onlineCount > 1 { + var string = "" + + string.append("\(strings.Conversation_StatusMembers(Int32(group.participantCount))), ") + string.append(strings.Conversation_StatusOnline(Int32(onlineCount))) + return PeerInfoStatusData(text: string, isActivity: false) + } else { + return PeerInfoStatusData(text: strings.Conversation_StatusMembers(Int32(group.participantCount)), isActivity: false) + } + } + + return PeerInfoStatusData(text: strings.Group_Status, isActivity: false) + } + |> distinctUntilChanged + + let membersContext = PeerInfoMembersContext(context: context, peerId: groupId) + + let membersData: Signal = membersContext.state + |> map { state -> PeerInfoMembersData? in + if state.members.count > 5 { + return .longList(membersContext) + } else { + return .shortList(membersContext: membersContext, members: state.members) + } + } + |> distinctUntilChanged + + let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.globalNotifications])) + var combinedKeys: [PostboxViewKey] = [] + combinedKeys.append(globalNotificationsKey) + return combineLatest(queue: .mainQueue(), + context.account.viewTracker.peerView(groupId, updateData: true), + peerInfoAvailableMediaPanes(context: context, peerId: groupId), + context.account.postbox.combinedView(keys: combinedKeys), + status, + membersData + ) + |> map { peerView, availablePanes, combinedView, status, membersData -> PeerInfoScreenData in + var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings + if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView { + if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings { + globalNotificationSettings = settings + } + } + + var discussionPeer: Peer? + if let linkedDiscussionPeerId = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] { + discussionPeer = peer + } + + var availablePanes = availablePanes + if let membersData = membersData, case .longList = membersData { + if availablePanes != nil { + availablePanes?.insert(.members, at: 0) + } else { + availablePanes = [.members] + } + } + + return PeerInfoScreenData( + peer: peerView.peers[groupId], + cachedData: peerView.cachedData, + status: status, + notificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings, + globalNotificationSettings: globalNotificationSettings, + isContact: peerView.peerIsContact, + availablePanes: availablePanes ?? [], + groupsInCommon: nil, + linkedDiscussionPeer: discussionPeer, + members: membersData, + encryptionKeyFingerprint: nil + ) + } + } + } +} + +func canEditPeerInfo(peer: Peer?) -> Bool { + if let channel = peer as? TelegramChannel { + if channel.hasPermission(.changeInfo) { + return true + } + } else if let group = peer as? TelegramGroup { + switch group.role { + case .admin, .creator: + return true + case .member: + break + } + if !group.hasBannedPermission(.banChangeInfo) { + return true + } + } + return false +} + +struct PeerInfoMemberActions: OptionSet { + var rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue + } + + static let restrict = PeerInfoMemberActions(rawValue: 1 << 0) + static let promote = PeerInfoMemberActions(rawValue: 1 << 1) +} + +func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer, member: PeerInfoMember) -> PeerInfoMemberActions { + var result: PeerInfoMemberActions = [] + + if member.id != accountPeerId { + if let channel = peer as? TelegramChannel { + if channel.flags.contains(.isCreator) { + result.insert(.restrict) + result.insert(.promote) + } else { + switch member { + case let .channelMember(channelMember): + switch channelMember.participant { + case .creator: + break + case let .member(member): + if let adminInfo = member.adminInfo { + if adminInfo.promotedBy == accountPeerId { + result.insert(.restrict) + if channel.hasPermission(.addAdmins) { + result.insert(.promote) + } + } + } else { + if channel.hasPermission(.banMembers) { + result.insert(.restrict) + } + } + } + case .legacyGroupMember: + break + } + } + } else if let group = peer as? TelegramGroup { + switch group.role { + case .creator: + result.insert(.restrict) + result.insert(.promote) + case .admin: + switch member { + case let .legacyGroupMember(legacyGroupMember): + if legacyGroupMember.invitedBy == accountPeerId { + result.insert(.restrict) + result.insert(.promote) + } + case .channelMember: + break + } + case .member: + switch member { + case let .legacyGroupMember(legacyGroupMember): + if legacyGroupMember.invitedBy == accountPeerId { + result.insert(.restrict) + } + case .channelMember: + break + } + } + } + } + + return result +} + +func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFromChat: Bool) -> [PeerInfoHeaderButtonKey] { + var result: [PeerInfoHeaderButtonKey] = [] + if let user = peer as? TelegramUser { + if !isOpenedFromChat { + result.append(.message) + } + var callsAvailable = false + if !user.isDeleted, user.botInfo == nil, !user.flags.contains(.isSupport) { + if let cachedUserData = cachedData as? CachedUserData { + callsAvailable = cachedUserData.callsAvailable + } + callsAvailable = true + } + if callsAvailable { + result.append(.call) + } + result.append(.mute) + if isOpenedFromChat { + result.append(.search) + } + result.append(.more) + } else if let channel = peer as? TelegramChannel { + var displayLeave = !channel.flags.contains(.isCreator) + switch channel.info { + case .broadcast: + displayLeave = true + case .group: + displayLeave = false + if channel.flags.contains(.isCreator) || channel.hasPermission(.inviteMembers) { + result.append(.addMember) + } + } + + result.append(.mute) + result.append(.search) + if displayLeave { + result.append(.leave) + } + result.append(.more) + } else if let group = peer as? TelegramGroup { + var canEditGroupInfo = false + var canEditMembers = false + var canAddMembers = false + var isPublic = false + var isCreator = false + + if case .creator = group.role { + isCreator = true + } + switch group.role { + case .admin, .creator: + canEditGroupInfo = true + canEditMembers = true + canAddMembers = true + case .member: + break + } + if !group.hasBannedPermission(.banChangeInfo) { + canEditGroupInfo = true + } + if !group.hasBannedPermission(.banAddMembers) { + canAddMembers = true + } + + if canAddMembers { + result.append(.addMember) + } + + result.append(.mute) + result.append(.search) + result.append(.more) + } + return result +} + +func peerInfoCanEdit(peer: Peer?, cachedData: CachedPeerData?) -> Bool { + if let user = peer as? TelegramUser { + if user.isDeleted { + return false + } + return true + } else if peer is TelegramChannel || peer is TelegramGroup { + return true + } + return false +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoHeaderNode.swift new file mode 100644 index 0000000000..6b5995e738 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoHeaderNode.swift @@ -0,0 +1,2250 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import Postbox +import SyncCore +import TelegramCore +import AvatarNode +import AccountContext +import SwiftSignalKit +import TelegramPresentationData +import PhotoResources +import PeerAvatarGalleryUI +import TelegramStringFormatting +import ActivityIndicator + +enum PeerInfoHeaderButtonKey: Hashable { + case message + case discussion + case call + case mute + case more + case addMember + case search + case leave +} + +enum PeerInfoHeaderButtonIcon { + case message + case call + case mute + case unmute + case more + case addMember + case search + case leave +} + +final class PeerInfoHeaderButtonNode: HighlightableButtonNode { + let key: PeerInfoHeaderButtonKey + private let action: (PeerInfoHeaderButtonNode) -> Void + let containerNode: ASDisplayNode + private let backgroundNode: ASImageNode + private let textNode: ImmediateTextNode + + private var theme: PresentationTheme? + private var icon: PeerInfoHeaderButtonIcon? + + init(key: PeerInfoHeaderButtonKey, action: @escaping (PeerInfoHeaderButtonNode) -> Void) { + self.key = key + self.action = action + + self.containerNode = ASDisplayNode() + + self.backgroundNode = ASImageNode() + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.backgroundNode) + self.containerNode.addSubnode(self.textNode) + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.layer.removeAnimation(forKey: "opacity") + strongSelf.alpha = 0.4 + } else { + strongSelf.alpha = 1.0 + strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + @objc private func buttonPressed() { + self.action(self) + } + + func update(size: CGSize, text: String, icon: PeerInfoHeaderButtonIcon, isExpanded: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + if self.theme != presentationData.theme || self.icon != icon { + self.theme = presentationData.theme + self.icon = icon + self.backgroundNode.image = generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(presentationData.theme.list.itemAccentColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.normal) + context.setFillColor(presentationData.theme.list.itemCheckColors.foregroundColor.cgColor) + let imageName: String + switch icon { + case .message: + imageName = "Peer Info/ButtonMessage" + case .call: + imageName = "Peer Info/ButtonCall" + case .mute: + imageName = "Peer Info/ButtonMute" + case .unmute: + imageName = "Peer Info/ButtonUnmute" + case .more: + imageName = "Peer Info/ButtonMore" + case .addMember: + imageName = "Peer Info/ButtonAddMember" + case .search: + imageName = "Peer Info/ButtonSearch" + case .leave: + imageName = "Peer Info/ButtonLeave" + } + if let image = UIImage(bundleImageName: imageName) { + let imageRect = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) + context.clip(to: imageRect, mask: image.cgImage!) + context.fill(imageRect) + } + }) + } + + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(12.0), textColor: presentationData.theme.list.itemAccentColor) + let titleSize = self.textNode.updateLayout(CGSize(width: 120.0, height: .greatestFiniteMagnitude)) + + transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrameAdditiveToCenter(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: size.height + 6.0), size: titleSize)) + transition.updateAlpha(node: self.textNode, alpha: isExpanded ? 0.0 : 1.0) + } +} + +final class PeerInfoHeaderNavigationTransition { + let sourceNavigationBar: NavigationBar + let sourceTitleView: ChatTitleView + let sourceTitleFrame: CGRect + let sourceSubtitleFrame: CGRect + let fraction: CGFloat + + init(sourceNavigationBar: NavigationBar, sourceTitleView: ChatTitleView, sourceTitleFrame: CGRect, sourceSubtitleFrame: CGRect, fraction: CGFloat) { + self.sourceNavigationBar = sourceNavigationBar + self.sourceTitleView = sourceTitleView + self.sourceTitleFrame = sourceTitleFrame + self.sourceSubtitleFrame = sourceSubtitleFrame + self.fraction = fraction + } +} + +enum PeerInfoAvatarListItem: Equatable { + case topImage([ImageRepresentationWithReference]) + case image(TelegramMediaImageReference?, [ImageRepresentationWithReference]) + + var id: WrappedMediaResourceId { + switch self { + case let .topImage(representations): + let representation = largestImageRepresentation(representations.map { $0.representation }) ?? representations[representations.count - 1].representation + return WrappedMediaResourceId(representation.resource.id) + case let .image(_, representations): + let representation = largestImageRepresentation(representations.map { $0.representation }) ?? representations[representations.count - 1].representation + return WrappedMediaResourceId(representation.resource.id) + } + } +} + +final class PeerInfoAvatarListItemNode: ASDisplayNode { + private let context: AccountContext + let imageNode: TransformImageNode + + let isReady = Promise() + private var didSetReady: Bool = false + + init(context: AccountContext) { + self.context = context + self.imageNode = TransformImageNode() + + super.init() + + self.imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] + self.addSubnode(self.imageNode) + + self.imageNode.imageUpdated = { [weak self] _ in + guard let strongSelf = self else { + return + } + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf.isReady.set(.single(true)) + } + } + } + + func setup(item: PeerInfoAvatarListItem, synchronous: Bool) { + let representations: [ImageRepresentationWithReference] + switch item { + case let .topImage(topRepresentations): + representations = topRepresentations + case let .image(_, imageRepresentations): + representations = imageRepresentations + } + self.imageNode.setSignal(chatAvatarGalleryPhoto(account: self.context.account, representations: representations, autoFetchFullSize: true, attemptSynchronously: synchronous), attemptSynchronously: synchronous, dispatchOnDisplayLink: false) + } + + func update(size: CGSize, transition: ContainedViewLayoutTransition) { + let imageSize = CGSize(width: min(size.width, size.height), height: min(size.width, size.height)) + let makeLayout = self.imageNode.asyncLayout() + let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) + let _ = applyLayout() + transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)) + } +} + +final class PeerInfoAvatarListContainerNode: ASDisplayNode { + private let context: AccountContext + + let controlsContainerNode: ASDisplayNode + let controlsClippingNode: ASDisplayNode + let controlsClippingOffsetNode: ASDisplayNode + let shadowNode: ASImageNode + + let contentNode: ASDisplayNode + let leftHighlightNode: ASImageNode + let rightHighlightNode: ASImageNode + var highlightedSide: Bool? + let stripContainerNode: ASDisplayNode + let highlightContainerNode: ASDisplayNode + private(set) var galleryEntries: [AvatarGalleryEntry] = [] + private var items: [PeerInfoAvatarListItem] = [] + private var itemNodes: [WrappedMediaResourceId: PeerInfoAvatarListItemNode] = [:] + private var stripNodes: [ASImageNode] = [] + private let activeStripImage: UIImage + private var appliedStripNodeCurrentIndex: Int? + private var currentIndex: Int = 0 + private var transitionFraction: CGFloat = 0.0 + + private var validLayout: CGSize? + + private let disposable = MetaDisposable() + private var initializedList = false + + let isReady = Promise() + private var didSetReady = false + + var currentItemNode: PeerInfoAvatarListItemNode? { + if self.currentIndex >= 0 && self.currentIndex < self.items.count { + return self.itemNodes[self.items[self.currentIndex].id] + } else { + return nil + } + } + + var currentEntry: AvatarGalleryEntry? { + if self.currentIndex >= 0 && self.currentIndex < self.galleryEntries.count { + return self.galleryEntries[self.currentIndex] + } else { + return nil + } + } + + init(context: AccountContext) { + self.context = context + + self.contentNode = ASDisplayNode() + + self.leftHighlightNode = ASImageNode() + self.leftHighlightNode.displaysAsynchronously = false + self.leftHighlightNode.displayWithoutProcessing = true + self.leftHighlightNode.contentMode = .scaleToFill + self.leftHighlightNode.image = generateImage(CGSize(width: 88.0, height: 1.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let topColor = UIColor(rgb: 0x000000, alpha: 0.1) + let bottomColor = UIColor(rgb: 0x000000, alpha: 0.0) + + var locations: [CGFloat] = [0.0, 1.0] + let colors: [CGColor] = [topColor.cgColor, bottomColor.cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + }) + self.leftHighlightNode.alpha = 0.0 + + self.rightHighlightNode = ASImageNode() + self.rightHighlightNode.displaysAsynchronously = false + self.rightHighlightNode.displayWithoutProcessing = true + self.rightHighlightNode.contentMode = .scaleToFill + self.rightHighlightNode.image = generateImage(CGSize(width: 88.0, height: 1.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let topColor = UIColor(rgb: 0x000000, alpha: 0.1) + let bottomColor = UIColor(rgb: 0x000000, alpha: 0.0) + + var locations: [CGFloat] = [0.0, 1.0] + let colors: [CGColor] = [topColor.cgColor, bottomColor.cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: 0.0), end: CGPoint(x: 0.0, y: 0.0), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + }) + self.rightHighlightNode.alpha = 0.0 + + self.stripContainerNode = ASDisplayNode() + self.contentNode.addSubnode(self.stripContainerNode) + self.activeStripImage = generateSmallHorizontalStretchableFilledCircleImage(diameter: 2.0, color: .white)! + + self.highlightContainerNode = ASDisplayNode() + self.highlightContainerNode.addSubnode(self.leftHighlightNode) + self.highlightContainerNode.addSubnode(self.rightHighlightNode) + + self.controlsContainerNode = ASDisplayNode() + self.controlsContainerNode.isUserInteractionEnabled = false + + self.controlsClippingOffsetNode = ASDisplayNode() + + self.controlsClippingNode = ASDisplayNode() + self.controlsClippingNode.isUserInteractionEnabled = false + self.controlsClippingNode.clipsToBounds = true + + self.shadowNode = ASImageNode() + self.shadowNode.displaysAsynchronously = false + self.shadowNode.displayWithoutProcessing = true + self.shadowNode.contentMode = .scaleToFill + + do { + let size = CGSize(width: 88.0, height: 88.0) + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + if let context = UIGraphicsGetCurrentContext() { + context.clip(to: CGRect(origin: CGPoint(), size: size)) + + let topColor = UIColor(rgb: 0x000000, alpha: 0.4) + let bottomColor = UIColor(rgb: 0x000000, alpha: 0.0) + + var locations: [CGFloat] = [0.0, 1.0] + let colors: [CGColor] = [topColor.cgColor, bottomColor.cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + if let image = image { + self.shadowNode.image = generateImage(image.size, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.rotate(by: -CGFloat.pi / 2.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) + }) + } + } + } + + super.init() + + self.backgroundColor = .black + + self.addSubnode(self.contentNode) + + self.controlsContainerNode.addSubnode(self.highlightContainerNode) + self.controlsContainerNode.addSubnode(self.shadowNode) + self.controlsContainerNode.addSubnode(self.stripContainerNode) + self.controlsClippingNode.addSubnode(self.controlsContainerNode) + self.controlsClippingOffsetNode.addSubnode(self.controlsClippingNode) + + self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in + guard let strongSelf = self else { + return false + } + return strongSelf.currentIndex != 0 + } + self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { _ in + return .keepWithSingleTap + } + recognizer.highlight = { [weak self] point in + guard let strongSelf = self, let size = strongSelf.validLayout else { + return + } + var highlightedSide: Bool? + if let point = point { + if point.x < size.width * 1.0 / 5.0 { + if strongSelf.items.count > 1 { + highlightedSide = false + } + } else { + if strongSelf.items.count > 1 { + highlightedSide = true + } + } + } + if strongSelf.highlightedSide != highlightedSide { + strongSelf.highlightedSide = highlightedSide + let leftAlpha: CGFloat + let rightAlpha: CGFloat + if let highlightedSide = highlightedSide { + leftAlpha = highlightedSide ? 0.0 : 1.0 + rightAlpha = highlightedSide ? 1.0 : 0.0 + } else { + leftAlpha = 0.0 + rightAlpha = 0.0 + } + if strongSelf.leftHighlightNode.alpha != leftAlpha { + strongSelf.leftHighlightNode.alpha = leftAlpha + if leftAlpha.isZero { + strongSelf.leftHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, timingFunction: kCAMediaTimingFunctionSpring) + } else { + strongSelf.leftHighlightNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.08) + } + } + if strongSelf.rightHighlightNode.alpha != rightAlpha { + strongSelf.rightHighlightNode.alpha = rightAlpha + if rightAlpha.isZero { + strongSelf.rightHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, timingFunction: kCAMediaTimingFunctionSpring) + } else { + strongSelf.rightHighlightNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.08) + } + } + } + } + self.view.addGestureRecognizer(recognizer) + } + + deinit { + self.disposable.dispose() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + + func selectFirstItem() { + self.currentIndex = 0 + if let size = self.validLayout { + self.updateItems(size: size, transition: .immediate, stripTransition: .immediate) + } + } + + func updateEntryIsHidden(entry: AvatarGalleryEntry?) { + if let entry = entry, let index = self.galleryEntries.index(of: entry) { + self.currentItemNode?.isHidden = index == self.currentIndex + } else { + self.currentItemNode?.isHidden = false + } + } + + @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + if let size = self.validLayout, case .tap = gesture { + if location.x < size.width * 1.0 / 5.0 { + if self.currentIndex != 0 { + self.currentIndex -= 1 + self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring)) + } else if self.items.count > 1 { + self.currentIndex = self.items.count - 1 + self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true) + } + } else { + if self.currentIndex < self.items.count - 1 { + self.currentIndex += 1 + self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring)) + } else if self.items.count > 1 { + self.currentIndex = 0 + self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true) + } + } + } + } + default: + break + } + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .changed: + let translation = recognizer.translation(in: self.view) + var transitionFraction = translation.x / self.bounds.width + if self.currentIndex <= 0 { + transitionFraction = min(0.0, transitionFraction) + } + if self.currentIndex >= self.items.count - 1 { + transitionFraction = max(0.0, transitionFraction) + } + self.transitionFraction = transitionFraction + if let size = self.validLayout { + self.updateItems(size: size, transition: .animated(duration: 0.3, curve: .spring), stripTransition: .animated(duration: 0.3, curve: .spring)) + } + case .cancelled, .ended: + let translation = recognizer.translation(in: self.view) + let velocity = recognizer.velocity(in: self.view) + var directionIsToRight = false + if abs(velocity.x) > 10.0 { + directionIsToRight = velocity.x < 0.0 + } else { + directionIsToRight = translation.x > self.bounds.width / 2.0 + } + var updatedIndex = self.currentIndex + if directionIsToRight { + updatedIndex = min(updatedIndex + 1, self.items.count - 1) + } else { + updatedIndex = max(updatedIndex - 1, 0) + } + self.currentIndex = updatedIndex + self.transitionFraction = 0.0 + if let size = self.validLayout { + self.updateItems(size: size, transition: .animated(duration: 0.3, curve: .spring), stripTransition: .animated(duration: 0.3, curve: .spring)) + } + default: + break + } + } + + func update(size: CGSize, peer: Peer?, transition: ContainedViewLayoutTransition) { + self.validLayout = size + + self.leftHighlightNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: floor(size.width * 1.0 / 5.0), height: size.height)) + self.rightHighlightNode.frame = CGRect(origin: CGPoint(x: size.width - floor(size.width * 1.0 / 5.0), y: 0.0), size: CGSize(width: floor(size.width * 1.0 / 5.0), height: size.height)) + + if let peer = peer, !self.initializedList { + self.initializedList = true + self.disposable.set((peerInfoProfilePhotosWithCache(context: self.context, peerId: peer.id) + |> deliverOnMainQueue).start(next: { [weak self] entries in + guard let strongSelf = self else { + return + } + var items: [PeerInfoAvatarListItem] = [] + for entry in entries { + switch entry { + case let .topImage(representations, _): + items.append(.topImage(representations)) + case let .image(_, reference, representations, _, _, _, _): + items.append(.image(reference, representations)) + } + } + strongSelf.galleryEntries = entries + strongSelf.items = items + if let size = strongSelf.validLayout { + strongSelf.updateItems(size: size, transition: .immediate, stripTransition: .immediate) + } + if items.isEmpty { + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf.isReady.set(.single(true)) + } + } + })) + } + self.updateItems(size: size, transition: transition, stripTransition: transition) + } + + private func updateItems(size: CGSize, transition: ContainedViewLayoutTransition, stripTransition: ContainedViewLayoutTransition, synchronous: Bool = false) { + var validIds: [WrappedMediaResourceId] = [] + var addedItemNodesForAdditiveTransition: [PeerInfoAvatarListItemNode] = [] + var additiveTransitionOffset: CGFloat = 0.0 + if self.currentIndex >= 0 && self.currentIndex < self.items.count { + for i in max(0, self.currentIndex - 1) ... min(self.currentIndex + 1, self.items.count - 1) { + validIds.append(self.items[i].id) + let itemNode: PeerInfoAvatarListItemNode + var wasAdded = false + if let current = self.itemNodes[self.items[i].id] { + itemNode = current + } else { + wasAdded = true + itemNode = PeerInfoAvatarListItemNode(context: self.context) + itemNode.setup(item: self.items[i], synchronous: synchronous && i == self.currentIndex) + self.itemNodes[self.items[i].id] = itemNode + self.contentNode.addSubnode(itemNode) + } + let indexOffset = CGFloat(i - self.currentIndex) + let itemFrame = CGRect(origin: CGPoint(x: indexOffset * size.width + self.transitionFraction * size.width - size.width / 2.0, y: -size.height / 2.0), size: size) + + if wasAdded { + addedItemNodesForAdditiveTransition.append(itemNode) + itemNode.frame = itemFrame + itemNode.update(size: size, transition: .immediate) + } else { + additiveTransitionOffset = itemNode.frame.minX - itemFrame.minX + transition.updateFrame(node: itemNode, frame: itemFrame) + itemNode.update(size: size, transition: transition) + } + } + } + for itemNode in addedItemNodesForAdditiveTransition { + transition.animatePositionAdditive(node: itemNode, offset: CGPoint(x: additiveTransitionOffset, y: 0.0)) + } + var removeIds: [WrappedMediaResourceId] = [] + for (id, _) in self.itemNodes { + if !validIds.contains(id) { + removeIds.append(id) + } + } + for id in removeIds { + if let itemNode = self.itemNodes.removeValue(forKey: id) { + itemNode.removeFromSupernode() + } + } + + let hadOneStripNode = self.stripNodes.count == 1 + if self.stripNodes.count != self.items.count { + if self.stripNodes.count < self.items.count { + for _ in 0 ..< self.items.count - self.stripNodes.count { + let stripNode = ASImageNode() + stripNode.displaysAsynchronously = false + stripNode.displayWithoutProcessing = true + stripNode.image = self.activeStripImage + if stripNodes.count != self.currentIndex { + stripNode.alpha = 0.2 + } + self.stripNodes.append(stripNode) + self.stripContainerNode.addSubnode(stripNode) + } + } else { + for i in (self.items.count ..< self.stripNodes.count).reversed() { + self.stripNodes[i].removeFromSupernode() + self.stripNodes.remove(at: i) + } + } + } + if self.appliedStripNodeCurrentIndex != self.currentIndex { + if let appliedStripNodeCurrentIndex = self.appliedStripNodeCurrentIndex { + if appliedStripNodeCurrentIndex >= 0 && appliedStripNodeCurrentIndex < self.stripNodes.count { + let previousAlpha = self.stripNodes[appliedStripNodeCurrentIndex].alpha + self.stripNodes[appliedStripNodeCurrentIndex].alpha = 0.2 + if previousAlpha == 1.0 { + self.stripNodes[appliedStripNodeCurrentIndex].layer.animateAlpha(from: 1.0, to: 0.2, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } + } + } + self.appliedStripNodeCurrentIndex = self.currentIndex + if self.currentIndex >= 0 && self.currentIndex < self.stripNodes.count { + self.stripNodes[self.currentIndex].alpha = 1.0 + } + } + if hadOneStripNode && self.stripNodes.count > 1 { + self.stripContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + let stripInset: CGFloat = 8.0 + let stripSpacing: CGFloat = 4.0 + let stripWidth: CGFloat = max(5.0, floor((size.width - stripInset * 2.0 - stripSpacing * CGFloat(self.stripNodes.count - 1)) / CGFloat(self.stripNodes.count))) + let currentStripMinX = stripInset + CGFloat(self.currentIndex) * (stripWidth + stripSpacing) + let currentStripMidX = floor(currentStripMinX + stripWidth / 2.0) + let lastStripMaxX = stripInset + CGFloat(self.stripNodes.count - 1) * (stripWidth + stripSpacing) + stripWidth + let maxStripOffset: CGFloat = 0.0 + let stripOffset: CGFloat = min(0.0, max(size.width - stripInset - lastStripMaxX, size.width / 2.0 - currentStripMidX)) + for i in 0 ..< self.stripNodes.count { + let stripX: CGFloat = stripInset + CGFloat(i) * (stripWidth + stripSpacing) + if i == 0 && self.stripNodes.count == 1 { + self.stripNodes[i].isHidden = true + } else { + self.stripNodes[i].isHidden = false + } + let stripFrame = CGRect(origin: CGPoint(x: stripOffset + stripX, y: 0.0), size: CGSize(width: stripWidth + 1.0, height: 2.0)) + stripTransition.updateFrame(node: self.stripNodes[i], frame: stripFrame) + } + + if let item = self.items.first, let itemNode = self.itemNodes[item.id] { + if !self.didSetReady { + self.didSetReady = true + self.isReady.set(itemNode.isReady.get()) + } + } + } +} + +final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { + let context: AccountContext + let avatarNode: AvatarNode + + var tapped: (() -> Void)? + + private var isFirstAvatarLoading = true + + init(context: AccountContext) { + self.context = context + let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0)) + self.avatarNode = AvatarNode(font: avatarFont) + + super.init() + + self.addSubnode(self.avatarNode) + self.avatarNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0)) + + self.avatarNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.tapped?() + } + } + + func update(peer: Peer?, theme: PresentationTheme) { + if let peer = peer { + var overrideImage: AvatarNodeImageOverride? + if peer.isDeleted { + overrideImage = .deletedIcon + } + self.avatarNode.setPeer(context: self.context, theme: theme, peer: peer, overrideImage: overrideImage, synchronousLoad: self.isFirstAvatarLoading, displayDimensions: CGSize(width: 100.0, height: 100.0), storeUnrounded: true) + self.isFirstAvatarLoading = false + } + } +} + +final class PeerInfoEditingAvatarNode: ASDisplayNode { + let context: AccountContext + let avatarNode: AvatarNode + + private let updatingAvatarOverlay: ASImageNode + private let activityIndicator: ActivityIndicator + + var tapped: (() -> Void)? + + init(context: AccountContext) { + self.context = context + let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0)) + self.avatarNode = AvatarNode(font: avatarFont) + + self.updatingAvatarOverlay = ASImageNode() + self.updatingAvatarOverlay.displayWithoutProcessing = true + self.updatingAvatarOverlay.displaysAsynchronously = false + self.updatingAvatarOverlay.isHidden = true + + self.activityIndicator = ActivityIndicator(type: .custom(.white, 22.0, 1.0, false)) + self.activityIndicator.isHidden = true + + super.init() + + self.addSubnode(self.avatarNode) + self.avatarNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0)) + self.updatingAvatarOverlay.frame = self.avatarNode.frame + let indicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0)) + self.activityIndicator.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(self.avatarNode.frame.midX - indicatorSize.width / 2.0), y: floorToScreenPixels(self.avatarNode.frame.midY - indicatorSize.height / 2.0)), size: indicatorSize) + + self.addSubnode(self.updatingAvatarOverlay) + self.addSubnode(self.activityIndicator) + + self.avatarNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.tapped?() + } + } + + func update(peer: Peer?, updatingAvatar: PeerInfoUpdatingAvatar?, theme: PresentationTheme) { + if let peer = peer { + let overrideImage: AvatarNodeImageOverride? + if canEditPeerInfo(peer: peer) { + if let updatingAvatar = updatingAvatar { + switch updatingAvatar { + case let .image(representation): + overrideImage = .image(representation) + case .none: + overrideImage = .none + } + self.activityIndicator.isHidden = false + self.updatingAvatarOverlay.isHidden = false + if self.updatingAvatarOverlay.image == nil { + self.updatingAvatarOverlay.image = generateFilledCircleImage(diameter: 100.0, color: UIColor(white: 0.0, alpha: 0.4), backgroundColor: nil) + } + } else { + overrideImage = .editAvatarIcon + self.activityIndicator.isHidden = true + self.updatingAvatarOverlay.isHidden = true + } + } else { + overrideImage = nil + self.activityIndicator.isHidden = true + self.updatingAvatarOverlay.isHidden = true + } + self.avatarNode.setPeer(context: self.context, theme: theme, peer: peer, overrideImage: overrideImage, synchronousLoad: false, displayDimensions: CGSize(width: 100.0, height: 100.0)) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.avatarNode.frame.contains(point) { + return self.avatarNode.view + } + return super.hitTest(point, with: event) + } +} + +final class PeerInfoAvatarListNode: ASDisplayNode { + let avatarContainerNode: PeerInfoAvatarTransformContainerNode + let listContainerTransformNode: ASDisplayNode + let listContainerNode: PeerInfoAvatarListContainerNode + + let isReady = Promise() + + init(context: AccountContext, readyWhenGalleryLoads: Bool) { + self.avatarContainerNode = PeerInfoAvatarTransformContainerNode(context: context) + self.listContainerTransformNode = ASDisplayNode() + self.listContainerNode = PeerInfoAvatarListContainerNode(context: context) + self.listContainerNode.clipsToBounds = true + self.listContainerNode.isHidden = true + + super.init() + + self.addSubnode(self.avatarContainerNode) + self.listContainerTransformNode.addSubnode(self.listContainerNode) + self.addSubnode(self.listContainerTransformNode) + + let avatarReady = self.avatarContainerNode.avatarNode.ready + |> mapToSignal { _ -> Signal in + return .complete() + } + |> then(.single(true)) + + let galleryReady = self.listContainerNode.isReady.get() + |> filter { $0 } + |> take(1) + + let combinedSignal: Signal + if readyWhenGalleryLoads { + combinedSignal = combineLatest(queue: .mainQueue(), + avatarReady, + galleryReady + ) + |> map { lhs, rhs in + return lhs && rhs + } + } else { + combinedSignal = avatarReady + } + + self.isReady.set(combinedSignal + |> filter { $0 } + |> take(1)) + } + + func update(size: CGSize, isExpanded: Bool, peer: Peer?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) { + self.avatarContainerNode.update(peer: peer, theme: theme) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.listContainerNode.isHidden { + if let result = self.listContainerNode.view.hitTest(self.view.convert(point, to: self.listContainerNode.view), with: event) { + return result + } + } else { + if let result = self.avatarContainerNode.avatarNode.view.hitTest(self.view.convert(point, to: self.avatarContainerNode.avatarNode.view), with: event) { + return result + } + } + + return super.hitTest(point, with: event) + } + + func animateAvatarCollapse(transition: ContainedViewLayoutTransition) { + if let currentItemNode = self.listContainerNode.currentItemNode, let unroundedImage = self.avatarContainerNode.avatarNode.unroundedImage, case let .animated(duration, curve) = transition { + let avatarCopyView = UIImageView() + avatarCopyView.image = unroundedImage + avatarCopyView.frame = self.avatarContainerNode.avatarNode.frame + avatarCopyView.center = currentItemNode.imageNode.position + currentItemNode.view.addSubview(avatarCopyView) + let scale = currentItemNode.imageNode.bounds.height / avatarCopyView.bounds.height + avatarCopyView.layer.transform = CATransform3DMakeScale(scale, scale, scale) + avatarCopyView.alpha = 0.0 + transition.updateAlpha(layer: avatarCopyView.layer, alpha: 1.0, completion: { [weak avatarCopyView] _ in + Queue.mainQueue().after(0.1, { + avatarCopyView?.removeFromSuperview() + }) + }) + } + } +} + +final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { + private let regularTextNode: ImmediateTextNode + private let whiteTextNode: ImmediateTextNode + private let iconNode: ASImageNode + + private var key: PeerInfoHeaderNavigationButtonKey? + private var theme: PresentationTheme? + + var isWhite: Bool = false { + didSet { + if self.isWhite != oldValue { + self.regularTextNode.isHidden = self.isWhite + self.whiteTextNode.isHidden = !self.isWhite + } + } + } + + var action: (() -> Void)? + + override init() { + self.regularTextNode = ImmediateTextNode() + self.whiteTextNode = ImmediateTextNode() + self.whiteTextNode.isHidden = true + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + + super.init() + + self.addSubnode(self.regularTextNode) + self.addSubnode(self.whiteTextNode) + self.addSubnode(self.iconNode) + + self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + } + + @objc private func pressed() { + self.action?() + } + + func update(key: PeerInfoHeaderNavigationButtonKey, presentationData: PresentationData) -> CGSize { + let textSize: CGSize + if self.key != key || self.theme !== presentationData.theme { + self.key = key + self.theme = presentationData.theme + + let text: String + var icon: UIImage? + var isBold = false + switch key { + case .edit: + text = presentationData.strings.Common_Edit + case .done, .cancel, .selectionDone: + text = presentationData.strings.Common_Done + isBold = true + case .select: + text = presentationData.strings.Common_Select + case .search: + text = "" + icon = PresentationResourcesRootController.navigationCompactSearchIcon(presentationData.theme) + } + + let font: UIFont = isBold ? Font.semibold(17.0) : Font.regular(17.0) + + self.regularTextNode.attributedText = NSAttributedString(string: text, font: font, textColor: presentationData.theme.rootController.navigationBar.accentTextColor) + self.whiteTextNode.attributedText = NSAttributedString(string: text, font: font, textColor: .white) + self.iconNode.image = icon + + textSize = self.regularTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + let _ = self.whiteTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + } else { + textSize = self.regularTextNode.bounds.size + } + + let inset: CGFloat = 0.0 + let height: CGFloat = 44.0 + + let textFrame = CGRect(origin: CGPoint(x: inset, y: floor((height - textSize.height) / 2.0)), size: textSize) + self.regularTextNode.frame = textFrame + self.whiteTextNode.frame = textFrame + + if let image = self.iconNode.image { + self.iconNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((height - image.size.height) / 2.0)), size: image.size) + + return CGSize(width: image.size.width + inset * 2.0, height: height) + } else { + return CGSize(width: textSize.width + inset * 2.0, height: height) + } + } +} + +enum PeerInfoHeaderNavigationButtonKey { + case edit + case done + case cancel + case select + case selectionDone + case search +} + +struct PeerInfoHeaderNavigationButtonSpec: Equatable { + let key: PeerInfoHeaderNavigationButtonKey + let isForExpandedView: Bool +} + +final class PeerInfoHeaderNavigationButtonContainerNode: ASDisplayNode { + private var buttonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:] + + private var currentButtons: [PeerInfoHeaderNavigationButtonSpec] = [] + + var isWhite: Bool = false { + didSet { + if self.isWhite != oldValue { + for (_, buttonNode) in self.buttonNodes { + buttonNode.isWhite = self.isWhite + } + } + } + } + + var performAction: ((PeerInfoHeaderNavigationButtonKey) -> Void)? + + override init() { + super.init() + } + + func update(size: CGSize, presentationData: PresentationData, buttons: [PeerInfoHeaderNavigationButtonSpec], expandFraction: CGFloat, transition: ContainedViewLayoutTransition) { + let maximumExpandOffset: CGFloat = 14.0 + let expandOffset: CGFloat = -expandFraction * maximumExpandOffset + if self.currentButtons != buttons { + self.currentButtons = buttons + + var nextRegularButtonOrigin = size.width - 16.0 + var nextExpandedButtonOrigin = size.width - 16.0 + for spec in buttons.reversed() { + let buttonNode: PeerInfoHeaderNavigationButton + var wasAdded = false + if let current = self.buttonNodes[spec.key] { + buttonNode = current + } else { + wasAdded = true + buttonNode = PeerInfoHeaderNavigationButton() + self.buttonNodes[spec.key] = buttonNode + self.addSubnode(buttonNode) + buttonNode.isWhite = self.isWhite + buttonNode.action = { [weak self] in + self?.performAction?(spec.key) + } + } + let buttonSize = buttonNode.update(key: spec.key, presentationData: presentationData) + var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin + let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin - buttonSize.width, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) + nextButtonOrigin -= buttonSize.width + 4.0 + if spec.isForExpandedView { + nextExpandedButtonOrigin = nextButtonOrigin + } else { + nextRegularButtonOrigin = nextButtonOrigin + } + let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) + if wasAdded { + buttonNode.frame = buttonFrame + buttonNode.alpha = 0.0 + transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) + } else { + transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) + transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) + } + } + var removeKeys: [PeerInfoHeaderNavigationButtonKey] = [] + for (key, _) in self.buttonNodes { + if !buttons.contains(where: { $0.key == key }) { + removeKeys.append(key) + } + } + for key in removeKeys { + if let buttonNode = self.buttonNodes.removeValue(forKey: key) { + buttonNode.removeFromSupernode() + } + } + } else { + var nextRegularButtonOrigin = size.width - 16.0 + var nextExpandedButtonOrigin = size.width - 16.0 + for spec in buttons.reversed() { + if let buttonNode = self.buttonNodes[spec.key] { + let buttonSize = buttonNode.bounds.size + var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin + let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin - buttonSize.width, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) + nextButtonOrigin -= buttonSize.width + 4.0 + if spec.isForExpandedView { + nextExpandedButtonOrigin = nextButtonOrigin + } else { + nextRegularButtonOrigin = nextButtonOrigin + } + transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) + let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) + transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) + } + } + } + } +} + +final class PeerInfoHeaderRegularContentNode: ASDisplayNode { + +} + +enum PeerInfoHeaderTextFieldNodeKey { + case firstName + case lastName + case title + case description +} + +protocol PeerInfoHeaderTextFieldNode: ASDisplayNode { + var text: String { get } + + func update(width: CGFloat, safeInset: CGFloat, hasPrevious: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat +} + +final class PeerInfoHeaderSingleLineTextFieldNode: ASDisplayNode, PeerInfoHeaderTextFieldNode, UITextFieldDelegate { + private let textNode: TextFieldNode + private let clearIconNode: ASImageNode + private let clearButtonNode: HighlightableButtonNode + private let topSeparator: ASDisplayNode + + private var theme: PresentationTheme? + + var text: String { + return self.textNode.textField.text ?? "" + } + + override init() { + self.textNode = TextFieldNode() + + self.clearIconNode = ASImageNode() + self.clearIconNode.isLayerBacked = true + self.clearIconNode.displayWithoutProcessing = true + self.clearIconNode.displaysAsynchronously = false + self.clearIconNode.isHidden = true + + self.clearButtonNode = HighlightableButtonNode() + self.clearButtonNode.isHidden = true + + self.topSeparator = ASDisplayNode() + + super.init() + + self.addSubnode(self.textNode) + self.addSubnode(self.clearIconNode) + self.addSubnode(self.clearButtonNode) + self.addSubnode(self.topSeparator) + + self.textNode.textField.delegate = self + + self.clearButtonNode.addTarget(self, action: #selector(self.clearButtonPressed), forControlEvents: .touchUpInside) + self.clearButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.clearIconNode.layer.removeAnimation(forKey: "opacity") + strongSelf.clearIconNode.alpha = 0.4 + } else { + strongSelf.clearIconNode.alpha = 1.0 + strongSelf.clearIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + @objc private func clearButtonPressed() { + self.textNode.textField.text = "" + self.updateClearButtonVisibility() + } + + @objc func textFieldDidBeginEditing(_ textField: UITextField) { + self.updateClearButtonVisibility() + } + + @objc func textFieldDidEndEditing(_ textField: UITextField) { + self.updateClearButtonVisibility() + } + + private func updateClearButtonVisibility() { + let isHidden = !self.textNode.textField.isFirstResponder || self.text.isEmpty + self.clearIconNode.isHidden = isHidden + self.clearButtonNode.isHidden = isHidden + self.clearButtonNode.isAccessibilityElement = isHidden + } + + func update(width: CGFloat, safeInset: CGFloat, hasPrevious: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat { + if self.theme !== presentationData.theme { + self.theme = presentationData.theme + self.textNode.textField.textColor = presentationData.theme.list.itemPrimaryTextColor + self.textNode.textField.keyboardAppearance = presentationData.theme.rootController.keyboardColor.keyboardAppearance + self.textNode.textField.tintColor = presentationData.theme.list.itemAccentColor + + self.clearIconNode.image = PresentationResourcesItemList.itemListClearInputIcon(presentationData.theme) + } + + let attributedPlaceholderText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: presentationData.theme.list.itemPlaceholderTextColor) + if self.textNode.textField.attributedPlaceholder == nil || !self.textNode.textField.attributedPlaceholder!.isEqual(to: attributedPlaceholderText) { + self.textNode.textField.attributedPlaceholder = attributedPlaceholderText + self.textNode.textField.accessibilityHint = attributedPlaceholderText.string + } + + if let updateText = updateText { + self.textNode.textField.text = updateText + } + + self.topSeparator.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + self.topSeparator.frame = CGRect(origin: CGPoint(x: safeInset + (hasPrevious ? 16.0 : 0.0), y: 0.0), size: CGSize(width: width, height: UIScreenPixel)) + + let height: CGFloat = 44.0 + + let buttonSize = CGSize(width: 38.0, height: height) + self.clearButtonNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width, y: 0.0), size: buttonSize) + if let image = self.clearIconNode.image { + self.clearIconNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width + floor((buttonSize.width - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size) + } + + self.textNode.frame = CGRect(origin: CGPoint(x: safeInset + 16.0, y: floor((height - 40.0) / 2.0)), size: CGSize(width: max(1.0, width - 16.0 * 2.0 - 32.0), height: 40.0)) + + self.textNode.isUserInteractionEnabled = isEnabled + self.textNode.alpha = isEnabled ? 1.0 : 0.6 + + return height + } +} + +final class PeerInfoHeaderMultiLineTextFieldNode: ASDisplayNode, PeerInfoHeaderTextFieldNode, ASEditableTextNodeDelegate { + private let textNode: EditableTextNode + private let textNodeContainer: ASDisplayNode + private let measureTextNode: ImmediateTextNode + private let clearIconNode: ASImageNode + private let clearButtonNode: HighlightableButtonNode + private let topSeparator: ASDisplayNode + + private let requestUpdateHeight: () -> Void + + private var theme: PresentationTheme? + private var currentParams: (width: CGFloat, safeInset: CGFloat)? + private var currentMeasuredHeight: CGFloat? + + var text: String { + return self.textNode.attributedText?.string ?? "" + } + + init(requestUpdateHeight: @escaping () -> Void) { + self.requestUpdateHeight = requestUpdateHeight + + self.textNode = EditableTextNode() + self.textNode.clipsToBounds = false + self.textNode.textView.clipsToBounds = false + self.textNode.textContainerInset = UIEdgeInsets() + + self.textNodeContainer = ASDisplayNode() + self.measureTextNode = ImmediateTextNode() + self.measureTextNode.maximumNumberOfLines = 0 + self.topSeparator = ASDisplayNode() + + self.clearIconNode = ASImageNode() + self.clearIconNode.isLayerBacked = true + self.clearIconNode.displayWithoutProcessing = true + self.clearIconNode.displaysAsynchronously = false + self.clearIconNode.isHidden = true + + self.clearButtonNode = HighlightableButtonNode() + self.clearButtonNode.isHidden = true + + super.init() + + self.textNodeContainer.addSubnode(self.textNode) + self.addSubnode(self.textNodeContainer) + self.addSubnode(self.clearIconNode) + self.addSubnode(self.clearButtonNode) + self.addSubnode(self.topSeparator) + + self.clearButtonNode.addTarget(self, action: #selector(self.clearButtonPressed), forControlEvents: .touchUpInside) + self.clearButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.clearIconNode.layer.removeAnimation(forKey: "opacity") + strongSelf.clearIconNode.alpha = 0.4 + } else { + strongSelf.clearIconNode.alpha = 1.0 + strongSelf.clearIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + @objc private func clearButtonPressed() { + guard let theme = self.theme else { + return + } + let attributedText = NSAttributedString(string: "", font: Font.regular(17.0), textColor: theme.list.itemPrimaryTextColor) + self.textNode.attributedText = attributedText + self.requestUpdateHeight() + self.updateClearButtonVisibility() + } + + func update(width: CGFloat, safeInset: CGFloat, hasPrevious: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat { + self.currentParams = (width, safeInset) + + if self.theme !== presentationData.theme { + self.theme = presentationData.theme + let textColor = presentationData.theme.list.itemPrimaryTextColor + + self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: textColor] + + self.textNode.clipsToBounds = true + self.textNode.delegate = self + self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) + + self.clearIconNode.image = PresentationResourcesItemList.itemListClearInputIcon(presentationData.theme) + } + + self.topSeparator.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + self.topSeparator.frame = CGRect(origin: CGPoint(x: safeInset + (hasPrevious ? 16.0 : 0.0), y: 0.0), size: CGSize(width: width, height: UIScreenPixel)) + + let attributedPlaceholderText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: presentationData.theme.list.itemPlaceholderTextColor) + if self.textNode.attributedPlaceholderText == nil || !self.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) { + self.textNode.attributedPlaceholderText = attributedPlaceholderText + } + + if let updateText = updateText { + let attributedText = NSAttributedString(string: updateText, font: Font.regular(17.0), textColor: presentationData.theme.list.itemPrimaryTextColor) + self.textNode.attributedText = attributedText + } + + var measureText = self.textNode.attributedText?.string ?? "" + if measureText.hasSuffix("\n") || measureText.isEmpty { + measureText += "|" + } + let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), textColor: .black) + self.measureTextNode.attributedText = attributedMeasureText + let measureTextSize = self.measureTextNode.updateLayout(CGSize(width: width - safeInset * 2.0 - 16.0 * 2.0 - 38.0, height: .greatestFiniteMagnitude)) + self.currentMeasuredHeight = measureTextSize.height + + let height = measureTextSize.height + 22.0 + + let buttonSize = CGSize(width: 38.0, height: height) + self.clearButtonNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width, y: 0.0), size: buttonSize) + if let image = self.clearIconNode.image { + self.clearIconNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width + floor((buttonSize.width - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size) + } + + let textNodeFrame = CGRect(origin: CGPoint(x: safeInset + 16.0, y: 10.0), size: CGSize(width: width - safeInset * 2.0 - 16.0 * 2.0 - 38.0, height: max(height, 1000.0))) + self.textNodeContainer.frame = textNodeFrame + self.textNode.frame = CGRect(origin: CGPoint(), size: textNodeFrame.size) + + return height + } + + func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + self.updateClearButtonVisibility() + } + + func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { + self.updateClearButtonVisibility() + } + + private func updateClearButtonVisibility() { + let isHidden = !self.textNode.isFirstResponder() || self.text.isEmpty + self.clearIconNode.isHidden = isHidden + self.clearButtonNode.isHidden = isHidden + self.clearButtonNode.isAccessibilityElement = isHidden + } + + func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + guard let theme = self.theme else { + return true + } + let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text) + if updatedText.count > 255 { + let attributedText = NSAttributedString(string: String(updatedText[updatedText.startIndex.. 0.1 { + self.requestUpdateHeight() + } + } + } + + func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { + let text: String? = UIPasteboard.general.string + if let _ = text { + return true + } else { + return false + } + } +} + +final class PeerInfoHeaderEditingContentNode: ASDisplayNode { + private let context: AccountContext + private let requestUpdateLayout: () -> Void + + let avatarNode: PeerInfoEditingAvatarNode + + var itemNodes: [PeerInfoHeaderTextFieldNodeKey: PeerInfoHeaderTextFieldNode] = [:] + + init(context: AccountContext, requestUpdateLayout: @escaping () -> Void) { + self.context = context + self.requestUpdateLayout = requestUpdateLayout + + self.avatarNode = PeerInfoEditingAvatarNode(context: context) + + super.init() + + self.addSubnode(self.avatarNode) + } + + func editingTextForKey(_ key: PeerInfoHeaderTextFieldNodeKey) -> String? { + return self.itemNodes[key]?.text + } + + func shakeTextForKey(_ key: PeerInfoHeaderTextFieldNodeKey) { + self.itemNodes[key]?.layer.addShakeAnimation() + } + + func update(width: CGFloat, safeInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, peer: Peer?, cachedData: CachedPeerData?, isContact: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat { + let avatarSize: CGFloat = 100.0 + let avatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize) / 2.0), y: statusBarHeight + 10.0), size: CGSize(width: avatarSize, height: avatarSize)) + transition.updateFrameAdditiveToCenter(node: self.avatarNode, frame: CGRect(origin: avatarFrame.center, size: CGSize())) + + var contentHeight: CGFloat = statusBarHeight + 10.0 + 100.0 + 20.0 + + var fieldKeys: [PeerInfoHeaderTextFieldNodeKey] = [] + if let user = peer as? TelegramUser { + if !user.isDeleted { + fieldKeys.append(.firstName) + if user.botInfo == nil { + fieldKeys.append(.lastName) + } + } + } else if let _ = peer as? TelegramGroup { + fieldKeys.append(.title) + if canEditPeerInfo(peer: peer) { + fieldKeys.append(.description) + } + } else if let _ = peer as? TelegramChannel { + fieldKeys.append(.title) + if canEditPeerInfo(peer: peer) { + fieldKeys.append(.description) + } + } + var hasPrevious = false + for key in fieldKeys { + let itemNode: PeerInfoHeaderTextFieldNode + var updateText: String? + if let current = self.itemNodes[key] { + itemNode = current + } else { + var isMultiline = false + switch key { + case .firstName: + updateText = (peer as? TelegramUser)?.firstName ?? "" + case .lastName: + updateText = (peer as? TelegramUser)?.lastName ?? "" + case .title: + updateText = peer?.debugDisplayTitle ?? "" + case .description: + isMultiline = true + if let cachedData = cachedData as? CachedChannelData { + updateText = cachedData.about ?? "" + } else if let cachedData = cachedData as? CachedGroupData { + updateText = cachedData.about ?? "" + } else { + updateText = "" + } + } + if isMultiline { + itemNode = PeerInfoHeaderMultiLineTextFieldNode(requestUpdateHeight: { [weak self] in + self?.requestUpdateLayout() + }) + } else { + itemNode = PeerInfoHeaderSingleLineTextFieldNode() + } + self.itemNodes[key] = itemNode + self.addSubnode(itemNode) + } + let placeholder: String + var isEnabled = true + switch key { + case .firstName: + placeholder = presentationData.strings.UserInfo_FirstNamePlaceholder + isEnabled = isContact + case .lastName: + placeholder = presentationData.strings.UserInfo_LastNamePlaceholder + isEnabled = isContact + case .title: + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + placeholder = presentationData.strings.GroupInfo_ChannelListNamePlaceholder + } else { + placeholder = presentationData.strings.GroupInfo_GroupNamePlaceholder + } + isEnabled = canEditPeerInfo(peer: peer) + case .description: + placeholder = presentationData.strings.Channel_Edit_AboutItem + isEnabled = canEditPeerInfo(peer: peer) + } + let itemHeight = itemNode.update(width: width, safeInset: safeInset, hasPrevious: hasPrevious, placeholder: placeholder, isEnabled: isEnabled, presentationData: presentationData, updateText: updateText) + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: itemHeight))) + contentHeight += itemHeight + hasPrevious = true + } + var removeKeys: [PeerInfoHeaderTextFieldNodeKey] = [] + for (key, _) in self.itemNodes { + if !fieldKeys.contains(key) { + removeKeys.append(key) + } + } + for key in removeKeys { + if let itemNode = self.itemNodes.removeValue(forKey: key) { + itemNode.removeFromSupernode() + } + } + + return contentHeight + } +} + +private let TitleNodeStateRegular = 0 +private let TitleNodeStateExpanded = 1 + +final class PeerInfoHeaderNode: ASDisplayNode { + private var context: AccountContext + private var presentationData: PresentationData? + + private let isOpenedFromChat: Bool + + private(set) var isAvatarExpanded: Bool + + let avatarListNode: PeerInfoAvatarListNode + + let regularContentNode: PeerInfoHeaderRegularContentNode + let editingContentNode: PeerInfoHeaderEditingContentNode + let titleNodeContainer: ASDisplayNode + let titleNodeRawContainer: ASDisplayNode + let titleNode: MultiScaleTextNode + let titleCredibilityIconNode: ASImageNode + let titleExpandedCredibilityIconNode: ASImageNode + let subtitleNodeContainer: ASDisplayNode + let subtitleNodeRawContainer: ASDisplayNode + let subtitleNode: MultiScaleTextNode + private var buttonNodes: [PeerInfoHeaderButtonKey: PeerInfoHeaderButtonNode] = [:] + private let backgroundNode: ASDisplayNode + private let expandedBackgroundNode: ASDisplayNode + let separatorNode: ASDisplayNode + let navigationButtonContainer: PeerInfoHeaderNavigationButtonContainerNode + + var performButtonAction: ((PeerInfoHeaderButtonKey) -> Void)? + var requestAvatarExpansion: (([AvatarGalleryEntry], AvatarGalleryEntry?, (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?) -> Void)? + var requestOpenAvatarForEditing: (() -> Void)? + var requestUpdateLayout: (() -> Void)? + + var navigationTransition: PeerInfoHeaderNavigationTransition? + + init(context: AccountContext, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool) { + self.context = context + self.isAvatarExpanded = avatarInitiallyExpanded + self.isOpenedFromChat = isOpenedFromChat + + self.avatarListNode = PeerInfoAvatarListNode(context: context, readyWhenGalleryLoads: avatarInitiallyExpanded) + + self.titleNodeContainer = ASDisplayNode() + self.titleNodeRawContainer = ASDisplayNode() + self.titleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) + self.titleNode.displaysAsynchronously = false + + self.titleCredibilityIconNode = ASImageNode() + self.titleCredibilityIconNode.displaysAsynchronously = false + self.titleCredibilityIconNode.displayWithoutProcessing = true + self.titleNode.stateNode(forKey: TitleNodeStateRegular)?.addSubnode(self.titleCredibilityIconNode) + + self.titleExpandedCredibilityIconNode = ASImageNode() + self.titleExpandedCredibilityIconNode.displaysAsynchronously = false + self.titleExpandedCredibilityIconNode.displayWithoutProcessing = true + self.titleNode.stateNode(forKey: TitleNodeStateExpanded)?.addSubnode(self.titleExpandedCredibilityIconNode) + + self.subtitleNodeContainer = ASDisplayNode() + self.subtitleNodeRawContainer = ASDisplayNode() + self.subtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) + self.subtitleNode.displaysAsynchronously = false + + self.regularContentNode = PeerInfoHeaderRegularContentNode() + var requestUpdateLayoutImpl: (() -> Void)? + self.editingContentNode = PeerInfoHeaderEditingContentNode(context: context, requestUpdateLayout: { + requestUpdateLayoutImpl?() + }) + self.editingContentNode.alpha = 0.0 + + self.navigationButtonContainer = PeerInfoHeaderNavigationButtonContainerNode() + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.expandedBackgroundNode = ASDisplayNode() + self.expandedBackgroundNode.isLayerBacked = true + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + super.init() + + requestUpdateLayoutImpl = { [weak self] in + self?.requestUpdateLayout?() + } + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.expandedBackgroundNode) + self.addSubnode(self.separatorNode) + self.titleNodeContainer.addSubnode(self.titleNode) + self.regularContentNode.addSubnode(self.titleNodeContainer) + self.subtitleNodeContainer.addSubnode(self.subtitleNode) + self.regularContentNode.addSubnode(self.subtitleNodeContainer) + self.regularContentNode.addSubnode(self.avatarListNode) + self.regularContentNode.addSubnode(self.avatarListNode.listContainerNode.controlsClippingOffsetNode) + self.addSubnode(self.regularContentNode) + self.addSubnode(self.editingContentNode) + self.addSubnode(self.navigationButtonContainer) + + self.avatarListNode.avatarContainerNode.tapped = { [weak self] in + self?.initiateAvatarExpansion() + } + self.editingContentNode.avatarNode.tapped = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.requestOpenAvatarForEditing?() + } + } + + func initiateAvatarExpansion() { + if self.isAvatarExpanded { + if let currentEntry = self.avatarListNode.listContainerNode.currentEntry { + self.requestAvatarExpansion?(self.avatarListNode.listContainerNode.galleryEntries, self.avatarListNode.listContainerNode.currentEntry, self.avatarTransitionArguments(entry: currentEntry)) + } + } else if let entry = self.avatarListNode.listContainerNode.galleryEntries.first{ + let avatarNode = self.avatarListNode.avatarContainerNode.avatarNode + self.requestAvatarExpansion?(self.avatarListNode.listContainerNode.galleryEntries, nil, self.avatarTransitionArguments(entry: entry)) + } + } + + func avatarTransitionArguments(entry: AvatarGalleryEntry) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + if self.isAvatarExpanded { + if let avatarNode = self.avatarListNode.listContainerNode.currentItemNode?.imageNode { + return (avatarNode, avatarNode.bounds, { [weak avatarNode] in + return (avatarNode?.view.snapshotContentTree(unhide: true), nil) + }) + } else { + return nil + } + } else if entry == self.avatarListNode.listContainerNode.galleryEntries.first { + let avatarNode = self.avatarListNode.avatarContainerNode.avatarNode + return (avatarNode, avatarNode.bounds, { [weak avatarNode] in + return (avatarNode?.view.snapshotContentTree(unhide: true), nil) + }) + } else { + return nil + } + } + + func addToAvatarTransitionSurface(view: UIView) { + if self.isAvatarExpanded { + self.avatarListNode.listContainerNode.view.addSubview(view) + } else { + self.view.addSubview(view) + } + } + + func updateAvatarIsHidden(entry: AvatarGalleryEntry?) { + if let entry = entry { + self.avatarListNode.avatarContainerNode.avatarNode.isHidden = entry == self.avatarListNode.listContainerNode.galleryEntries.first + } else { + self.avatarListNode.avatarContainerNode.avatarNode.isHidden = false + } + self.avatarListNode.listContainerNode.updateEntryIsHidden(entry: entry) + } + + func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, contentOffset: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, notificationSettings: TelegramPeerNotificationSettings?, statusData: PeerInfoStatusData?, isContact: Bool, state: PeerInfoState, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat { + let themeUpdated = self.presentationData?.theme !== presentationData.theme + self.presentationData = presentationData + + if themeUpdated { + if let sourceImage = UIImage(bundleImageName: "Peer Info/VerifiedIcon") { + let image = generateImage(sourceImage.size, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(presentationData.theme.list.itemCheckColors.foregroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: 7.0, dy: 7.0)) + context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) + context.clip(to: CGRect(origin: CGPoint(), size: size), mask: sourceImage.cgImage!) + context.fill(CGRect(origin: CGPoint(), size: size)) + }) + self.titleCredibilityIconNode.image = image + self.titleExpandedCredibilityIconNode.image = image + } + } + + self.regularContentNode.alpha = state.isEditing ? 0.0 : 1.0 + self.editingContentNode.alpha = state.isEditing ? 1.0 : 0.0 + + let editingContentHeight = self.editingContentNode.update(width: width, safeInset: containerInset, statusBarHeight: statusBarHeight, navigationHeight: navigationHeight, peer: state.isEditing ? peer : nil, cachedData: cachedData, isContact: isContact, presentationData: presentationData, transition: transition) + transition.updateFrame(node: self.editingContentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -contentOffset), size: CGSize(width: width, height: editingContentHeight))) + + var transitionSourceHeight: CGFloat = 0.0 + var transitionFraction: CGFloat = 0.0 + var transitionSourceAvatarFrame = CGRect() + var transitionSourceTitleFrame = CGRect() + var transitionSourceSubtitleFrame = CGRect() + + self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor + self.expandedBackgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor + + if let navigationTransition = self.navigationTransition, let sourceAvatarNode = (navigationTransition.sourceNavigationBar.rightButtonNode.singleCustomNode as? ChatAvatarNavigationNode)?.avatarNode { + transitionSourceHeight = navigationTransition.sourceNavigationBar.bounds.height + transitionFraction = navigationTransition.fraction + transitionSourceAvatarFrame = sourceAvatarNode.view.convert(sourceAvatarNode.view.bounds, to: navigationTransition.sourceNavigationBar.view) + transitionSourceTitleFrame = navigationTransition.sourceTitleFrame + transitionSourceSubtitleFrame = navigationTransition.sourceSubtitleFrame + + transition.updateAlpha(node: self.expandedBackgroundNode, alpha: transitionFraction) + + if self.isAvatarExpanded, case .animated = transition, transitionFraction == 1.0 { + self.avatarListNode.animateAvatarCollapse(transition: transition) + } + } else { + let backgroundTransitionFraction: CGFloat = max(0.0, min(1.0, contentOffset / (212.0))) + transition.updateAlpha(node: self.expandedBackgroundNode, alpha: backgroundTransitionFraction) + } + + self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + let defaultButtonSize: CGFloat = 40.0 + let defaultMaxButtonSpacing: CGFloat = 40.0 + let expandedAvatarControlsHeight: CGFloat = 61.0 + let expandedAvatarListHeight = min(width, containerHeight - expandedAvatarControlsHeight) + let expandedAvatarListSize = CGSize(width: width, height: expandedAvatarListHeight) + + let buttonKeys: [PeerInfoHeaderButtonKey] = peerInfoHeaderButtons(peer: peer, cachedData: cachedData, isOpenedFromChat: self.isOpenedFromChat) + + var isVerified = false + let titleString: NSAttributedString + let subtitleString: NSAttributedString + if let peer = peer, peer.isVerified { + isVerified = true + } + + if let peer = peer { + if peer.id == self.context.account.peerId { + titleString = NSAttributedString(string: presentationData.strings.Conversation_SavedMessages, font: Font.medium(24.0), textColor: presentationData.theme.list.itemPrimaryTextColor) + } else { + titleString = NSAttributedString(string: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.medium(24.0), textColor: presentationData.theme.list.itemPrimaryTextColor) + } + + if let statusData = statusData { + let subtitleColor: UIColor + if statusData.isActivity { + subtitleColor = presentationData.theme.list.itemAccentColor + } else { + subtitleColor = presentationData.theme.list.itemSecondaryTextColor + } + subtitleString = NSAttributedString(string: statusData.text, font: Font.regular(15.0), textColor: subtitleColor) + } else { + subtitleString = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) + } + } else { + titleString = NSAttributedString(string: " ", font: Font.medium(24.0), textColor: presentationData.theme.list.itemPrimaryTextColor) + subtitleString = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) + } + + let textSideInset: CGFloat = 44.0 + let expandedAvatarHeight: CGFloat = expandedAvatarListSize.height + expandedAvatarControlsHeight + + let titleConstrainedSize = CGSize(width: width - textSideInset * 2.0 - (isVerified ? 16.0 : 0.0), height: .greatestFiniteMagnitude) + + let titleNodeLayout = self.titleNode.updateLayout(states: [ + TitleNodeStateRegular: MultiScaleTextState(attributedText: titleString, constrainedSize: titleConstrainedSize), + TitleNodeStateExpanded: MultiScaleTextState(attributedText: titleString, constrainedSize: CGSize(width: titleConstrainedSize.width, height: titleConstrainedSize.height)) + ], mainState: TitleNodeStateRegular) + + let subtitleNodeLayout = self.subtitleNode.updateLayout(states: [ + TitleNodeStateRegular: MultiScaleTextState(attributedText: subtitleString, constrainedSize: titleConstrainedSize), + TitleNodeStateExpanded: MultiScaleTextState(attributedText: subtitleString, constrainedSize: CGSize(width: titleConstrainedSize.width - 82.0, height: titleConstrainedSize.height)) + ], mainState: TitleNodeStateRegular) + + self.titleNode.update(stateFractions: [ + TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, + TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 + ], transition: transition) + + self.subtitleNode.update(stateFractions: [ + TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, + TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 + ], transition: transition) + + let avatarSize: CGFloat = 100.0 + let avatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize) / 2.0), y: statusBarHeight + 10.0), size: CGSize(width: avatarSize, height: avatarSize)) + let avatarCenter = CGPoint(x: (1.0 - transitionFraction) * avatarFrame.midX + transitionFraction * transitionSourceAvatarFrame.midX, y: (1.0 - transitionFraction) * avatarFrame.midY + transitionFraction * transitionSourceAvatarFrame.midY) + + let titleSize = titleNodeLayout[TitleNodeStateRegular]!.size + let titleExpandedSize = titleNodeLayout[TitleNodeStateExpanded]!.size + let subtitleSize = subtitleNodeLayout[TitleNodeStateRegular]!.size + + if let image = self.titleCredibilityIconNode.image { + transition.updateFrame(node: self.titleCredibilityIconNode, frame: CGRect(origin: CGPoint(x: titleSize.width + 4.0, y: floor((titleSize.height - image.size.height) / 2.0) + 1.0), size: image.size)) + self.titleCredibilityIconNode.isHidden = !isVerified + + transition.updateFrame(node: self.titleExpandedCredibilityIconNode, frame: CGRect(origin: CGPoint(x: titleExpandedSize.width + 4.0, y: floor((titleExpandedSize.height - image.size.height) / 2.0) + 1.0), size: image.size)) + self.titleExpandedCredibilityIconNode.isHidden = !isVerified + } + + let titleFrame: CGRect + let subtitleFrame: CGRect + if self.isAvatarExpanded { + let minTitleSize = CGSize(width: titleSize.width * 0.7, height: titleSize.height * 0.7) + let minTitleFrame = CGRect(origin: CGPoint(x: 16.0, y: expandedAvatarHeight - expandedAvatarControlsHeight + 9.0 + (subtitleSize.height.isZero ? 10.0 : 0.0)), size: minTitleSize) + titleFrame = CGRect(origin: CGPoint(x: minTitleFrame.midX - titleSize.width / 2.0, y: minTitleFrame.midY - titleSize.height / 2.0), size: titleSize) + subtitleFrame = CGRect(origin: CGPoint(x: 16.0, y: minTitleFrame.maxY + 4.0), size: subtitleSize) + } else { + titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: avatarFrame.maxY + 10.0 + (subtitleSize.height.isZero ? 11.0 : 0.0)), size: titleSize) + subtitleFrame = CGRect(origin: CGPoint(x: floor((width - subtitleSize.width) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize) + } + + let singleTitleLockOffset: CGFloat = (peer?.id == self.context.account.peerId || subtitleSize.height.isZero) ? 8.0 : 0.0 + + let titleLockOffset: CGFloat = 7.0 + singleTitleLockOffset + let titleMaxLockOffset: CGFloat = 7.0 + let titleCollapseOffset = titleFrame.midY - statusBarHeight - titleLockOffset + let titleOffset = -min(titleCollapseOffset, contentOffset) + let titleCollapseFraction = max(0.0, min(1.0, contentOffset / titleCollapseOffset)) + + let titleMinScale: CGFloat = 0.7 + let subtitleMinScale: CGFloat = 0.8 + let avatarMinScale: CGFloat = 0.7 + + let apparentTitleLockOffset = (1.0 - titleCollapseFraction) * 0.0 + titleCollapseFraction * titleMaxLockOffset + + let avatarScale: CGFloat + let avatarOffset: CGFloat + if self.navigationTransition != nil { + avatarScale = ((1.0 - transitionFraction) * avatarFrame.width + transitionFraction * transitionSourceAvatarFrame.width) / avatarFrame.width + avatarOffset = 0.0 + } else { + avatarScale = 1.0 * (1.0 - titleCollapseFraction) + avatarMinScale * titleCollapseFraction + avatarOffset = apparentTitleLockOffset + 0.0 * (1.0 - titleCollapseFraction) + 10.0 * titleCollapseFraction + } + let avatarListFrame = CGRect(origin: CGPoint(), size: expandedAvatarListSize) + + if self.isAvatarExpanded { + self.avatarListNode.listContainerNode.isHidden = false + if !transitionSourceAvatarFrame.width.isZero { + transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: transitionFraction * transitionSourceAvatarFrame.width / 2.0) + transition.updateCornerRadius(node: self.avatarListNode.listContainerNode.controlsClippingNode, cornerRadius: transitionFraction * transitionSourceAvatarFrame.width / 2.0) + } else { + transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: 0.0) + transition.updateCornerRadius(node: self.avatarListNode.listContainerNode.controlsClippingNode, cornerRadius: 0.0) + } + } else if self.avatarListNode.listContainerNode.cornerRadius != 50.0 { + transition.updateCornerRadius(node: self.avatarListNode.listContainerNode.controlsClippingNode, cornerRadius: 50.0) + transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: 50.0, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.avatarListNode.listContainerNode.isHidden = true + }) + } + + self.avatarListNode.update(size: CGSize(), isExpanded: self.isAvatarExpanded, peer: peer, theme: presentationData.theme, transition: transition) + self.editingContentNode.avatarNode.update(peer: peer, updatingAvatar: state.updatingAvatar, theme: presentationData.theme) + if additive { + transition.updateSublayerTransformScaleAdditive(node: self.avatarListNode.avatarContainerNode, scale: avatarScale) + } else { + transition.updateSublayerTransformScale(node: self.avatarListNode.avatarContainerNode, scale: avatarScale) + } + let apparentAvatarFrame: CGRect + let controlsClippingFrame: CGRect + if self.isAvatarExpanded { + let expandedAvatarCenter = CGPoint(x: expandedAvatarListSize.width / 2.0, y: expandedAvatarListSize.height / 2.0 - contentOffset / 2.0) + apparentAvatarFrame = CGRect(origin: CGPoint(x: expandedAvatarCenter.x * (1.0 - transitionFraction) + transitionFraction * avatarCenter.x, y: expandedAvatarCenter.y * (1.0 - transitionFraction) + transitionFraction * avatarCenter.y), size: CGSize()) + if !transitionSourceAvatarFrame.width.isZero { + let expandedFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedAvatarListSize) + controlsClippingFrame = CGRect(origin: CGPoint(x: transitionFraction * transitionSourceAvatarFrame.minX + (1.0 - transitionFraction) * expandedFrame.minX, y: transitionFraction * transitionSourceAvatarFrame.minY + (1.0 - transitionFraction) * expandedFrame.minY), size: CGSize(width: transitionFraction * transitionSourceAvatarFrame.width + (1.0 - transitionFraction) * expandedFrame.width, height: transitionFraction * transitionSourceAvatarFrame.height + (1.0 - transitionFraction) * expandedFrame.height)) + } else { + controlsClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedAvatarListSize) + } + } else { + apparentAvatarFrame = CGRect(origin: CGPoint(x: avatarCenter.x - avatarFrame.width / 2.0, y: -contentOffset + avatarOffset + avatarCenter.y - avatarFrame.height / 2.0), size: avatarFrame.size) + controlsClippingFrame = apparentAvatarFrame + } + if case let .animated(duration, curve) = transition, !transitionSourceAvatarFrame.width.isZero, false { + let previousFrame = self.avatarListNode.frame + self.avatarListNode.frame = CGRect(origin: apparentAvatarFrame.center, size: CGSize()) + let horizontalTransition: ContainedViewLayoutTransition + let verticalTransition: ContainedViewLayoutTransition + if transitionFraction < .ulpOfOne { + horizontalTransition = .animated(duration: duration * 0.85, curve: curve) + verticalTransition = .animated(duration: duration * 1.15, curve: curve) + } else { + horizontalTransition = transition + verticalTransition = .animated(duration: duration * 0.6, curve: curve) + } + horizontalTransition.animatePositionAdditive(node: self.avatarListNode, offset: CGPoint(x: previousFrame.midX - apparentAvatarFrame.midX, y: 0.0)) + verticalTransition.animatePositionAdditive(node: self.avatarListNode, offset: CGPoint(x: 0.0, y: previousFrame.midY - apparentAvatarFrame.midY)) + } else { + transition.updateFrameAdditive(node: self.avatarListNode, frame: CGRect(origin: apparentAvatarFrame.center, size: CGSize())) + } + + let avatarListContainerFrame: CGRect + let avatarListContainerScale: CGFloat + if self.isAvatarExpanded { + if !transitionSourceAvatarFrame.width.isZero { + let neutralAvatarListContainerSize = expandedAvatarListSize + let avatarListContainerSize = CGSize(width: neutralAvatarListContainerSize.width * (1.0 - transitionFraction) + transitionSourceAvatarFrame.width * transitionFraction, height: neutralAvatarListContainerSize.height * (1.0 - transitionFraction) + transitionSourceAvatarFrame.height * transitionFraction) + avatarListContainerFrame = CGRect(origin: CGPoint(x: -avatarListContainerSize.width / 2.0, y: -avatarListContainerSize.height / 2.0), size: avatarListContainerSize) + } else { + avatarListContainerFrame = CGRect(origin: CGPoint(x: -expandedAvatarListSize.width / 2.0, y: -expandedAvatarListSize.height / 2.0), size: expandedAvatarListSize) + } + avatarListContainerScale = 1.0 + max(0.0, -contentOffset / avatarListContainerFrame.height) + } else { + avatarListContainerFrame = CGRect(origin: CGPoint(x: -apparentAvatarFrame.width / 2.0, y: -apparentAvatarFrame.height / 2.0), size: apparentAvatarFrame.size) + avatarListContainerScale = avatarScale + } + transition.updateFrame(node: self.avatarListNode.listContainerNode, frame: avatarListContainerFrame) + let innerScale = avatarListContainerFrame.height / expandedAvatarListSize.height + let innerDeltaX = (avatarListContainerFrame.width - expandedAvatarListSize.width) / 2.0 + let innerDeltaY = (avatarListContainerFrame.height - expandedAvatarListSize.height) / 2.0 + transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerNode, scale: innerScale) + transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.contentNode, frame: CGRect(origin: CGPoint(x: innerDeltaX + expandedAvatarListSize.width / 2.0, y: innerDeltaY + expandedAvatarListSize.height / 2.0), size: CGSize())) + + transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.controlsClippingOffsetNode, frame: CGRect(origin: controlsClippingFrame.center, size: CGSize())) + transition.updateFrame(node: self.avatarListNode.listContainerNode.controlsClippingNode, frame: CGRect(origin: CGPoint(x: -controlsClippingFrame.width / 2.0, y: -controlsClippingFrame.height / 2.0), size: controlsClippingFrame.size)) + transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.controlsContainerNode, frame: CGRect(origin: CGPoint(x: -controlsClippingFrame.minX, y: -controlsClippingFrame.minY), size: CGSize(width: expandedAvatarListSize.width, height: expandedAvatarListSize.height))) + + transition.updateFrame(node: self.avatarListNode.listContainerNode.shadowNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: expandedAvatarListSize.width, height: navigationHeight + 20.0))) + transition.updateFrame(node: self.avatarListNode.listContainerNode.stripContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: statusBarHeight < 25.0 ? (statusBarHeight + 2.0) : (statusBarHeight - 3.0)), size: CGSize(width: expandedAvatarListSize.width, height: 2.0))) + transition.updateFrame(node: self.avatarListNode.listContainerNode.highlightContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: expandedAvatarListSize.width, height: expandedAvatarListSize.height))) + transition.updateAlpha(node: self.avatarListNode.listContainerNode.controlsContainerNode, alpha: self.isAvatarExpanded ? (1.0 - transitionFraction) : 0.0) + + if additive { + transition.updateSublayerTransformScaleAdditive(node: self.avatarListNode.listContainerTransformNode, scale: avatarListContainerScale) + } else { + transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerTransformNode, scale: avatarListContainerScale) + } + + self.avatarListNode.listContainerNode.update(size: expandedAvatarListSize, peer: peer, transition: transition) + + let buttonsCollapseStart = titleCollapseOffset + let buttonsCollapseEnd = 212.0 - (navigationHeight - statusBarHeight) + 10.0 + + let buttonsCollapseFraction = max(0.0, contentOffset - buttonsCollapseStart) / (buttonsCollapseEnd - buttonsCollapseStart) + + let rawHeight: CGFloat + let height: CGFloat + if self.isAvatarExpanded { + rawHeight = expandedAvatarHeight + height = max(navigationHeight, rawHeight - contentOffset) + } else { + rawHeight = navigationHeight + 212.0 + height = navigationHeight + max(0.0, 212.0 - contentOffset) + } + + let apparentHeight = (1.0 - transitionFraction) * height + transitionFraction * transitionSourceHeight + + if !titleSize.width.isZero && !titleSize.height.isZero { + if self.navigationTransition != nil { + var neutralTitleScale: CGFloat = 1.0 + var neutralSubtitleScale: CGFloat = 1.0 + if self.isAvatarExpanded { + neutralTitleScale = 0.7 + neutralSubtitleScale = 1.0 + } + + let titleScale = (transitionFraction * transitionSourceTitleFrame.height + (1.0 - transitionFraction) * titleFrame.height * neutralTitleScale) / (titleFrame.height) + let subtitleScale = max(0.01, min(10.0, (transitionFraction * transitionSourceSubtitleFrame.height + (1.0 - transitionFraction) * subtitleFrame.height * neutralSubtitleScale) / (subtitleFrame.height))) + + let titleCenter = CGPoint(x: transitionFraction * transitionSourceTitleFrame.midX + (1.0 - transitionFraction) * titleFrame.midX, y: transitionFraction * transitionSourceTitleFrame.midY + (1.0 - transitionFraction) * titleFrame.midY) + let subtitleCenter = CGPoint(x: transitionFraction * transitionSourceSubtitleFrame.midX + (1.0 - transitionFraction) * subtitleFrame.midX, y: transitionFraction * transitionSourceSubtitleFrame.midY + (1.0 - transitionFraction) * subtitleFrame.midY) + + let rawTitleFrame = CGRect(origin: CGPoint(x: titleCenter.x - titleFrame.size.width * neutralTitleScale / 2.0, y: titleCenter.y - titleFrame.size.height * neutralTitleScale / 2.0), size: CGSize(width: titleFrame.size.width * neutralTitleScale, height: titleFrame.size.height * neutralTitleScale)) + self.titleNodeRawContainer.frame = rawTitleFrame + transition.updateFrameAdditiveToCenter(node: self.titleNodeContainer, frame: CGRect(origin: rawTitleFrame.center, size: CGSize())) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(), size: CGSize())) + let rawSubtitleFrame = CGRect(origin: CGPoint(x: subtitleCenter.x - subtitleFrame.size.width / 2.0, y: subtitleCenter.y - subtitleFrame.size.height / 2.0), size: subtitleFrame.size) + self.subtitleNodeRawContainer.frame = rawSubtitleFrame + transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: CGRect(origin: rawSubtitleFrame.center, size: CGSize())) + transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(), size: CGSize())) + transition.updateSublayerTransformScale(node: self.titleNodeContainer, scale: titleScale) + transition.updateSublayerTransformScale(node: self.subtitleNodeContainer, scale: subtitleScale) + } else { + let titleScale: CGFloat + let subtitleScale: CGFloat + if self.isAvatarExpanded { + titleScale = 0.7 + subtitleScale = 1.0 + } else { + titleScale = (1.0 - titleCollapseFraction) * 1.0 + titleCollapseFraction * titleMinScale + subtitleScale = (1.0 - titleCollapseFraction) * 1.0 + titleCollapseFraction * subtitleMinScale + } + + let rawTitleFrame = titleFrame + self.titleNodeRawContainer.frame = rawTitleFrame + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(), size: CGSize())) + let rawSubtitleFrame = subtitleFrame + self.subtitleNodeRawContainer.frame = rawSubtitleFrame + if self.isAvatarExpanded { + transition.updateFrameAdditive(node: self.titleNodeContainer, frame: CGRect(origin: rawTitleFrame.center, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset + apparentTitleLockOffset)) + transition.updateFrameAdditive(node: self.subtitleNodeContainer, frame: CGRect(origin: rawSubtitleFrame.center, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset)) + } else { + transition.updateFrameAdditiveToCenter(node: self.titleNodeContainer, frame: CGRect(origin: rawTitleFrame.center, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset + apparentTitleLockOffset)) + transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: CGRect(origin: rawSubtitleFrame.center, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset)) + } + transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(), size: CGSize())) + transition.updateSublayerTransformScaleAdditive(node: self.titleNodeContainer, scale: titleScale) + transition.updateSublayerTransformScaleAdditive(node: self.subtitleNodeContainer, scale: subtitleScale) + } + } + + let buttonSpacing: CGFloat + if self.isAvatarExpanded { + buttonSpacing = 16.0 + } else { + let normWidth = min(width, containerHeight) + let buttonSpacingValue = floor((normWidth - floor(CGFloat(buttonKeys.count) * defaultButtonSize)) / CGFloat(buttonKeys.count + 1)) + buttonSpacing = min(buttonSpacingValue, 160.0) + } + + let expandedButtonSize: CGFloat = 32.0 + let buttonsWidth = buttonSpacing * CGFloat(buttonKeys.count - 1) + CGFloat(buttonKeys.count) * defaultButtonSize + var buttonRightOrigin: CGPoint + if self.isAvatarExpanded { + buttonRightOrigin = CGPoint(x: width - 16.0, y: apparentHeight - 74.0) + } else { + buttonRightOrigin = CGPoint(x: floor((width - buttonsWidth) / 2.0) + buttonsWidth, y: apparentHeight - 74.0) + } + let buttonsScale: CGFloat + let buttonsAlpha: CGFloat + let apparentButtonSize: CGFloat + let buttonsVerticalOffset: CGFloat + + var buttonsAlphaTransition = transition + + if self.navigationTransition != nil { + if case let .animated(duration, curve) = transition, transitionFraction >= 1.0 - CGFloat.ulpOfOne { + buttonsAlphaTransition = .animated(duration: duration * 0.6, curve: curve) + } + if self.isAvatarExpanded { + apparentButtonSize = expandedButtonSize + } else { + apparentButtonSize = defaultButtonSize + } + let neutralButtonsScale = apparentButtonSize / defaultButtonSize + buttonsScale = (1.0 - transitionFraction) * neutralButtonsScale + 0.2 * transitionFraction + buttonsAlpha = 1.0 - transitionFraction + + let neutralButtonsOffset: CGFloat + if self.isAvatarExpanded { + neutralButtonsOffset = 74.0 - 15.0 - defaultButtonSize + (defaultButtonSize - apparentButtonSize) / 2.0 + } else { + neutralButtonsOffset = (1.0 - buttonsScale) * apparentButtonSize + } + + buttonsVerticalOffset = (1.0 - transitionFraction) * neutralButtonsOffset + ((1.0 - buttonsScale) * apparentButtonSize) * transitionFraction + } else { + apparentButtonSize = self.isAvatarExpanded ? expandedButtonSize : defaultButtonSize + if self.isAvatarExpanded { + buttonsScale = apparentButtonSize / defaultButtonSize + buttonsVerticalOffset = 74.0 - 15.0 - defaultButtonSize + (defaultButtonSize - apparentButtonSize) / 2.0 + } else { + buttonsScale = (1.0 - buttonsCollapseFraction) * 1.0 + 0.2 * buttonsCollapseFraction + buttonsVerticalOffset = (1.0 - buttonsScale) * apparentButtonSize + } + buttonsAlpha = 1.0 - buttonsCollapseFraction + } + let buttonsScaledOffset = (defaultButtonSize - apparentButtonSize) / 2.0 + for buttonKey in buttonKeys.reversed() { + let buttonNode: PeerInfoHeaderButtonNode + var wasAdded = false + if let current = self.buttonNodes[buttonKey] { + buttonNode = current + } else { + wasAdded = true + buttonNode = PeerInfoHeaderButtonNode(key: buttonKey, action: { [weak self] buttonNode in + self?.buttonPressed(buttonNode) + }) + self.buttonNodes[buttonKey] = buttonNode + self.regularContentNode.addSubnode(buttonNode) + } + + let buttonFrame = CGRect(origin: CGPoint(x: buttonRightOrigin.x - defaultButtonSize + buttonsScaledOffset, y: buttonRightOrigin.y), size: CGSize(width: defaultButtonSize, height: defaultButtonSize)) + let buttonTransition: ContainedViewLayoutTransition = wasAdded ? .immediate : transition + + let apparentButtonFrame = buttonFrame.offsetBy(dx: 0.0, dy: buttonsVerticalOffset) + if additive { + buttonTransition.updateFrameAdditiveToCenter(node: buttonNode, frame: apparentButtonFrame) + } else { + buttonTransition.updateFrame(node: buttonNode, frame: apparentButtonFrame) + } + let buttonText: String + let buttonIcon: PeerInfoHeaderButtonIcon + switch buttonKey { + case .message: + buttonText = presentationData.strings.PeerInfo_ButtonMessage + buttonIcon = .message + case .discussion: + buttonText = presentationData.strings.PeerInfo_ButtonDiscuss + buttonIcon = .message + case .call: + buttonText = presentationData.strings.PeerInfo_ButtonCall + buttonIcon = .call + case .mute: + if let notificationSettings = notificationSettings, case .muted = notificationSettings.muteState { + buttonText = presentationData.strings.PeerInfo_ButtonUnmute + buttonIcon = .unmute + } else { + buttonText = presentationData.strings.PeerInfo_ButtonMute + buttonIcon = .mute + } + case .more: + buttonText = presentationData.strings.PeerInfo_ButtonMore + buttonIcon = .more + case .addMember: + buttonText = presentationData.strings.PeerInfo_ButtonAddMember + buttonIcon = .addMember + case .search: + buttonText = presentationData.strings.PeerInfo_ButtonSearch + buttonIcon = .search + case .leave: + buttonText = presentationData.strings.PeerInfo_ButtonLeave + buttonIcon = .leave + } + buttonNode.update(size: buttonFrame.size, text: buttonText, icon: buttonIcon, isExpanded: self.isAvatarExpanded, presentationData: presentationData, transition: buttonTransition) + transition.updateSublayerTransformScaleAdditive(node: buttonNode, scale: buttonsScale) + + if wasAdded { + buttonNode.alpha = 0.0 + } + buttonsAlphaTransition.updateAlpha(node: buttonNode, alpha: buttonsAlpha) + + let hiddenWhileExpanded: Bool + if buttonKeys.count > 3 { + if self.isOpenedFromChat { + switch buttonKey { + case .message, .search: + hiddenWhileExpanded = true + default: + hiddenWhileExpanded = false + } + } else { + switch buttonKey { + case .mute, .search: + hiddenWhileExpanded = true + default: + hiddenWhileExpanded = false + } + } + } else { + hiddenWhileExpanded = false + } + + if self.isAvatarExpanded, hiddenWhileExpanded { + if case let .animated(duration, curve) = transition { + ContainedViewLayoutTransition.animated(duration: duration * 0.3, curve: curve).updateAlpha(node: buttonNode.containerNode, alpha: 0.0) + } else { + transition.updateAlpha(node: buttonNode.containerNode, alpha: 0.0) + } + } else { + if case .mute = buttonKey, buttonNode.containerNode.alpha.isZero, additive { + if case let .animated(duration, curve) = transition { + ContainedViewLayoutTransition.animated(duration: duration * 0.3, curve: curve).updateAlpha(node: buttonNode.containerNode, alpha: 1.0) + } else { + transition.updateAlpha(node: buttonNode.containerNode, alpha: 1.0) + } + } else { + transition.updateAlpha(node: buttonNode.containerNode, alpha: 1.0) + } + buttonRightOrigin.x -= apparentButtonSize + buttonSpacing + } + } + + for key in self.buttonNodes.keys { + if !buttonKeys.contains(key) { + if let buttonNode = self.buttonNodes[key] { + self.buttonNodes.removeValue(forKey: key) + buttonNode.removeFromSupernode() + } + } + } + + let resolvedRegularHeight: CGFloat + if self.isAvatarExpanded { + resolvedRegularHeight = expandedAvatarListSize.height + expandedAvatarControlsHeight + } else { + resolvedRegularHeight = 212.0 + navigationHeight + } + + let backgroundFrame: CGRect + let separatorFrame: CGRect + + let resolvedHeight: CGFloat + if state.isEditing { + resolvedHeight = editingContentHeight + backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -2000.0 + resolvedHeight - contentOffset), size: CGSize(width: width, height: 2000.0)) + separatorFrame = CGRect(origin: CGPoint(x: 0.0, y: resolvedHeight - contentOffset), size: CGSize(width: width, height: UIScreenPixel)) + } else { + resolvedHeight = resolvedRegularHeight + backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -2000.0 + apparentHeight), size: CGSize(width: width, height: 2000.0)) + separatorFrame = CGRect(origin: CGPoint(x: 0.0, y: apparentHeight), size: CGSize(width: width, height: UIScreenPixel)) + } + + transition.updateFrame(node: self.regularContentNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: resolvedHeight))) + + if additive { + transition.updateFrameAdditive(node: self.backgroundNode, frame: backgroundFrame) + transition.updateFrameAdditive(node: self.expandedBackgroundNode, frame: backgroundFrame) + transition.updateFrameAdditive(node: self.separatorNode, frame: separatorFrame) + } else { + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + transition.updateFrame(node: self.expandedBackgroundNode, frame: backgroundFrame) + transition.updateFrame(node: self.separatorNode, frame: separatorFrame) + } + + return resolvedHeight + } + + private func buttonPressed(_ buttonNode: PeerInfoHeaderButtonNode) { + self.performButtonAction?(buttonNode.key) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + if result.isDescendant(of: self.navigationButtonContainer.view) { + return result + } + if !self.backgroundNode.frame.contains(point) { + return nil + } + if result == self.view || result == self.regularContentNode.view || result == self.editingContentNode.view { + return nil + } + return result + } + + func updateIsAvatarExpanded(_ isAvatarExpanded: Bool, transition: ContainedViewLayoutTransition) { + if self.isAvatarExpanded != isAvatarExpanded { + self.isAvatarExpanded = isAvatarExpanded + if isAvatarExpanded { + self.avatarListNode.listContainerNode.selectFirstItem() + } + if case .animated = transition, !isAvatarExpanded { + self.avatarListNode.animateAvatarCollapse(transition: transition) + } + } + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoMembers.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoMembers.swift new file mode 100644 index 0000000000..cbfd8230b7 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoMembers.swift @@ -0,0 +1,289 @@ +import Foundation +import SwiftSignalKit +import Postbox +import SyncCore +import TelegramCore +import AccountContext +import TemporaryCachedPeerDataManager + +enum PeerInfoMemberRole { + case creator + case admin + case member +} + +enum PeerInfoMember: Equatable { + case channelMember(RenderedChannelParticipant) + case legacyGroupMember(peer: RenderedPeer, role: PeerInfoMemberRole, invitedBy: PeerId?, presence: TelegramUserPresence?) + + var id: PeerId { + switch self { + case let .channelMember(channelMember): + return channelMember.peer.id + case let .legacyGroupMember(legacyGroupMember): + return legacyGroupMember.peer.peerId + } + } + + var peer: Peer { + switch self { + case let .channelMember(channelMember): + return channelMember.peer + case let .legacyGroupMember(legacyGroupMember): + return legacyGroupMember.peer.peers[legacyGroupMember.peer.peerId]! + } + } + + var presence: TelegramUserPresence? { + switch self { + case let .channelMember(channelMember): + return channelMember.presences[channelMember.peer.id] as? TelegramUserPresence + case let .legacyGroupMember(legacyGroupMember): + return legacyGroupMember.presence + } + } + + var role: PeerInfoMemberRole { + switch self { + case let .channelMember(channelMember): + switch channelMember.participant { + case .creator: + return .creator + case let .member(member): + if member.adminInfo != nil { + return .admin + } else { + return .member + } + } + case let .legacyGroupMember(legacyGroupMember): + return legacyGroupMember.role + } + } + + var rank: String? { + switch self { + case let .channelMember(channelMember): + switch channelMember.participant { + case let .creator(creator): + return creator.rank + case let .member(member): + return member.rank + } + case .legacyGroupMember: + return nil + } + } +} + +enum PeerInfoMembersDataState: Equatable { + case loading(isInitial: Bool) + case ready(canLoadMore: Bool) +} + +struct PeerInfoMembersState: Equatable { + var members: [PeerInfoMember] + var dataState: PeerInfoMembersDataState +} + +private func membersSortedByPresence(_ members: [PeerInfoMember], accountPeerId: PeerId) -> [PeerInfoMember] { + return members.sorted(by: { lhs, rhs in + if lhs.id == accountPeerId { + return true + } else if rhs.id == accountPeerId { + return false + } + + let lhsPresence = lhs.presence + let rhsPresence = rhs.presence + if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { + if lhsPresence.status < rhsPresence.status { + return false + } else if lhsPresence.status > rhsPresence.status { + return true + } + } else if let _ = lhsPresence { + return true + } else if let _ = rhsPresence { + return false + } + return lhs.id < rhs.id + }) +} + +private final class PeerInfoMembersContextImpl { + private let queue: Queue + private let context: AccountContext + private let peerId: PeerId + + private var members: [PeerInfoMember] = [] + private var dataState: PeerInfoMembersDataState = .loading(isInitial: true) + private var removingMemberIds: [PeerId: Disposable] = [:] + + private let stateValue = Promise() + var state: Signal { + return self.stateValue.get() + } + private let disposable = MetaDisposable() + + private var channelMembersControl: PeerChannelMemberCategoryControl? + + init(queue: Queue, context: AccountContext, peerId: PeerId) { + self.queue = queue + self.context = context + self.peerId = peerId + + self.pushState() + + if peerId.namespace == Namespaces.Peer.CloudChannel { + let (disposable, control) = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { [weak self] state in + queue.async { + guard let strongSelf = self else { + return + } + let unsortedMembers = state.list.map(PeerInfoMember.channelMember) + let members: [PeerInfoMember] + if unsortedMembers.count <= 50 { + members = membersSortedByPresence(unsortedMembers, accountPeerId: strongSelf.context.account.peerId) + } else { + members = unsortedMembers + } + strongSelf.members = members + switch state.loadingState { + case let .loading(initial): + strongSelf.dataState = .loading(isInitial: initial) + case let .ready(hasMore): + strongSelf.dataState = .ready(canLoadMore: hasMore) + } + strongSelf.pushState() + } + }) + self.disposable.set(disposable) + self.channelMembersControl = control + } else if peerId.namespace == Namespaces.Peer.CloudGroup { + disposable.set((context.account.postbox.peerView(id: peerId) + |> deliverOn(self.queue)).start(next: { [weak self] view in + guard let strongSelf = self, let cachedData = view.cachedData as? CachedGroupData, let participantsData = cachedData.participants else { + return + } + var unsortedMembers: [PeerInfoMember] = [] + for participant in participantsData.participants { + if let peer = view.peers[participant.peerId] { + let role: PeerInfoMemberRole + let invitedBy: PeerId? + switch participant { + case .creator: + role = .creator + invitedBy = nil + case let .admin(admin): + role = .admin + invitedBy = admin.invitedBy + case let .member(member): + role = .member + invitedBy = member.invitedBy + } + unsortedMembers.append(.legacyGroupMember(peer: RenderedPeer(peer: peer), role: role, invitedBy: invitedBy, presence: view.peerPresences[participant.peerId] as? TelegramUserPresence)) + } + } + strongSelf.members = membersSortedByPresence(unsortedMembers, accountPeerId: strongSelf.context.account.peerId) + strongSelf.dataState = .ready(canLoadMore: false) + strongSelf.pushState() + })) + } else { + self.dataState = .ready(canLoadMore: false) + self.pushState() + } + } + + deinit { + self.disposable.dispose() + } + + private func pushState() { + if self.removingMemberIds.isEmpty { + self.stateValue.set(.single(PeerInfoMembersState(members: self.members, dataState: self.dataState))) + } else { + self.stateValue.set(.single(PeerInfoMembersState(members: self.members.filter { member in + return self.removingMemberIds[member.id] == nil + }, dataState: self.dataState))) + } + } + + func loadMore() { + if case .ready(true) = self.dataState, let channelMembersControl = self.channelMembersControl { + self.context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: self.peerId, control: channelMembersControl) + } + } + + func removeMember(memberId: PeerId) { + if removingMemberIds[memberId] == nil { + let signal: Signal + if self.peerId.namespace == Namespaces.Peer.CloudChannel { + signal = context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: self.context.account, peerId: self.peerId, memberId: memberId, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)) + |> ignoreValues + } else { + signal = removePeerMember(account: self.context.account, peerId: self.peerId, memberId: memberId) + |> ignoreValues + } + let completed: () -> Void = { [weak self] in + guard let strongSelf = self else { + return + } + if let _ = strongSelf.removingMemberIds.removeValue(forKey: memberId) { + strongSelf.pushState() + } + } + let disposable = MetaDisposable() + self.removingMemberIds[memberId] = disposable + + self.pushState() + + disposable.set((signal + |> deliverOn(self.queue)).start(error: { _ in + completed() + }, completed: { + completed() + })) + } + } +} + +final class PeerInfoMembersContext: Equatable { + private let queue = Queue.mainQueue() + private let impl: QueueLocalObject + + var state: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.state.start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } + + init(context: AccountContext, peerId: PeerId) { + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return PeerInfoMembersContextImpl(queue: queue, context: context, peerId: peerId) + }) + } + + func loadMore() { + self.impl.with { impl in + impl.loadMore() + } + } + + func removeMember(memberId: PeerId) { + self.impl.with { impl in + impl.removeMember(memberId: memberId) + } + } + + static func ==(lhs: PeerInfoMembersContext, rhs: PeerInfoMembersContext) -> Bool { + return lhs === rhs + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoPaneContainerNode.swift new file mode 100644 index 0000000000..070320143f --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoPaneContainerNode.swift @@ -0,0 +1,868 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramPresentationData +import Postbox +import SyncCore +import TelegramCore +import AccountContext +import ContextUI + +protocol PeerInfoPaneNode: ASDisplayNode { + var isReady: Signal { get } + + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) + func scrollToTop() -> Bool + func transferVelocity(_ velocity: CGFloat) + func cancelPreviewGestures() + func findLoadedMessage(id: MessageId) -> Message? + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? + func addToTransitionSurface(view: UIView) + func updateHiddenMedia() + func updateSelectedMessages(animated: Bool) +} + +final class PeerInfoPaneWrapper { + let key: PeerInfoPaneKey + let node: PeerInfoPaneNode + var isAnimatingOut: Bool = false + private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, Bool, PresentationData)? + + init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) { + self.key = key + self.node = node + } + + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + if let (currentSize, currentSideInset, currentBottomInset, visibleHeight, currentIsScrollingLockedAtTop, currentPresentationData) = self.appliedParams { + if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentPresentationData === presentationData { + return + } + } + self.appliedParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData) + self.node.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: synchronous, transition: transition) + } +} + +enum PeerInfoPaneKey { + case media + case files + case links + case voice + case music + case groupsInCommon + case members +} + +final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { + private let pressed: () -> Void + + private let titleNode: ImmediateTextNode + private let buttonNode: HighlightTrackingButtonNode + + private var isSelected: Bool = false + + init(pressed: @escaping () -> Void) { + self.pressed = pressed + + self.titleNode = ImmediateTextNode() + self.titleNode.displaysAsynchronously = false + + self.buttonNode = HighlightTrackingButtonNode() + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + /*self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted && !strongSelf.isSelected { + strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") + strongSelf.titleNode.alpha = 0.4 + } else { + strongSelf.titleNode.alpha = 1.0 + strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + }*/ + } + + @objc private func buttonPressed() { + self.pressed() + } + + func updateText(_ title: String, isSelected: Bool, presentationData: PresentationData) { + self.isSelected = isSelected + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: isSelected ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor) + } + + func updateLayout(height: CGFloat) -> CGFloat { + let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize) + return titleSize.width + } + + func updateArea(size: CGSize, sideInset: CGFloat) { + self.buttonNode.frame = CGRect(origin: CGPoint(x: -sideInset, y: 0.0), size: CGSize(width: size.width + sideInset * 2.0, height: size.height)) + } +} + +struct PeerInfoPaneSpecifier: Equatable { + var key: PeerInfoPaneKey + var title: String +} + +private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect { + return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t))) +} + +final class PeerInfoPaneTabsContainerNode: ASDisplayNode { + private let scrollNode: ASScrollNode + private var paneNodes: [PeerInfoPaneKey: PeerInfoPaneTabsContainerPaneNode] = [:] + private let selectedLineNode: ASImageNode + + private var currentParams: ([PeerInfoPaneSpecifier], PeerInfoPaneKey?, PresentationData)? + + var requestSelectPane: ((PeerInfoPaneKey) -> Void)? + + override init() { + self.scrollNode = ASScrollNode() + + self.selectedLineNode = ASImageNode() + self.selectedLineNode.displaysAsynchronously = false + self.selectedLineNode.displayWithoutProcessing = true + + super.init() + + self.scrollNode.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in + guard let strongSelf = self else { + return false + } + return strongSelf.scrollNode.view.contentOffset.x > .ulpOfOne + } + self.scrollNode.view.showsHorizontalScrollIndicator = false + self.scrollNode.view.scrollsToTop = false + if #available(iOS 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + + self.addSubnode(self.scrollNode) + self.scrollNode.addSubnode(self.selectedLineNode) + } + + func update(size: CGSize, presentationData: PresentationData, paneList: [PeerInfoPaneSpecifier], selectedPane: PeerInfoPaneKey?, transitionFraction: CGFloat, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size)) + + let focusOnSelectedPane = self.currentParams?.1 != selectedPane + + if self.currentParams?.2.theme !== presentationData.theme { + self.selectedLineNode.image = generateImage(CGSize(width: 7.0, height: 4.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(presentationData.theme.list.itemAccentColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width))) + })?.stretchableImage(withLeftCapWidth: 4, topCapHeight: 1) + } + + if self.currentParams?.0 != paneList || self.currentParams?.1 != selectedPane || self.currentParams?.2 !== presentationData { + self.currentParams = (paneList, selectedPane, presentationData) + for specifier in paneList { + let paneNode: PeerInfoPaneTabsContainerPaneNode + var wasAdded = false + if let current = self.paneNodes[specifier.key] { + paneNode = current + } else { + wasAdded = true + paneNode = PeerInfoPaneTabsContainerPaneNode(pressed: { [weak self] in + self?.paneSelected(specifier.key) + }) + self.paneNodes[specifier.key] = paneNode + } + paneNode.updateText(specifier.title, isSelected: selectedPane == specifier.key, presentationData: presentationData) + } + var removeKeys: [PeerInfoPaneKey] = [] + for (key, _) in self.paneNodes { + if !paneList.contains(where: { $0.key == key }) { + removeKeys.append(key) + } + } + for key in removeKeys { + if let paneNode = self.paneNodes.removeValue(forKey: key) { + paneNode.removeFromSupernode() + } + } + } + + var tabSizes: [(CGSize, PeerInfoPaneTabsContainerPaneNode, Bool)] = [] + var totalRawTabSize: CGFloat = 0.0 + var selectionFrames: [CGRect] = [] + + for specifier in paneList { + guard let paneNode = self.paneNodes[specifier.key] else { + continue + } + let wasAdded = paneNode.supernode == nil + if wasAdded { + self.scrollNode.addSubnode(paneNode) + } + let paneNodeWidth = paneNode.updateLayout(height: size.height) + let paneNodeSize = CGSize(width: paneNodeWidth, height: size.height) + tabSizes.append((paneNodeSize, paneNode, wasAdded)) + totalRawTabSize += paneNodeSize.width + } + + let minSpacing: CGFloat = 10.0 + if tabSizes.count <= 1 { + for i in 0 ..< tabSizes.count { + let (paneNodeSize, paneNode, wasAdded) = tabSizes[i] + let leftOffset: CGFloat = 16.0 + + let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) + if wasAdded { + paneNode.frame = paneFrame + paneNode.alpha = 0.0 + transition.updateAlpha(node: paneNode, alpha: 1.0) + } else { + transition.updateFrameAdditiveToCenter(node: paneNode, frame: paneFrame) + } + let areaSideInset: CGFloat = 16.0 + paneNode.updateArea(size: paneFrame.size, sideInset: areaSideInset) + paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -areaSideInset, bottom: 0.0, right: -areaSideInset) + + selectionFrames.append(paneFrame) + } + self.scrollNode.view.contentSize = CGSize(width: size.width, height: size.height) + } else if totalRawTabSize + CGFloat(tabSizes.count + 1) * minSpacing <= size.width { + let availableSpace = size.width + let availableSpacing = availableSpace - totalRawTabSize + let perTabSpacing = floor(availableSpacing / CGFloat(tabSizes.count + 1)) + + let normalizedPerTabWidth = floor(availableSpace / CGFloat(tabSizes.count)) + var maxSpacing: CGFloat = 0.0 + var minSpacing: CGFloat = .greatestFiniteMagnitude + for i in 0 ..< tabSizes.count - 1 { + let distanceToNextBoundary = (normalizedPerTabWidth - tabSizes[i].0.width) / 2.0 + let nextDistanceToBoundary = (normalizedPerTabWidth - tabSizes[i + 1].0.width) / 2.0 + let distance = nextDistanceToBoundary + distanceToNextBoundary + maxSpacing = max(distance, maxSpacing) + minSpacing = min(distance, minSpacing) + } + + if minSpacing >= 100.0 || (maxSpacing / minSpacing) < 0.2 { + for i in 0 ..< tabSizes.count { + let (paneNodeSize, paneNode, wasAdded) = tabSizes[i] + + let paneFrame = CGRect(origin: CGPoint(x: CGFloat(i) * normalizedPerTabWidth + floor((normalizedPerTabWidth - paneNodeSize.width) / 2.0), y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) + if wasAdded { + paneNode.frame = paneFrame + paneNode.alpha = 0.0 + transition.updateAlpha(node: paneNode, alpha: 1.0) + } else { + transition.updateFrameAdditiveToCenter(node: paneNode, frame: paneFrame) + } + let areaSideInset = floor((normalizedPerTabWidth - paneNodeSize.width) / 2.0) + paneNode.updateArea(size: paneFrame.size, sideInset: areaSideInset) + paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -areaSideInset, bottom: 0.0, right: -areaSideInset) + + selectionFrames.append(paneFrame) + } + } else { + var leftOffset = perTabSpacing + for i in 0 ..< tabSizes.count { + let (paneNodeSize, paneNode, wasAdded) = tabSizes[i] + + let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) + if wasAdded { + paneNode.frame = paneFrame + paneNode.alpha = 0.0 + transition.updateAlpha(node: paneNode, alpha: 1.0) + } else { + transition.updateFrameAdditiveToCenter(node: paneNode, frame: paneFrame) + } + let areaSideInset = floor(perTabSpacing / 2.0) + paneNode.updateArea(size: paneFrame.size, sideInset: areaSideInset) + paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -areaSideInset, bottom: 0.0, right: -areaSideInset) + + leftOffset += paneNodeSize.width + perTabSpacing + + selectionFrames.append(paneFrame) + } + } + self.scrollNode.view.contentSize = CGSize(width: size.width, height: size.height) + } else { + let sideInset: CGFloat = 16.0 + var leftOffset: CGFloat = sideInset + for i in 0 ..< tabSizes.count { + let (paneNodeSize, paneNode, wasAdded) = tabSizes[i] + let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) + if wasAdded { + paneNode.frame = paneFrame + paneNode.alpha = 0.0 + transition.updateAlpha(node: paneNode, alpha: 1.0) + } else { + transition.updateFrameAdditiveToCenter(node: paneNode, frame: paneFrame) + } + paneNode.updateArea(size: paneFrame.size, sideInset: minSpacing) + paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -minSpacing, bottom: 0.0, right: -minSpacing) + + selectionFrames.append(paneFrame) + + leftOffset += paneNodeSize.width + minSpacing + } + self.scrollNode.view.contentSize = CGSize(width: leftOffset - minSpacing + sideInset, height: size.height) + } + + var selectedFrame: CGRect? + if let selectedPane = selectedPane, let currentIndex = paneList.index(where: { $0.key == selectedPane }) { + if currentIndex != 0 && transitionFraction > 0.0 { + let currentFrame = selectionFrames[currentIndex] + let previousFrame = selectionFrames[currentIndex - 1] + selectedFrame = interpolateFrame(from: currentFrame, to: previousFrame, t: abs(transitionFraction)) + } else if currentIndex != paneList.count - 1 && transitionFraction < 0.0 { + let currentFrame = selectionFrames[currentIndex] + let previousFrame = selectionFrames[currentIndex + 1] + selectedFrame = interpolateFrame(from: currentFrame, to: previousFrame, t: abs(transitionFraction)) + } else { + selectedFrame = selectionFrames[currentIndex] + } + } + + if let selectedFrame = selectedFrame { + let wasAdded = self.selectedLineNode.isHidden + self.selectedLineNode.isHidden = false + let lineFrame = CGRect(origin: CGPoint(x: selectedFrame.minX, y: size.height - 4.0), size: CGSize(width: selectedFrame.width, height: 4.0)) + if wasAdded { + self.selectedLineNode.frame = lineFrame + self.selectedLineNode.alpha = 0.0 + transition.updateAlpha(node: self.selectedLineNode, alpha: 1.0) + } else { + transition.updateFrame(node: self.selectedLineNode, frame: lineFrame) + } + if focusOnSelectedPane { + if selectedPane == paneList.first?.key { + transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)) + } else if selectedPane == paneList.last?.key { + transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: max(0.0, self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width), y: 0.0), size: self.scrollNode.bounds.size)) + } else { + let contentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, floor(selectedFrame.midX - self.scrollNode.bounds.width / 2.0))) + transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size)) + } + } + } else { + self.selectedLineNode.isHidden = true + } + } + + private func paneSelected(_ key: PeerInfoPaneKey) { + self.requestSelectPane?(key) + } +} + +private final class PeerInfoPendingPane { + let pane: PeerInfoPaneWrapper + private var disposable: Disposable? + var isReady: Bool = false + + init( + context: AccountContext, + chatControllerInteraction: ChatControllerInteraction, + data: PeerInfoScreenData, + openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, + requestPerformPeerMemberAction: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, + peerId: PeerId, + key: PeerInfoPaneKey, + hasBecomeReady: @escaping (PeerInfoPaneKey) -> Void + ) { + let paneNode: PeerInfoPaneNode + switch key { + case .media: + paneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId) + case .files: + paneNode = PeerInfoListPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .file) + case .links: + paneNode = PeerInfoListPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .webPage) + case .voice: + paneNode = PeerInfoListPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .voiceOrInstantVideo) + case .music: + paneNode = PeerInfoListPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .music) + case .groupsInCommon: + paneNode = PeerInfoGroupsInCommonPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction, groupsInCommonContext: data.groupsInCommon!) + case .members: + if case let .longList(membersContext) = data.members { + paneNode = PeerInfoMembersPaneNode(context: context, peerId: peerId, membersContext: membersContext, action: { member, action in + requestPerformPeerMemberAction(member, action) + }) + } else { + preconditionFailure() + } + } + + self.pane = PeerInfoPaneWrapper(key: key, node: paneNode) + self.disposable = (paneNode.isReady + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + self?.isReady = true + hasBecomeReady(key) + }) + } + + deinit { + self.disposable?.dispose() + } +} + +final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { + private let context: AccountContext + private let peerId: PeerId + + private let coveringBackgroundNode: ASDisplayNode + private let separatorNode: ASDisplayNode + private let tabsContainerNode: PeerInfoPaneTabsContainerNode + private let tapsSeparatorNode: ASDisplayNode + + let isReady = Promise() + var didSetIsReady = false + + private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?)? + + private(set) var currentPaneKey: PeerInfoPaneKey? + var pendingSwitchToPaneKey: PeerInfoPaneKey? + + var currentPane: PeerInfoPaneWrapper? { + if let currentPaneKey = self.currentPaneKey { + return self.currentPanes[currentPaneKey] + } else { + return nil + } + } + + private var currentPanes: [PeerInfoPaneKey: PeerInfoPaneWrapper] = [:] + private var pendingPanes: [PeerInfoPaneKey: PeerInfoPendingPane] = [:] + + private var transitionFraction: CGFloat = 0.0 + + var selectionPanelNode: PeerInfoSelectionPanelNode? + + var chatControllerInteraction: ChatControllerInteraction? + var openPeerContextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)? + var requestPerformPeerMemberAction: ((PeerInfoMember, PeerMembersListAction) -> Void)? + + var currentPaneUpdated: ((Bool) -> Void)? + var requestExpandTabs: (() -> Bool)? + + private var currentAvailablePanes: [PeerInfoPaneKey]? + + init(context: AccountContext, peerId: PeerId) { + self.context = context + self.peerId = peerId + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + self.coveringBackgroundNode = ASDisplayNode() + self.coveringBackgroundNode.isLayerBacked = true + + self.tabsContainerNode = PeerInfoPaneTabsContainerNode() + + self.tapsSeparatorNode = ASDisplayNode() + self.tapsSeparatorNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.separatorNode) + self.addSubnode(self.coveringBackgroundNode) + self.addSubnode(self.tabsContainerNode) + self.addSubnode(self.tapsSeparatorNode) + + self.tabsContainerNode.requestSelectPane = { [weak self] key in + guard let strongSelf = self else { + return + } + if strongSelf.currentPaneKey == key { + if let requestExpandTabs = strongSelf.requestExpandTabs, requestExpandTabs() { + } else { + strongSelf.currentPane?.node.scrollToTop() + } + return + } + if strongSelf.currentPanes[key] != nil { + strongSelf.currentPaneKey = key + + if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.4, curve: .spring)) + + strongSelf.currentPaneUpdated?(true) + } + } else if strongSelf.pendingSwitchToPaneKey != key { + strongSelf.pendingSwitchToPaneKey = key + + if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.4, curve: .spring)) + } + } + } + } + + override func didLoad() { + super.didLoad() + + let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] in + guard let strongSelf = self, let currentPaneKey = strongSelf.currentPaneKey, let availablePanes = strongSelf.currentParams?.data?.availablePanes, let index = availablePanes.index(of: currentPaneKey) else { + return [] + } + if index == 0 { + return .left + } + return [.left, .right] + }) + panRecognizer.delegate = self + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = true + self.view.addGestureRecognizer(panRecognizer) + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer { + return false + } + if let _ = otherGestureRecognizer as? UIPanGestureRecognizer { + return true + } + return false + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .changed: + if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = self.currentParams, let availablePanes = data?.availablePanes, availablePanes.count > 1, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.index(of: currentPaneKey) { + let translation = recognizer.translation(in: self.view) + var transitionFraction = translation.x / size.width + if currentIndex <= 0 { + transitionFraction = min(0.0, transitionFraction) + } + if currentIndex >= availablePanes.count - 1 { + transitionFraction = max(0.0, transitionFraction) + } + self.transitionFraction = transitionFraction + self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .immediate) + } + case .cancelled, .ended: + if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = self.currentParams, let availablePanes = data?.availablePanes, availablePanes.count > 1, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.index(of: currentPaneKey) { + let translation = recognizer.translation(in: self.view) + let velocity = recognizer.velocity(in: self.view) + var directionIsToRight: Bool? + if abs(velocity.x) > 10.0 { + directionIsToRight = velocity.x < 0.0 + } else { + if abs(translation.x) > size.width / 2.0 { + directionIsToRight = translation.x > size.width / 2.0 + } + } + var updated = false + if let directionIsToRight = directionIsToRight { + var updatedIndex = currentIndex + if directionIsToRight { + updatedIndex = min(updatedIndex + 1, availablePanes.count - 1) + } else { + updatedIndex = max(updatedIndex - 1, 0) + } + let switchToKey = availablePanes[updatedIndex] + if switchToKey != self.currentPaneKey && self.currentPanes[switchToKey] != nil{ + self.currentPaneKey = switchToKey + updated = true + } + } + self.transitionFraction = 0.0 + self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.35, curve: .spring)) + self.currentPaneUpdated?(false) + } + default: + break + } + } + + func scrollToTop() -> Bool { + if let currentPane = self.currentPane { + return currentPane.node.scrollToTop() + } else { + return false + } + } + + func findLoadedMessage(id: MessageId) -> Message? { + return self.currentPane?.node.findLoadedMessage(id: id) + } + + func updateHiddenMedia() { + self.currentPane?.node.updateHiddenMedia() + } + + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + return self.currentPane?.node.transitionNodeForGallery(messageId: messageId, media: media) + } + + func updateSelectedMessageIds(_ selectedMessageIds: Set?, animated: Bool) { + for (_, pane) in self.currentPanes { + pane.node.updateSelectedMessages(animated: animated) + } + for (_, pane) in self.pendingPanes { + pane.pane.node.updateSelectedMessages(animated: animated) + } + } + + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?, transition: ContainedViewLayoutTransition) { + let previousAvailablePanes = self.currentAvailablePanes ?? [] + let availablePanes = data?.availablePanes ?? [] + self.currentAvailablePanes = availablePanes + + let previousCurrentPaneKey = self.currentPaneKey + + if let currentPaneKey = self.currentPaneKey, !availablePanes.contains(currentPaneKey) { + var nextCandidatePaneKey: PeerInfoPaneKey? + if let index = previousAvailablePanes.index(of: currentPaneKey), index != 0 { + for i in (0 ... index - 1).reversed() { + if availablePanes.contains(previousAvailablePanes[i]) { + nextCandidatePaneKey = previousAvailablePanes[i] + } + } + } + if nextCandidatePaneKey == nil { + nextCandidatePaneKey = availablePanes.first + } + + if let nextCandidatePaneKey = nextCandidatePaneKey { + self.pendingSwitchToPaneKey = nextCandidatePaneKey + } else { + self.currentPaneKey = nil + self.pendingSwitchToPaneKey = nil + } + } else if self.currentPaneKey == nil { + self.pendingSwitchToPaneKey = availablePanes.first + } + + let currentIndex: Int? + if let currentPaneKey = self.currentPaneKey { + currentIndex = availablePanes.index(of: currentPaneKey) + } else { + currentIndex = nil + } + + self.currentParams = (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) + + transition.updateAlpha(node: self.coveringBackgroundNode, alpha: expansionFraction) + + self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor + self.coveringBackgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor + self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + self.tapsSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + let tabsHeight: CGFloat = 48.0 + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.coveringBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: tabsHeight + UIScreenPixel))) + + transition.updateFrame(node: self.tapsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) + + let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: tabsHeight), size: CGSize(width: size.width, height: size.height - tabsHeight)) + + var visiblePaneIndices: [Int] = [] + var requiredPendingKeys: [PeerInfoPaneKey] = [] + if let currentIndex = currentIndex { + if currentIndex != 0 { + visiblePaneIndices.append(currentIndex - 1) + } + visiblePaneIndices.append(currentIndex) + if currentIndex != availablePanes.count - 1 { + visiblePaneIndices.append(currentIndex + 1) + } + + for index in visiblePaneIndices { + let indexOffset = CGFloat(index - currentIndex) + let key = availablePanes[index] + if self.currentPanes[key] == nil && self.pendingPanes[key] == nil { + requiredPendingKeys.append(key) + } + } + } + if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey { + if self.currentPanes[pendingSwitchToPaneKey] == nil && self.pendingPanes[pendingSwitchToPaneKey] == nil { + if !requiredPendingKeys.contains(pendingSwitchToPaneKey) { + requiredPendingKeys.append(pendingSwitchToPaneKey) + } + } + } + + for key in requiredPendingKeys { + if self.pendingPanes[key] == nil { + var leftScope = false + let pane = PeerInfoPendingPane( + context: self.context, + chatControllerInteraction: self.chatControllerInteraction!, + data: data!, + openPeerContextAction: { [weak self] peer, node, gesture in + self?.openPeerContextAction?(peer, node, gesture) + }, + requestPerformPeerMemberAction: { [weak self] member, action in + self?.requestPerformPeerMemberAction?(member, action) + }, + peerId: self.peerId, + key: key, + hasBecomeReady: { [weak self] key in + let apply: () -> Void = { + guard let strongSelf = self else { + return + } + if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { + var transition: ContainedViewLayoutTransition = .immediate + if strongSelf.pendingSwitchToPaneKey == key && strongSelf.currentPaneKey != nil { + transition = .animated(duration: 0.4, curve: .spring) + } + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: transition) + } + } + if leftScope { + apply() + } + } + ) + self.pendingPanes[key] = pane + pane.pane.node.frame = paneFrame + pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: true, transition: .immediate) + leftScope = true + } + } + + for (key, pane) in self.pendingPanes { + pane.pane.node.frame = paneFrame + pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate) + + if pane.isReady { + self.pendingPanes.removeValue(forKey: key) + self.currentPanes[key] = pane.pane + } + } + + var paneDefaultTransition = transition + var previousPaneKey: PeerInfoPaneKey? + var paneSwitchAnimationOffset: CGFloat = 0.0 + + var updatedCurrentIndex = currentIndex + var animatePaneTransitionOffset: CGFloat? + if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey, let pane = self.currentPanes[pendingSwitchToPaneKey] { + self.pendingSwitchToPaneKey = nil + previousPaneKey = self.currentPaneKey + self.currentPaneKey = pendingSwitchToPaneKey + updatedCurrentIndex = availablePanes.index(of: pendingSwitchToPaneKey) + if let previousPaneKey = previousPaneKey, let previousIndex = availablePanes.index(of: previousPaneKey), let updatedCurrentIndex = updatedCurrentIndex { + if updatedCurrentIndex < previousIndex { + paneSwitchAnimationOffset = -size.width + } else { + paneSwitchAnimationOffset = size.width + } + } + + paneDefaultTransition = .immediate + } + + for (key, pane) in self.currentPanes { + if let index = availablePanes.index(of: key), let updatedCurrentIndex = updatedCurrentIndex { + var paneWasAdded = false + if pane.node.supernode == nil { + self.addSubnode(pane.node) + paneWasAdded = true + } + let indexOffset = CGFloat(index - updatedCurrentIndex) + + let paneTransition: ContainedViewLayoutTransition = paneWasAdded ? .immediate : paneDefaultTransition + let adjustedFrame = paneFrame.offsetBy(dx: size.width * self.transitionFraction + indexOffset * size.width, dy: 0.0) + + let paneCompletion: () -> Void = { [weak self, weak pane] in + guard let strongSelf = self, let pane = pane else { + return + } + pane.isAnimatingOut = false + if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { + if let availablePanes = data?.availablePanes, let currentPaneKey = strongSelf.currentPaneKey, let currentIndex = availablePanes.index(of: currentPaneKey), let paneIndex = availablePanes.index(of: key), abs(paneIndex - currentIndex) <= 1 { + } else { + if let pane = strongSelf.currentPanes.removeValue(forKey: key) { + //print("remove \(key)") + pane.node.removeFromSupernode() + } + } + } + } + if let previousPaneKey = previousPaneKey, key == previousPaneKey { + pane.node.frame = adjustedFrame + let isAnimatingOut = pane.isAnimatingOut + pane.isAnimatingOut = true + transition.animateFrame(node: pane.node, from: paneFrame, to: paneFrame.offsetBy(dx: -paneSwitchAnimationOffset, dy: 0.0), completion: isAnimatingOut ? nil : { _ in + paneCompletion() + }) + } else if let previousPaneKey = previousPaneKey, key == self.currentPaneKey { + pane.node.frame = adjustedFrame + let isAnimatingOut = pane.isAnimatingOut + pane.isAnimatingOut = true + transition.animatePositionAdditive(node: pane.node, offset: CGPoint(x: paneSwitchAnimationOffset, y: 0.0), completion: isAnimatingOut ? nil : { + paneCompletion() + }) + } else { + let isAnimatingOut = pane.isAnimatingOut + pane.isAnimatingOut = true + paneTransition.updateFrame(node: pane.node, frame: adjustedFrame, completion: isAnimatingOut ? nil : { _ in + paneCompletion() + }) + } + pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition) + } + } + + //print("currentPanes: \(self.currentPanes.map { $0.0 })") + + transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: tabsHeight))) + self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in + let title: String + switch key { + case .media: + title = presentationData.strings.PeerInfo_PaneMedia + case .files: + title = presentationData.strings.PeerInfo_PaneFiles + case .links: + title = presentationData.strings.PeerInfo_PaneLinks + case .voice: + title = presentationData.strings.PeerInfo_PaneVoice + case .music: + title = presentationData.strings.PeerInfo_PaneAudio + case .groupsInCommon: + title = presentationData.strings.PeerInfo_PaneGroups + case .members: + title = presentationData.strings.PeerInfo_PaneMembers + } + return PeerInfoPaneSpecifier(key: key, title: title) + }, selectedPane: self.currentPaneKey, transitionFraction: self.transitionFraction, transition: transition) + + for (_, pane) in self.pendingPanes { + let paneTransition: ContainedViewLayoutTransition = .immediate + paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame) + pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: true, transition: paneTransition) + } + if !self.didSetIsReady && data != nil { + if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] { + self.didSetIsReady = true + self.isReady.set(currentPane.node.isReady) + } else if self.pendingSwitchToPaneKey == nil { + self.didSetIsReady = true + self.isReady.set(.single(true)) + } + } + if let previousCurrentPaneKey = previousCurrentPaneKey, self.currentPaneKey != previousCurrentPaneKey { + self.currentPaneUpdated?(true) + } + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoScreen.swift new file mode 100644 index 0000000000..fc1cd2f59d --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoScreen.swift @@ -0,0 +1,4493 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SyncCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import TelegramUIPreferences +import AvatarNode +import TelegramStringFormatting +import PhoneNumberFormat +import AppBundle +import PresentationDataUtils +import NotificationMuteSettingsUI +import NotificationSoundSelectionUI +import OverlayStatusController +import ShareController +import PhotoResources +import PeerAvatarGalleryUI +import TelegramIntents +import PeerInfoUI +import SearchBarNode +import SearchUI +import ContextUI +import OpenInExternalAppUI +import SafariServices +import GalleryUI +import LegacyUI +import MapResourceToAvatarSizes +import LegacyComponents +import WebSearchUI +import LocationResources +import LocationUI +import Geocoding +import TextFormat + +protocol PeerInfoScreenItem: class { + var id: AnyHashable { get } + func node() -> PeerInfoScreenItemNode +} + +class PeerInfoScreenItemNode: ASDisplayNode { + var bringToFrontForHighlight: (() -> Void)? + + func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat { + preconditionFailure() + } +} + +private final class PeerInfoScreenItemSectionContainerNode: ASDisplayNode { + private let backgroundNode: ASDisplayNode + private let topSeparatorNode: ASDisplayNode + private let bottomSeparatorNode: ASDisplayNode + private let itemContainerNode: ASDisplayNode + + private var currentItems: [PeerInfoScreenItem] = [] + private var itemNodes: [AnyHashable: PeerInfoScreenItemNode] = [:] + + override init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topSeparatorNode = ASDisplayNode() + self.topSeparatorNode.isLayerBacked = true + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + + self.itemContainerNode = ASDisplayNode() + self.itemContainerNode.clipsToBounds = true + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.itemContainerNode) + self.addSubnode(self.topSeparatorNode) + self.addSubnode(self.bottomSeparatorNode) + } + + func update(width: CGFloat, presentationData: PresentationData, items: [PeerInfoScreenItem], transition: ContainedViewLayoutTransition) -> CGFloat { + self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor + self.topSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + var contentHeight: CGFloat = 0.0 + var contentWithBackgroundHeight: CGFloat = 0.0 + var contentWithBackgroundOffset: CGFloat = 0.0 + + for i in 0 ..< items.count { + let item = items[i] + + let itemNode: PeerInfoScreenItemNode + var wasAdded = false + if let current = self.itemNodes[item.id] { + itemNode = current + } else { + wasAdded = true + itemNode = item.node() + self.itemNodes[item.id] = itemNode + self.itemContainerNode.addSubnode(itemNode) + itemNode.bringToFrontForHighlight = { [weak self, weak itemNode] in + guard let strongSelf = self, let itemNode = itemNode else { + return + } + strongSelf.view.bringSubviewToFront(itemNode.view) + } + } + + let itemTransition: ContainedViewLayoutTransition = wasAdded ? .immediate : transition + + let topItem: PeerInfoScreenItem? + if i == 0 { + topItem = nil + } else if items[i - 1] is PeerInfoScreenHeaderItem { + topItem = nil + } else { + topItem = items[i - 1] + } + + let bottomItem: PeerInfoScreenItem? + if i == items.count - 1 { + bottomItem = nil + } else if items[i + 1] is PeerInfoScreenCommentItem { + bottomItem = nil + } else { + bottomItem = items[i + 1] + } + + let itemHeight = itemNode.update(width: width, presentationData: presentationData, item: item, topItem: topItem, bottomItem: bottomItem, transition: itemTransition) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: itemHeight)) + itemTransition.updateFrame(node: itemNode, frame: itemFrame) + if wasAdded { + itemNode.alpha = 0.0 + transition.updateAlpha(node: itemNode, alpha: 1.0) + } + + if item is PeerInfoScreenCommentItem { + } else { + contentWithBackgroundHeight += itemHeight + } + contentHeight += itemHeight + + if item is PeerInfoScreenHeaderItem { + contentWithBackgroundOffset = contentHeight + } + } + + var removeIds: [AnyHashable] = [] + for (id, _) in self.itemNodes { + if !items.contains(where: { $0.id == id }) { + removeIds.append(id) + } + } + for id in removeIds { + if let itemNode = self.itemNodes.removeValue(forKey: id) { + itemNode.view.superview?.sendSubviewToBack(itemNode.view) + transition.updateAlpha(node: itemNode, alpha: 0.0, completion: { [weak itemNode] _ in + itemNode?.removeFromSupernode() + }) + } + } + + transition.updateFrame(node: self.itemContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: contentHeight))) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentWithBackgroundOffset), size: CGSize(width: width, height: max(0.0, contentWithBackgroundHeight - contentWithBackgroundOffset)))) + transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentWithBackgroundOffset - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentWithBackgroundHeight), size: CGSize(width: width, height: UIScreenPixel))) + + if contentHeight.isZero { + transition.updateAlpha(node: self.topSeparatorNode, alpha: 0.0) + transition.updateAlpha(node: self.bottomSeparatorNode, alpha: 0.0) + } else { + transition.updateAlpha(node: self.topSeparatorNode, alpha: 1.0) + transition.updateAlpha(node: self.bottomSeparatorNode, alpha: 1.0) + } + + return contentHeight + } +} + +private final class PeerInfoScreenDynamicItemSectionContainerNode: ASDisplayNode { + private let backgroundNode: ASDisplayNode + private let topSeparatorNode: ASDisplayNode + private let bottomSeparatorNode: ASDisplayNode + + private var currentItems: [PeerInfoScreenItem] = [] + private var itemNodes: [AnyHashable: PeerInfoScreenItemNode] = [:] + + override init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topSeparatorNode = ASDisplayNode() + self.topSeparatorNode.isLayerBacked = true + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.topSeparatorNode) + self.addSubnode(self.bottomSeparatorNode) + } + + func update(width: CGFloat, presentationData: PresentationData, items: [PeerInfoScreenItem], transition: ContainedViewLayoutTransition) -> CGFloat { + self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor + self.topSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + var contentHeight: CGFloat = 0.0 + var contentWithBackgroundHeight: CGFloat = 0.0 + var contentWithBackgroundOffset: CGFloat = 0.0 + + for i in 0 ..< items.count { + let item = items[i] + + let itemNode: PeerInfoScreenItemNode + var wasAdded = false + if let current = self.itemNodes[item.id] { + itemNode = current + } else { + wasAdded = true + itemNode = item.node() + self.itemNodes[item.id] = itemNode + self.addSubnode(itemNode) + itemNode.bringToFrontForHighlight = { [weak self, weak itemNode] in + guard let strongSelf = self, let itemNode = itemNode else { + return + } + strongSelf.view.bringSubviewToFront(itemNode.view) + } + } + + let itemTransition: ContainedViewLayoutTransition = wasAdded ? .immediate : transition + + let topItem: PeerInfoScreenItem? + if i == 0 { + topItem = nil + } else if items[i - 1] is PeerInfoScreenHeaderItem { + topItem = nil + } else { + topItem = items[i - 1] + } + + let bottomItem: PeerInfoScreenItem? + if i == items.count - 1 { + bottomItem = nil + } else if items[i + 1] is PeerInfoScreenCommentItem { + bottomItem = nil + } else { + bottomItem = items[i + 1] + } + + let itemHeight = itemNode.update(width: width, presentationData: presentationData, item: item, topItem: topItem, bottomItem: bottomItem, transition: itemTransition) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: itemHeight)) + itemTransition.updateFrame(node: itemNode, frame: itemFrame) + if wasAdded { + itemNode.alpha = 0.0 + transition.updateAlpha(node: itemNode, alpha: 1.0) + } + + if item is PeerInfoScreenCommentItem { + } else { + contentWithBackgroundHeight += itemHeight + } + contentHeight += itemHeight + + if item is PeerInfoScreenHeaderItem { + contentWithBackgroundOffset = contentHeight + } + } + + var removeIds: [AnyHashable] = [] + for (id, _) in self.itemNodes { + if !items.contains(where: { $0.id == id }) { + removeIds.append(id) + } + } + for id in removeIds { + if let itemNode = self.itemNodes.removeValue(forKey: id) { + transition.updateAlpha(node: itemNode, alpha: 0.0, completion: { [weak itemNode] _ in + itemNode?.removeFromSupernode() + }) + } + } + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentWithBackgroundOffset), size: CGSize(width: width, height: max(0.0, contentWithBackgroundHeight - contentWithBackgroundOffset)))) + transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentWithBackgroundOffset - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentWithBackgroundHeight), size: CGSize(width: width, height: UIScreenPixel))) + + if contentHeight.isZero { + transition.updateAlpha(node: self.topSeparatorNode, alpha: 0.0) + transition.updateAlpha(node: self.bottomSeparatorNode, alpha: 0.0) + } else { + transition.updateAlpha(node: self.topSeparatorNode, alpha: 1.0) + transition.updateAlpha(node: self.bottomSeparatorNode, alpha: 1.0) + } + + return contentHeight + } + + func updateVisibleItems(in rect: CGRect) { + + } +} + +final class PeerInfoSelectionPanelNode: ASDisplayNode { + private let context: AccountContext + private let peerId: PeerId + + private let deleteMessages: () -> Void + private let shareMessages: () -> Void + private let forwardMessages: () -> Void + private let reportMessages: () -> Void + + let selectionPanel: ChatMessageSelectionInputPanelNode + let separatorNode: ASDisplayNode + let backgroundNode: ASDisplayNode + + init(context: AccountContext, peerId: PeerId, deleteMessages: @escaping () -> Void, shareMessages: @escaping () -> Void, forwardMessages: @escaping () -> Void, reportMessages: @escaping () -> Void) { + self.context = context + self.peerId = peerId + self.deleteMessages = deleteMessages + self.shareMessages = shareMessages + self.forwardMessages = forwardMessages + self.reportMessages = reportMessages + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + self.separatorNode = ASDisplayNode() + self.backgroundNode = ASDisplayNode() + + self.selectionPanel = ChatMessageSelectionInputPanelNode(theme: presentationData.theme, strings: presentationData.strings, peerMedia: true) + self.selectionPanel.context = context + self.selectionPanel.backgroundColor = presentationData.theme.chat.inputPanel.panelBackgroundColor + + let interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _, _ in + }, setupEditMessage: { _, _ in + }, beginMessageSelection: { _, _ in + }, deleteSelectedMessages: { + deleteMessages() + }, reportSelectedMessages: { + reportMessages() + }, reportMessages: { _, _ in + }, deleteMessages: { _, _, f in + f(.default) + }, forwardSelectedMessages: { + forwardMessages() + }, forwardCurrentForwardMessages: { + }, forwardMessages: { _ in + }, shareSelectedMessages: { + shareMessages() + }, updateTextInputStateAndMode: { _ in + }, updateInputModeAndDismissedButtonKeyboardMessageId: { _ in + }, openStickers: { + }, editMessage: { + }, beginMessageSearch: { _, _ in + }, dismissMessageSearch: { + }, updateMessageSearch: { _ in + }, openSearchResults: { + }, navigateMessageSearch: { _ in + }, openCalendarSearch: { + }, toggleMembersSearch: { _ in + }, navigateToMessage: { _ in + }, navigateToChat: { _ in + }, openPeerInfo: { + }, togglePeerNotifications: { + }, sendContextResult: { _, _, _, _ in + return false + }, sendBotCommand: { _, _ in + }, sendBotStart: { _ in + }, botSwitchChatWithPayload: { _, _ in + }, beginMediaRecording: { _ in + }, finishMediaRecording: { _ in + }, stopMediaRecording: { + }, lockMediaRecording: { + }, deleteRecordedMedia: { + }, sendRecordedMedia: { + }, displayRestrictedInfo: { _, _ in + }, displayVideoUnmuteTip: { _ in + }, switchMediaRecordingMode: { + }, setupMessageAutoremoveTimeout: { + }, sendSticker: { _, _, _ in + return false + }, unblockPeer: { + }, pinMessage: { _ in + }, unpinMessage: { + }, shareAccountContact: { + }, reportPeer: { + }, presentPeerContact: { + }, dismissReportPeer: { + }, deleteChat: { + }, beginCall: { + }, toggleMessageStickerStarred: { _ in + }, presentController: { _, _ in + }, getNavigationController: { + return nil + }, presentGlobalOverlayController: { _, _ in + }, navigateFeed: { + }, openGrouping: { + }, toggleSilentPost: { + }, requestUnvoteInMessage: { _ in + }, requestStopPollInMessage: { _ in + }, updateInputLanguage: { _ in + }, unarchiveChat: { + }, openLinkEditing: { + }, reportPeerIrrelevantGeoLocation: { + }, displaySlowmodeTooltip: { _, _ in + }, displaySendMessageOptions: { _, _ in + }, openScheduledMessages: { + }, displaySearchResultsTooltip: { _, _ in + }, statuses: nil) + + self.selectionPanel.interfaceInteraction = interfaceInteraction + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.selectionPanel) + } + + func update(layout: ContainerViewLayout, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat { + self.backgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor + self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor + + let interfaceState = ChatPresentationInterfaceState(chatWallpaper: .color(0), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: .defaultValue, fontSize: .regular, bubbleCorners: PresentationChatBubbleCorners(mainRadius: 16.0, auxiliaryRadius: 8.0, mergeBubbleCorners: true), accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(self.peerId), isScheduledMessages: false) + let panelHeight = self.selectionPanel.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: 0.0, isSecondary: false, transition: transition, interfaceState: interfaceState, metrics: layout.metrics) + + transition.updateFrame(node: self.selectionPanel, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: panelHeight))) + + let panelHeightWithInset = panelHeight + layout.intrinsicInsets.bottom + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: panelHeightWithInset))) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + + return panelHeightWithInset + } +} + +private enum PeerInfoBotCommand { + case settings + case help + case privacy +} + +private enum PeerInfoParticipantsSection { + case members + case admins + case banned +} + +private enum PeerInfoMemberAction { + case promote + case restrict + case remove +} + +private enum PeerInfoContextSubject { + case bio + case phone(String) + case link +} + +private final class PeerInfoInteraction { + let openChat: () -> Void + let openUsername: (String) -> Void + let openPhone: (String) -> Void + let editingOpenNotificationSettings: () -> Void + let editingOpenSoundSettings: () -> Void + let editingToggleShowMessageText: (Bool) -> Void + let requestDeleteContact: () -> Void + let openAddContact: () -> Void + let updateBlocked: (Bool) -> Void + let openReport: (Bool) -> Void + let openShareBot: () -> Void + let openAddBotToGroup: () -> Void + let performBotCommand: (PeerInfoBotCommand) -> Void + let editingOpenPublicLinkSetup: () -> Void + let editingOpenDiscussionGroupSetup: () -> Void + let editingToggleMessageSignatures: (Bool) -> Void + let openParticipantsSection: (PeerInfoParticipantsSection) -> Void + let editingOpenPreHistorySetup: () -> Void + let openPermissions: () -> Void + let editingOpenStickerPackSetup: () -> Void + let openLocation: () -> Void + let editingOpenSetupLocation: () -> Void + let openPeerInfo: (Peer) -> Void + let performMemberAction: (PeerInfoMember, PeerInfoMemberAction) -> Void + let openPeerInfoContextMenu: (PeerInfoContextSubject, ASDisplayNode) -> Void + let performBioLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void + let requestLayout: () -> Void + let openEncryptionKey: () -> Void + + init( + openUsername: @escaping (String) -> Void, + openPhone: @escaping (String) -> Void, + editingOpenNotificationSettings: @escaping () -> Void, + editingOpenSoundSettings: @escaping () -> Void, + editingToggleShowMessageText: @escaping (Bool) -> Void, + requestDeleteContact: @escaping () -> Void, + openChat: @escaping () -> Void, + openAddContact: @escaping () -> Void, + updateBlocked: @escaping (Bool) -> Void, + openReport: @escaping (Bool) -> Void, + openShareBot: @escaping () -> Void, + openAddBotToGroup: @escaping () -> Void, + performBotCommand: @escaping (PeerInfoBotCommand) -> Void, + editingOpenPublicLinkSetup: @escaping () -> Void, + editingOpenDiscussionGroupSetup: @escaping () -> Void, + editingToggleMessageSignatures: @escaping (Bool) -> Void, + openParticipantsSection: @escaping (PeerInfoParticipantsSection) -> Void, + editingOpenPreHistorySetup: @escaping () -> Void, + openPermissions: @escaping () -> Void, + editingOpenStickerPackSetup: @escaping () -> Void, + openLocation: @escaping () -> Void, + editingOpenSetupLocation: @escaping () -> Void, + openPeerInfo: @escaping (Peer) -> Void, + performMemberAction: @escaping (PeerInfoMember, PeerInfoMemberAction) -> Void, + openPeerInfoContextMenu: @escaping (PeerInfoContextSubject, ASDisplayNode) -> Void, + performBioLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void, + requestLayout: @escaping () -> Void, + openEncryptionKey: @escaping () -> Void + ) { + self.openUsername = openUsername + self.openPhone = openPhone + self.editingOpenNotificationSettings = editingOpenNotificationSettings + self.editingOpenSoundSettings = editingOpenSoundSettings + self.editingToggleShowMessageText = editingToggleShowMessageText + self.requestDeleteContact = requestDeleteContact + self.openChat = openChat + self.openAddContact = openAddContact + self.updateBlocked = updateBlocked + self.openReport = openReport + self.openShareBot = openShareBot + self.openAddBotToGroup = openAddBotToGroup + self.performBotCommand = performBotCommand + self.editingOpenPublicLinkSetup = editingOpenPublicLinkSetup + self.editingOpenDiscussionGroupSetup = editingOpenDiscussionGroupSetup + self.editingToggleMessageSignatures = editingToggleMessageSignatures + self.openParticipantsSection = openParticipantsSection + self.editingOpenPreHistorySetup = editingOpenPreHistorySetup + self.openPermissions = openPermissions + self.editingOpenStickerPackSetup = editingOpenStickerPackSetup + self.openLocation = openLocation + self.editingOpenSetupLocation = editingOpenSetupLocation + self.openPeerInfo = openPeerInfo + self.performMemberAction = performMemberAction + self.openPeerInfoContextMenu = openPeerInfoContextMenu + self.performBioLinkAction = performBioLinkAction + self.requestLayout = requestLayout + self.openEncryptionKey = openEncryptionKey + } +} + +private let enabledBioEntities: EnabledEntityTypes = [.url, .mention, .hashtag] + +private func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction, nearbyPeer: Bool, callMessages: [Message]) -> [(AnyHashable, [PeerInfoScreenItem])] { + guard let data = data else { + return [] + } + + enum Section: Int, CaseIterable { + case groupLocation + case calls + case peerInfo + case peerMembers + } + + var items: [Section: [PeerInfoScreenItem]] = [:] + for section in Section.allCases { + items[section] = [] + } + + let bioContextAction: (ASDisplayNode) -> Void = { sourceNode in + interaction.openPeerInfoContextMenu(.bio, sourceNode) + } + let bioLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void = { action, item in + interaction.performBioLinkAction(action, item) + } + + if let user = data.peer as? TelegramUser { + if !callMessages.isEmpty { + items[.calls]!.append(PeerInfoScreenCallListItem(id: 20, messages: callMessages)) + } + + if let phone = user.phone { + let formattedPhone = formatPhoneNumber(phone) + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 2, label: presentationData.strings.ContactInfo_PhoneLabelMobile, text: formattedPhone, textColor: .accent, action: { + interaction.openPhone(phone) + }, longTapAction: { sourceNode in + interaction.openPeerInfoContextMenu(.phone(formattedPhone), sourceNode) + }, requestLayout: { + interaction.requestLayout() + })) + } + if let username = user.username { + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 1, label: presentationData.strings.Profile_Username, text: "@\(username)", textColor: .accent, action: { + interaction.openUsername(username) + }, longTapAction: { sourceNode in + interaction.openPeerInfoContextMenu(.link, sourceNode) + }, requestLayout: { + interaction.requestLayout() + })) + } + if let cachedData = data.cachedData as? CachedUserData { + if user.isScam { + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Channel_AboutItem, text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledBioEntities : []), action: nil, requestLayout: { + interaction.requestLayout() + })) + } else if let about = cachedData.about, !about.isEmpty { + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Channel_AboutItem, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: []), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: { + interaction.requestLayout() + })) + } + } + if nearbyPeer { + items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 3, text: presentationData.strings.UserInfo_SendMessage, action: { + interaction.openChat() + })) + + items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.ReportPeer_Report, color: .destructive, action: { + interaction.openReport(true) + })) + } else { + if !data.isContact { + if user.botInfo == nil { + items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 3, text: presentationData.strings.PeerInfo_AddToContacts, action: { + interaction.openAddContact() + })) + } + } + + if let cachedData = data.cachedData as? CachedUserData { + if cachedData.isBlocked { + items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: user.botInfo != nil ? presentationData.strings.Bot_Unblock : presentationData.strings.Conversation_Unblock, action: { + interaction.updateBlocked(false) + })) + } else { + if user.flags.contains(.isSupport) || data.isContact { + } else { + items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: user.botInfo != nil ? presentationData.strings.Bot_Stop : presentationData.strings.Conversation_BlockUser, color: .destructive, action: { + interaction.updateBlocked(true) + })) + } + } + } + } + + if let encryptionKeyFingerprint = data.encryptionKeyFingerprint { + items[.peerInfo]!.append(PeerInfoScreenDisclosureEncryptionKeyItem(id: 6, text: presentationData.strings.Profile_EncryptionKey, fingerprint: encryptionKeyFingerprint, action: { + interaction.openEncryptionKey() + })) + } + + if user.botInfo != nil, !user.isVerified { + items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 5, text: presentationData.strings.ReportPeer_Report, action: { + interaction.openReport(false) + })) + } + } else if let channel = data.peer as? TelegramChannel { + let ItemUsername = 1 + let ItemAbout = 2 + let ItemAdmins = 3 + let ItemMembers = 4 + let ItemBanned = 5 + let ItemReport = 6 + let ItemLocationHeader = 7 + let ItemLocation = 8 + + if let location = (data.cachedData as? CachedChannelData)?.peerGeoLocation { + items[.groupLocation]!.append(PeerInfoScreenHeaderItem(id: ItemLocationHeader, text: presentationData.strings.GroupInfo_Location.uppercased())) + + let imageSignal = chatMapSnapshotImage(account: context.account, resource: MapSnapshotMediaResource(latitude: location.latitude, longitude: location.longitude, width: 90, height: 90)) + items[.groupLocation]!.append(PeerInfoScreenAddressItem( + id: ItemLocation, + label: "", + text: location.address.replacingOccurrences(of: ", ", with: "\n"), + imageSignal: imageSignal, + action: { + interaction.openLocation() + } + )) + } + + if let username = channel.username { + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemUsername, label: presentationData.strings.Channel_LinkItem, text: "https://t.me/\(username)", textColor: .accent, action: { + interaction.openUsername(username) + }, longTapAction: { sourceNode in + interaction.openPeerInfoContextMenu(.link, sourceNode) + }, requestLayout: { + interaction.requestLayout() + })) + } + if let cachedData = data.cachedData as? CachedChannelData { + if channel.isScam { + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_AboutItem, text: presentationData.strings.GroupInfo_ScamGroupWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, requestLayout: { + interaction.requestLayout() + })) + } else if let about = cachedData.about, !about.isEmpty { + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_AboutItem, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: { + interaction.requestLayout() + })) + } + + if case .broadcast = channel.info { + var canEditMembers = false + if channel.hasPermission(.banMembers) { + canEditMembers = true + } + if canEditMembers { + if channel.adminRights != nil || channel.flags.contains(.isCreator) { + let adminCount = cachedData.participantsSummary.adminCount ?? 0 + let memberCount = cachedData.participantsSummary.memberCount ?? 0 + let bannedCount = cachedData.participantsSummary.kickedCount ?? 0 + + items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemAdmins, label: "\(adminCount == 0 ? "" : "\(presentationStringsFormattedNumber(adminCount, presentationData.dateTimeFormat.groupingSeparator))")", text: presentationData.strings.GroupInfo_Administrators, action: { + interaction.openParticipantsSection(.admins) + })) + items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemMembers, label: "\(memberCount == 0 ? "" : "\(presentationStringsFormattedNumber(memberCount, presentationData.dateTimeFormat.groupingSeparator))")", text: presentationData.strings.Channel_Info_Subscribers, action: { + interaction.openParticipantsSection(.members) + })) + items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemBanned, label: "\(bannedCount == 0 ? "" : "\(presentationStringsFormattedNumber(bannedCount, presentationData.dateTimeFormat.groupingSeparator))")", text: presentationData.strings.GroupInfo_Permissions_Removed, action: { + interaction.openParticipantsSection(.banned) + })) + } + } + } + } + } else if let group = data.peer as? TelegramGroup { + if let cachedData = data.cachedData as? CachedGroupData { + if group.isScam { + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Channel_AboutItem, text: presentationData.strings.GroupInfo_ScamGroupWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, requestLayout: { + interaction.requestLayout() + })) + } else if let about = cachedData.about, !about.isEmpty { + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Channel_AboutItem, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: { + interaction.requestLayout() + })) + } + } + } + + if let peer = data.peer, let members = data.members, case let .shortList(_, memberList) = members { + for member in memberList { + var presence = member.presence + let isAccountPeer = member.id == context.account.peerId + if isAccountPeer { + presence = TelegramUserPresence(status: .present(until: Int32.max - 1), lastActivity: 0) + } + items[.peerMembers]!.append(PeerInfoScreenMemberItem(id: member.id, context: context, enclosingPeer: peer, member: member, action: isAccountPeer ? nil : { action in + switch action { + case .open: + interaction.openPeerInfo(member.peer) + case .promote: + interaction.performMemberAction(member, .promote) + case .restrict: + interaction.performMemberAction(member, .restrict) + case .remove: + interaction.performMemberAction(member, .remove) + } + })) + } + } + + var result: [(AnyHashable, [PeerInfoScreenItem])] = [] + for section in Section.allCases { + if let sectionItems = items[section], !sectionItems.isEmpty { + result.append((section, sectionItems)) + } + } + return result +} + +private func editingItems(data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction) -> [(AnyHashable, [PeerInfoScreenItem])] { + enum Section: Int, CaseIterable { + case notifications + case groupLocation + case peerPublicSettings + case peerSettings + } + + var items: [Section: [PeerInfoScreenItem]] = [:] + for section in Section.allCases { + items[section] = [] + } + + if let data = data, let notificationSettings = data.notificationSettings { + let notificationsLabel: String + let soundLabel: String + let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings ?? TelegramPeerNotificationSettings.defaultSettings + if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { + if until < Int32.max - 1 { + notificationsLabel = stringForRemainingMuteInterval(strings: presentationData.strings, muteInterval: until) + } else { + notificationsLabel = presentationData.strings.UserInfo_NotificationsDisabled + } + } else { + notificationsLabel = presentationData.strings.UserInfo_NotificationsEnabled + } + + let globalNotificationSettings: GlobalNotificationSettings = data.globalNotificationSettings ?? GlobalNotificationSettings.defaultSettings + soundLabel = localizedPeerNotificationSoundString(strings: presentationData.strings, sound: notificationSettings.messageSound, default: globalNotificationSettings.effective.privateChats.sound) + + items[.notifications]!.append(PeerInfoScreenDisclosureItem(id: 0, label: notificationsLabel, text: presentationData.strings.GroupInfo_Notifications, action: { + interaction.editingOpenNotificationSettings() + })) + items[.notifications]!.append(PeerInfoScreenDisclosureItem(id: 1, label: soundLabel, text: presentationData.strings.GroupInfo_Sound, action: { + interaction.editingOpenSoundSettings() + })) + items[.notifications]!.append(PeerInfoScreenSwitchItem(id: 2, text: presentationData.strings.Notification_Exceptions_PreviewAlwaysOn, value: notificationSettings.displayPreviews != .hide, toggled: { value in + interaction.editingToggleShowMessageText(value) + })) + } + + if let data = data { + if let user = data.peer as? TelegramUser { + let ItemDelete = 0 + if data.isContact { + items[.peerSettings]!.append(PeerInfoScreenActionItem(id: ItemDelete, text: presentationData.strings.UserInfo_DeleteContact, color: .destructive, action: { + interaction.requestDeleteContact() + })) + } + } else if let channel = data.peer as? TelegramChannel { + let ItemUsername = 1 + let ItemDiscussionGroup = 2 + let ItemSignMessages = 3 + let ItemSignMessagesHelp = 4 + + switch channel.info { + case .broadcast: + if channel.flags.contains(.isCreator) { + let linkText: String + if let username = channel.username { + linkText = "@\(username)" + } else { + linkText = presentationData.strings.Channel_Setup_TypePrivate + } + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: linkText, text: presentationData.strings.Channel_TypeSetup_Title, action: { + interaction.editingOpenPublicLinkSetup() + })) + } + + if channel.flags.contains(.isCreator) || (channel.adminRights != nil && channel.hasPermission(.pinMessages)) { + let discussionGroupTitle: String + if let cachedData = data.cachedData as? CachedChannelData { + if let peer = data.linkedDiscussionPeer { + if let addressName = peer.addressName, !addressName.isEmpty { + discussionGroupTitle = "@\(addressName)" + } else { + discussionGroupTitle = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + } + } else { + discussionGroupTitle = presentationData.strings.Channel_DiscussionGroupAdd + } + } else { + discussionGroupTitle = "..." + } + + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemDiscussionGroup, label: discussionGroupTitle, text: presentationData.strings.Channel_DiscussionGroup, action: { + interaction.editingOpenDiscussionGroupSetup() + })) + + let messagesShouldHaveSignatures: Bool + switch channel.info { + case let .broadcast(info): + messagesShouldHaveSignatures = info.flags.contains(.messagesShouldHaveSignatures) + default: + messagesShouldHaveSignatures = false + } + items[.peerSettings]!.append(PeerInfoScreenSwitchItem(id: ItemSignMessages, text: presentationData.strings.Channel_SignMessages, value: messagesShouldHaveSignatures, toggled: { value in + interaction.editingToggleMessageSignatures(value) + })) + items[.peerSettings]!.append(PeerInfoScreenCommentItem(id: ItemSignMessagesHelp, text: presentationData.strings.Channel_SignMessages_Help)) + } + case .group: + let ItemUsername = 1 + let ItemLinkedChannel = 2 + let ItemPreHistory = 3 + let ItemStickerPack = 4 + let ItemPermissions = 5 + let ItemAdmins = 6 + let ItemLocationHeader = 7 + let ItemLocation = 8 + let ItemLocationSetup = 9 + + let isCreator = channel.flags.contains(.isCreator) + let isPublic = channel.username != nil + + if let cachedData = data.cachedData as? CachedChannelData { + if isCreator, let location = cachedData.peerGeoLocation { + items[.groupLocation]!.append(PeerInfoScreenHeaderItem(id: ItemLocationHeader, text: presentationData.strings.GroupInfo_Location.uppercased())) + + let imageSignal = chatMapSnapshotImage(account: context.account, resource: MapSnapshotMediaResource(latitude: location.latitude, longitude: location.longitude, width: 90, height: 90)) + items[.groupLocation]!.append(PeerInfoScreenAddressItem( + id: ItemLocation, + label: "", + text: location.address.replacingOccurrences(of: ", ", with: "\n"), + imageSignal: imageSignal, + action: { + interaction.openLocation() + } + )) + if cachedData.flags.contains(.canChangePeerGeoLocation) { + items[.groupLocation]!.append(PeerInfoScreenActionItem(id: ItemLocationSetup, text: presentationData.strings.Group_Location_ChangeLocation, action: { + interaction.editingOpenSetupLocation() + })) + } + } + + if isCreator || (channel.adminRights != nil && channel.hasPermission(.pinMessages)) { + if cachedData.peerGeoLocation != nil { + if isCreator { + let linkText: String + if let username = channel.username { + linkText = "@\(username)" + } else { + linkText = presentationData.strings.GroupInfo_PublicLinkAdd + } + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: linkText, text: presentationData.strings.GroupInfo_PublicLink, action: { + interaction.editingOpenPublicLinkSetup() + })) + } + } else { + if cachedData.flags.contains(.canChangeUsername) { + items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: isPublic ? presentationData.strings.Channel_Setup_TypePublic : presentationData.strings.Channel_Setup_TypePrivate, text: presentationData.strings.GroupInfo_GroupType, action: { + interaction.editingOpenPublicLinkSetup() + })) + + if let linkedDiscussionPeer = data.linkedDiscussionPeer { + let peerTitle: String + if let addressName = linkedDiscussionPeer.addressName, !addressName.isEmpty { + peerTitle = "@\(addressName)" + } else { + peerTitle = linkedDiscussionPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + } + items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemLinkedChannel, label: peerTitle, text: presentationData.strings.Group_LinkedChannel, action: { + interaction.editingOpenDiscussionGroupSetup() + })) + } + } + if !isPublic && cachedData.linkedDiscussionPeerId == nil { + items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPreHistory, label: cachedData.flags.contains(.preHistoryEnabled) ? presentationData.strings.GroupInfo_GroupHistoryVisible : presentationData.strings.GroupInfo_GroupHistoryHidden, text: presentationData.strings.GroupInfo_GroupHistory, action: { + interaction.editingOpenPreHistorySetup() + })) + } + } + } + + if cachedData.flags.contains(.canSetStickerSet) && canEditPeerInfo(peer: channel) { + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemStickerPack, label: cachedData.stickerPack?.title ?? presentationData.strings.GroupInfo_SharedMediaNone, text: presentationData.strings.Stickers_GroupStickers, action: { + interaction.editingOpenStickerPackSetup() + })) + } + + var canViewAdminsAndBanned = false + if let adminRights = channel.adminRights, !adminRights.isEmpty { + canViewAdminsAndBanned = true + } else if channel.flags.contains(.isCreator) { + canViewAdminsAndBanned = true + } + + if canViewAdminsAndBanned { + var activePermissionCount: Int? + if let defaultBannedRights = channel.defaultBannedRights { + var count = 0 + for (right, _) in allGroupPermissionList { + if !defaultBannedRights.flags.contains(right) { + count += 1 + } + } + activePermissionCount = count + } + + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPermissions, label: activePermissionCount.flatMap({ "\($0)/\(allGroupPermissionList.count)" }) ?? "", text: presentationData.strings.GroupInfo_Permissions, action: { + interaction.openPermissions() + })) + + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAdmins, label: cachedData.participantsSummary.adminCount.flatMap { "\(presentationStringsFormattedNumber($0, presentationData.dateTimeFormat.groupingSeparator))" } ?? "", text: presentationData.strings.GroupInfo_Administrators, action: { + interaction.openParticipantsSection(.admins) + })) + } + } + } + } else if let group = data.peer as? TelegramGroup { + let ItemUsername = 1 + let ItemPreHistory = 2 + let ItemPermissions = 3 + let ItemAdmins = 4 + + if case .creator = group.role { + if let cachedData = data.cachedData as? CachedGroupData { + if cachedData.flags.contains(.canChangeUsername) { + items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: presentationData.strings.Group_Setup_TypePrivate, text: presentationData.strings.GroupInfo_GroupType, action: { + interaction.editingOpenPublicLinkSetup() + })) + } + } + items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPreHistory, label: presentationData.strings.GroupInfo_GroupHistoryHidden, text: presentationData.strings.GroupInfo_GroupHistory, action: { + interaction.editingOpenPreHistorySetup() + })) + var activePermissionCount: Int? + if let defaultBannedRights = group.defaultBannedRights { + var count = 0 + for (right, _) in allGroupPermissionList { + if !defaultBannedRights.flags.contains(right) { + count += 1 + } + } + activePermissionCount = count + } + + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPermissions, label: activePermissionCount.flatMap({ "\($0)/\(allGroupPermissionList.count)" }) ?? "", text: presentationData.strings.GroupInfo_Permissions, action: { + interaction.openPermissions() + })) + + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAdmins, label: "", text: presentationData.strings.GroupInfo_Administrators, action: { + interaction.openParticipantsSection(.admins) + })) + } + } + } + + var result: [(AnyHashable, [PeerInfoScreenItem])] = [] + for section in Section.allCases { + if let sectionItems = items[section], !sectionItems.isEmpty { + result.append((section, sectionItems)) + } + } + return result +} + +private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate { + private weak var controller: PeerInfoScreen? + + private let context: AccountContext + private let peerId: PeerId + private let isOpenedFromChat: Bool + private let callMessages: [Message] + + private let isMediaOnly: Bool + + private var presentationData: PresentationData + + let scrollNode: ASScrollNode + + let headerNode: PeerInfoHeaderNode + private var regularSections: [AnyHashable: PeerInfoScreenItemSectionContainerNode] = [:] + private var editingSections: [AnyHashable: PeerInfoScreenItemSectionContainerNode] = [:] + private let paneContainerNode: PeerInfoPaneContainerNode + private var ignoreScrolling: Bool = false + private var hapticFeedback: HapticFeedback? + + private var searchDisplayController: SearchDisplayController? + + private var _interaction: PeerInfoInteraction? + private var interaction: PeerInfoInteraction { + return self._interaction! + } + + private var _chatInterfaceInteraction: ChatControllerInteraction? + private var chatInterfaceInteraction: ChatControllerInteraction { + return self._chatInterfaceInteraction! + } + private var hiddenMediaDisposable: Disposable? + private let hiddenAvatarRepresentationDisposable = MetaDisposable() + + private(set) var validLayout: (ContainerViewLayout, CGFloat)? + private(set) var data: PeerInfoScreenData? + private(set) var state = PeerInfoState( + isEditing: false, + selectedMessageIds: nil, + updatingAvatar: nil + ) + private let nearbyPeer: Bool + private var dataDisposable: Disposable? + + private let activeActionDisposable = MetaDisposable() + private let resolveUrlDisposable = MetaDisposable() + private let toggleShouldChannelMessagesSignaturesDisposable = MetaDisposable() + private let selectAddMemberDisposable = MetaDisposable() + private let addMemberDisposable = MetaDisposable() + + private let updateAvatarDisposable = MetaDisposable() + private let currentAvatarMixin = Atomic(value: nil) + + private var groupMembersSearchContext: GroupMembersSearchContext? + + private let _ready = Promise() + var ready: Promise { + return self._ready + } + private var didSetReady = false + + init(controller: PeerInfoScreen, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeer: Bool, callMessages: [Message]) { + self.controller = controller + self.context = context + self.peerId = peerId + self.isOpenedFromChat = isOpenedFromChat + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.nearbyPeer = nearbyPeer + self.callMessages = callMessages + self.isMediaOnly = context.account.peerId == peerId + + self.scrollNode = ASScrollNode() + self.scrollNode.view.delaysContentTouches = false + + self.headerNode = PeerInfoHeaderNode(context: context, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat) + self.paneContainerNode = PeerInfoPaneContainerNode(context: context, peerId: peerId) + + super.init() + + self._interaction = PeerInfoInteraction( + openUsername: { [weak self] value in + self?.openUsername(value: value) + }, + openPhone: { [weak self] value in + self?.openPhone(value: value) + }, + editingOpenNotificationSettings: { [weak self] in + self?.editingOpenNotificationSettings() + }, + editingOpenSoundSettings: { [weak self] in + self?.editingOpenSoundSettings() + }, + editingToggleShowMessageText: { [weak self] value in + self?.editingToggleShowMessageText(value: value) + }, + requestDeleteContact: { [weak self] in + self?.requestDeleteContact() + }, + openChat: { [weak self] in + self?.openChat() + }, + openAddContact: { [weak self] in + self?.openAddContact() + }, + updateBlocked: { [weak self] block in + self?.updateBlocked(block: block) + }, + openReport: { [weak self] user in + self?.openReport(user: user) + }, + openShareBot: { [weak self] in + self?.openShareBot() + }, + openAddBotToGroup: { [weak self] in + self?.openAddBotToGroup() + }, + performBotCommand: { [weak self] command in + self?.performBotCommand(command: command) + }, + editingOpenPublicLinkSetup: { [weak self] in + self?.editingOpenPublicLinkSetup() + }, + editingOpenDiscussionGroupSetup: { [weak self] in + self?.editingOpenDiscussionGroupSetup() + }, + editingToggleMessageSignatures: { [weak self] value in + self?.editingToggleMessageSignatures(value: value) + }, + openParticipantsSection: { [weak self] section in + self?.openParticipantsSection(section: section) + }, + editingOpenPreHistorySetup: { [weak self] in + self?.editingOpenPreHistorySetup() + }, + openPermissions: { [weak self] in + self?.openPermissions() + }, + editingOpenStickerPackSetup: { [weak self] in + self?.editingOpenStickerPackSetup() + }, + openLocation: { [weak self] in + self?.openLocation() + }, + editingOpenSetupLocation: { [weak self] in + self?.editingOpenSetupLocation() + }, + openPeerInfo: { [weak self] peer in + self?.openPeerInfo(peer: peer) + }, + performMemberAction: { [weak self] member, action in + self?.performMemberAction(member: member, action: action) + }, + openPeerInfoContextMenu: { [weak self] subject, sourceNode in + self?.openPeerInfoContextMenu(subject: subject, sourceNode: sourceNode) + }, + performBioLinkAction: { [weak self] action, item in + self?.performBioLinkAction(action: action, item: item) + }, + requestLayout: { [weak self] in + self?.requestLayout() + }, + openEncryptionKey: { [weak self] in + self?.openEncryptionKey() + } + ) + + self._chatInterfaceInteraction = ChatControllerInteraction(openMessage: { [weak self] message, mode in + guard let strongSelf = self else { + return false + } + return strongSelf.openMessage(id: message.id) + }, openPeer: { [weak self] id, navigation, _ in + if let id = id { + self?.openPeer(peerId: id, navigation: navigation) + } + }, openPeerMention: { _ in + }, openMessageContextMenu: { [weak self] message, _, _, _, _ in + guard let strongSelf = self else { + return + } + let items = (chatAvailableMessageActionsImpl(postbox: strongSelf.context.account.postbox, accountPeerId: strongSelf.context.account.peerId, messageIds: [message.id]) + |> deliverOnMainQueue).start(next: { actions in + var messageIds = Set() + messageIds.insert(message.id) + + if let strongSelf = self { + if let message = strongSelf.paneContainerNode.findLoadedMessage(id: message.id) { + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) + var items: [ActionSheetButtonItem] = [] + + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.SharedMedia_ViewInChat, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), subject: .message(message.id))) + } + })) + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuForward, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.forwardMessages(messageIds: messageIds) + } + })) + if actions.options.contains(.deleteLocally) || actions.options.contains(.deleteGlobally) { + items.append( ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuDelete, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.deleteMessages(messageIds: Set(messageIds)) + } + })) + } + if strongSelf.searchDisplayController == nil { + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuMore, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.chatInterfaceInteraction.toggleMessagesSelection([message.id], true) + strongSelf.expandTabs() + } + })) + } + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.view.endEditing(true) + strongSelf.controller?.present(actionSheet, in: .window(.root)) + } + } + }) + }, openMessageContextActions: { [weak self] message, node, rect, gesture in + guard let strongSelf = self else { + gesture?.cancel() + return + } + + let _ = (chatMediaListPreviewControllerData(context: strongSelf.context, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: strongSelf.controller?.navigationController as? NavigationController) + |> deliverOnMainQueue).start(next: { previewData in + guard let strongSelf = self else { + gesture?.cancel() + return + } + if let previewData = previewData { + let context = strongSelf.context + let strings = strongSelf.presentationData.strings + let items = chatAvailableMessageActionsImpl(postbox: strongSelf.context.account.postbox, accountPeerId: strongSelf.context.account.peerId, messageIds: [message.id]) + |> map { actions -> [ContextMenuItem] in + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { c, f in + c.dismiss(completion: { + if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), subject: .message(message.id))) + } + }) + }))) + + items.append(.action(ContextMenuActionItem(text: strings.Conversation_ContextMenuForward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { c, f in + c.dismiss(completion: { + if let strongSelf = self { + strongSelf.forwardMessages(messageIds: [message.id]) + } + }) + }))) + + if actions.options.contains(.deleteLocally) || actions.options.contains(.deleteGlobally) { + items.append(.action(ContextMenuActionItem(text: strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { c, f in + c.setItems(context.account.postbox.transaction { transaction -> [ContextMenuItem] in + var items: [ContextMenuItem] = [] + let messageIds = [message.id] + + if let peer = transaction.getPeer(message.id.peerId) { + var personalPeerName: String? + var isChannel = false + if let user = peer as? TelegramUser { + personalPeerName = user.compactDisplayTitle + } else if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + isChannel = true + } + + if actions.options.contains(.deleteGlobally) { + let globalTitle: String + if isChannel { + globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe + } else if let personalPeerName = personalPeerName { + globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).0 + } else { + globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone + } + items.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { c, f in + c.dismiss(completion: { + if let strongSelf = self { + strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone) + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start() + } + }) + }))) + } + + if actions.options.contains(.deleteLocally) { + var localOptionText = strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe + if strongSelf.context.account.peerId == strongSelf.peerId { + if messageIds.count == 1 { + localOptionText = strongSelf.presentationData.strings.Conversation_Moderate_Delete + } else { + localOptionText = strongSelf.presentationData.strings.Conversation_DeleteManyMessages + } + } + items.append(.action(ContextMenuActionItem(text: localOptionText, textColor: .destructive, icon: { _ in nil }, action: { c, f in + c.dismiss(completion: { + if let strongSelf = self { + strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone) + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forLocalPeer).start() + } + }) + }))) + } + } + + return items + }) + }))) + } + + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: strings.Conversation_ContextMenuMore, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/More"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + guard let strongSelf = self else { + return + } + strongSelf.chatInterfaceInteraction.toggleMessagesSelection([message.id], true) + strongSelf.expandTabs() + f(.default) + }))) + + return items + } + + switch previewData { + case let .gallery(gallery): + gallery.setHintWillBePresentedInPreviewingContext(true) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node)), items: items, reactionItems: [], gesture: gesture) + strongSelf.controller?.presentInGlobalOverlay(contextController) + case .instantPage: + break + } + } + }) + }, navigateToMessage: { fromId, id in + }, tapMessage: nil, clickThroughMessage: { + }, toggleMessagesSelection: { [weak self] ids, value in + guard let strongSelf = self else { + return + } + if var selectedMessageIds = strongSelf.state.selectedMessageIds { + for id in ids { + if value { + selectedMessageIds.insert(id) + } else { + selectedMessageIds.remove(id) + } + } + strongSelf.state = strongSelf.state.withSelectedMessageIds(selectedMessageIds) + } else { + strongSelf.state = strongSelf.state.withSelectedMessageIds(value ? Set(ids) : Set()) + } + strongSelf.chatInterfaceInteraction.selectionState = strongSelf.state.selectedMessageIds.flatMap { ChatInterfaceSelectionState(selectedIds: $0) } + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring), additive: false) + } + strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true) + }, sendCurrentMessage: { _ in + }, sendMessage: { _ in + }, sendSticker: { _, _, _, _ in + return false + }, sendGif: { _, _, _ in + return false + }, requestMessageActionCallback: { _, _, _ in + }, requestMessageActionUrlAuth: { _, _, _ in + }, activateSwitchInline: { _, _ in + }, openUrl: { [weak self] url, _, external, _ in + guard let strongSelf = self else { + return + } + strongSelf.openUrl(url: url, external: external ?? false) + }, shareCurrentLocation: { + }, shareAccountContact: { + }, sendBotCommand: { _, _ in + }, openInstantPage: { [weak self] message, associatedData in + guard let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController else { + return + } + var foundGalleryMessage: Message? + if let searchContentNode = strongSelf.searchDisplayController?.contentNode as? ChatHistorySearchContainerNode { + if let galleryMessage = searchContentNode.messageForGallery(message.id) { + let _ = (strongSelf.context.account.postbox.transaction { transaction -> Void in + if transaction.getMessage(galleryMessage.id) == nil { + storeMessageFromSearch(transaction: transaction, message: galleryMessage) + } + }).start() + foundGalleryMessage = galleryMessage + } + } + if foundGalleryMessage == nil, let galleryMessage = strongSelf.paneContainerNode.findLoadedMessage(id: message.id) { + foundGalleryMessage = galleryMessage + } + + if let foundGalleryMessage = foundGalleryMessage { + openChatInstantPage(context: strongSelf.context, message: foundGalleryMessage, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController) + } + }, openWallpaper: { _ in + }, openTheme: { _ in + }, openHashtag: { _, _ in + }, updateInputState: { _ in + }, updateInputMode: { _ in + }, openMessageShareMenu: { _ in + }, presentController: { _, _ in + }, navigationController: { + return nil + }, chatControllerNode: { + return nil + }, reactionContainerNode: { + return nil + }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in + }, longTap: { [weak self] content, _ in + guard let strongSelf = self else { + return + } + strongSelf.view.endEditing(true) + switch content { + case let .url(url): + let canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1 + let openText = canOpenIn ? strongSelf.presentationData.strings.Conversation_FileOpenIn : strongSelf.presentationData.strings.Conversation_LinkDialogOpen + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: url), + ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + if canOpenIn { + let actionSheet = OpenInActionSheetController(context: strongSelf.context, item: .url(url: url), openUrl: { [weak self] url in + if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { + strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.presentationData, navigationController: navigationController, dismissInput: { + }) + } + }) + strongSelf.view.endEditing(true) + strongSelf.controller?.present(actionSheet, in: .window(.root)) + } else { + strongSelf.context.sharedContext.applicationBindings.openUrl(url) + } + } + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = url + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let link = URL(string: url) { + let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) + } + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.view.endEditing(true) + strongSelf.controller?.present(actionSheet, in: .window(.root)) + default: + break + } + }, openCheckoutOrReceipt: { _ in + }, openSearch: { + }, setupReply: { _ in + }, canSetupReply: { _ in + return false + }, navigateToFirstDateMessage: { _ in + }, requestRedeliveryOfFailedMessages: { _ in + }, addContact: { _ in + }, rateCall: { _, _ in + }, requestSelectMessagePollOptions: { _, _ in + }, requestOpenMessagePollResults: { _, _ in + }, openAppStorePage: { + }, displayMessageTooltip: { _, _, _, _ in + }, seekToTimecode: { _, _, _ in + }, scheduleCurrentMessage: { + }, sendScheduledMessagesNow: { _ in + }, editScheduledMessagesTime: { _ in + }, performTextSelectionAction: { _, _, _ in + }, updateMessageReaction: { _, _ in + }, openMessageReactions: { _ in + }, displaySwipeToReplyHint: { + }, dismissReplyMarkupMessage: { _ in + }, openMessagePollResults: { _, _ in + }, openPollCreation: { _ in + }, requestMessageUpdate: { _ in + }, cancelInteractiveKeyboardGestures: { + }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, + pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false)) + self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in + guard let strongSelf = self else { + return + } + var hiddenMedia: [MessageId: [Media]] = [:] + for id in ids { + if case let .chat(accountId, messageId, media) = id, accountId == strongSelf.context.account.id { + hiddenMedia[messageId] = [media] + } + } + strongSelf.chatInterfaceInteraction.hiddenMedia = hiddenMedia + strongSelf.paneContainerNode.updateHiddenMedia() + }) + + self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor + + self.scrollNode.view.showsVerticalScrollIndicator = false + if #available(iOS 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + self.scrollNode.view.alwaysBounceVertical = true + self.scrollNode.view.scrollsToTop = false + self.scrollNode.view.delegate = self + self.addSubnode(self.scrollNode) + self.scrollNode.addSubnode(self.paneContainerNode) + self.addSubnode(self.headerNode) + self.scrollNode.view.isScrollEnabled = !self.isMediaOnly + + self.paneContainerNode.chatControllerInteraction = self.chatInterfaceInteraction + self.paneContainerNode.openPeerContextAction = { [weak self] peer, node, gesture in + guard let strongSelf = self, let controller = strongSelf.controller else { + return + } + let presentationData = strongSelf.presentationData + let chatController = strongSelf.context.sharedContext.makeChatController(context: context, chatLocation: .peer(peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true)) + chatController.canReadHistory.set(false) + let items: [ContextMenuItem] = [ + .action(ContextMenuActionItem(text: presentationData.strings.Conversation_LinkDialogOpen, icon: { _ in nil }, action: { _, f in + f(.dismissWithoutContent) + self?.chatInterfaceInteraction.openPeer(peer.id, .default, nil) + })) + ] + let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) + controller.presentInGlobalOverlay(contextController) + } + + self.paneContainerNode.currentPaneUpdated = { [weak self] expand in + guard let strongSelf = self else { + return + } + if let (layout, navigationHeight) = strongSelf.validLayout { + if strongSelf.headerNode.isAvatarExpanded { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) + + strongSelf.headerNode.updateIsAvatarExpanded(false, transition: transition) + strongSelf.updateNavigationExpansionPresentation(isExpanded: false, animated: true) + + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition, additive: true) + } + } + + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + if expand { + strongSelf.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: strongSelf.paneContainerNode.frame.minY - navigationHeight), animated: true) + } + } + } + + self.paneContainerNode.requestExpandTabs = { [weak self] in + guard let strongSelf = self, let (_, navigationHeight) = strongSelf.validLayout else { + return false + } + + if strongSelf.headerNode.isAvatarExpanded { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) + + strongSelf.headerNode.updateIsAvatarExpanded(false, transition: transition) + strongSelf.updateNavigationExpansionPresentation(isExpanded: false, animated: true) + + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition, additive: true) + } + } + + let contentOffset = strongSelf.scrollNode.view.contentOffset + let paneAreaExpansionFinalPoint: CGFloat = strongSelf.paneContainerNode.frame.minY - navigationHeight + if contentOffset.y < paneAreaExpansionFinalPoint - CGFloat.ulpOfOne { + strongSelf.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: paneAreaExpansionFinalPoint), animated: true) + return true + } else { + return false + } + } + + self.paneContainerNode.requestPerformPeerMemberAction = { [weak self] member, action in + guard let strongSelf = self else { + return + } + switch action { + case .open: + strongSelf.openPeerInfo(peer: member.peer) + case .promote: + strongSelf.performMemberAction(member: member, action: .promote) + case .restrict: + strongSelf.performMemberAction(member: member, action: .restrict) + case .remove: + strongSelf.performMemberAction(member: member, action: .remove) + } + } + + self.headerNode.performButtonAction = { [weak self] key in + self?.performButtonAction(key: key) + } + + self.headerNode.requestAvatarExpansion = { [weak self] entries, centralEntry, _ in + guard let strongSelf = self, let peer = strongSelf.data?.peer, peer.smallProfileImage != nil else { + return + } + if strongSelf.hapticFeedback == nil { + strongSelf.hapticFeedback = HapticFeedback() + } + strongSelf.hapticFeedback?.tap() + + let entriesPromise = Promise<[AvatarGalleryEntry]>(entries) + let galleryController = AvatarGalleryController(context: strongSelf.context, peer: peer, sourceHasRoundCorners: !strongSelf.headerNode.isAvatarExpanded, remoteEntries: entriesPromise, centralEntryIndex: centralEntry.flatMap { entries.index(of: $0) }, replaceRootController: { controller, ready in + }) + strongSelf.hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).start(next: { entry in + self?.headerNode.updateAvatarIsHidden(entry: entry) + })) + strongSelf.view.endEditing(true) + strongSelf.controller?.present(galleryController, in: .window(.root), with: AvatarGalleryControllerPresentationArguments(transitionArguments: { entry in + if let transitionNode = self?.headerNode.avatarTransitionArguments(entry: entry) { + return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { view in + self?.headerNode.addToAvatarTransitionSurface(view: view) + }) + } else { + return nil + } + })) + } + + self.headerNode.requestOpenAvatarForEditing = { [weak self] in + self?.openAvatarForEditing() + } + + self.headerNode.requestUpdateLayout = { [weak self] in + guard let strongSelf = self else { + return + } + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + } + + self.headerNode.navigationButtonContainer.performAction = { [weak self] key in + guard let strongSelf = self else { + return + } + switch key { + case .edit: + strongSelf.state = strongSelf.state.withIsEditing(true) + if strongSelf.headerNode.isAvatarExpanded { + strongSelf.headerNode.updateIsAvatarExpanded(false, transition: .immediate) + strongSelf.updateNavigationExpansionPresentation(isExpanded: false, animated: true) + } + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.scrollNode.view.setContentOffset(CGPoint(), animated: false) + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + UIView.transition(with: strongSelf.view, duration: 0.3, options: [.transitionCrossDissolve], animations: { + }, completion: nil) + strongSelf.controller?.navigationItem.setLeftBarButton(UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, style: .plain, target: strongSelf, action: #selector(strongSelf.editingCancelPressed)), animated: true) + case .done, .cancel: + if case .done = key { + guard let data = strongSelf.data else { + strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) + return + } + if let peer = data.peer as? TelegramUser { + if data.isContact { + let firstName = strongSelf.headerNode.editingContentNode.editingTextForKey(.firstName) ?? "" + let lastName = strongSelf.headerNode.editingContentNode.editingTextForKey(.lastName) ?? "" + + if peer.firstName != firstName || peer.lastName != lastName { + if firstName.isEmpty && lastName.isEmpty { + if strongSelf.hapticFeedback == nil { + strongSelf.hapticFeedback = HapticFeedback() + } + strongSelf.hapticFeedback?.error() + strongSelf.headerNode.editingContentNode.shakeTextForKey(.firstName) + } else { + var dismissStatus: (() -> Void)? + let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: { + dismissStatus?() + })) + dismissStatus = { [weak statusController] in + self?.activeActionDisposable.set(nil) + statusController?.dismiss() + } + strongSelf.controller?.present(statusController, in: .window(.root)) + strongSelf.activeActionDisposable.set((updateContactName(account: context.account, peerId: peer.id, firstName: firstName, lastName: lastName) + |> deliverOnMainQueue).start(error: { _ in + dismissStatus?() + + guard let strongSelf = self else { + return + } + strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) + }, completed: { + dismissStatus?() + + guard let strongSelf = self else { + return + } + let context = strongSelf.context + let _ = (getUserPeer(postbox: strongSelf.context.account.postbox, peerId: peer.id) + |> mapToSignal { peer, _ -> Signal in + guard let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty else { + return .complete() + } + return (context.sharedContext.contactDataManager?.basicDataForNormalizedPhoneNumber(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) ?? .single([])) + |> take(1) + |> mapToSignal { records -> Signal in + var signals: [Signal] = [] + if let contactDataManager = context.sharedContext.contactDataManager { + for (id, basicData) in records { + signals.append(contactDataManager.appendContactData(DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: firstName, lastName: lastName, phoneNumbers: basicData.phoneNumbers), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: ""), to: id)) + } + } + return combineLatest(signals) + |> mapToSignal { _ -> Signal in + return .complete() + } + } + }).start() + strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) + })) + } + } else { + strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) + } + } else { + strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) + } + } else if let group = data.peer as? TelegramGroup, canEditPeerInfo(peer: group) { + let title = strongSelf.headerNode.editingContentNode.editingTextForKey(.title) ?? "" + let description = strongSelf.headerNode.editingContentNode.editingTextForKey(.description) ?? "" + + if title.isEmpty { + if strongSelf.hapticFeedback == nil { + strongSelf.hapticFeedback = HapticFeedback() + } + strongSelf.hapticFeedback?.error() + + strongSelf.headerNode.editingContentNode.shakeTextForKey(.title) + } else { + var updateDataSignals: [Signal] = [] + + if title != group.title { + updateDataSignals.append( + updatePeerTitle(account: strongSelf.context.account, peerId: group.id, title: title) + |> ignoreValues + |> mapError { _ in return Void() } + ) + } + if description != (data.cachedData as? CachedGroupData)?.about { + updateDataSignals.append( + updatePeerDescription(account: strongSelf.context.account, peerId: group.id, description: description.isEmpty ? nil : description) + |> ignoreValues + |> mapError { _ in return Void() } + ) + } + var dismissStatus: (() -> Void)? + let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: { + dismissStatus?() + })) + dismissStatus = { [weak statusController] in + self?.activeActionDisposable.set(nil) + statusController?.dismiss() + } + strongSelf.controller?.present(statusController, in: .window(.root)) + + strongSelf.activeActionDisposable.set((combineLatest(updateDataSignals) + |> deliverOnMainQueue).start(error: { _ in + dismissStatus?() + + guard let strongSelf = self else { + return + } + strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) + }, completed: { + dismissStatus?() + + guard let strongSelf = self else { + return + } + strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) + })) + } + } else if let channel = data.peer as? TelegramChannel, canEditPeerInfo(peer: channel) { + let title = strongSelf.headerNode.editingContentNode.editingTextForKey(.title) ?? "" + let description = strongSelf.headerNode.editingContentNode.editingTextForKey(.description) ?? "" + + if title.isEmpty { + strongSelf.headerNode.editingContentNode.shakeTextForKey(.title) + } else { + var updateDataSignals: [Signal] = [] + + if title != channel.title { + updateDataSignals.append( + updatePeerTitle(account: strongSelf.context.account, peerId: channel.id, title: title) + |> ignoreValues + |> mapError { _ in return Void() } + ) + } + if description != (data.cachedData as? CachedChannelData)?.about { + updateDataSignals.append( + updatePeerDescription(account: strongSelf.context.account, peerId: channel.id, description: description.isEmpty ? nil : description) + |> ignoreValues + |> mapError { _ in return Void() } + ) + } + + var dismissStatus: (() -> Void)? + let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: { + dismissStatus?() + })) + dismissStatus = { [weak statusController] in + self?.activeActionDisposable.set(nil) + statusController?.dismiss() + } + strongSelf.controller?.present(statusController, in: .window(.root)) + + strongSelf.activeActionDisposable.set((combineLatest(updateDataSignals) + |> deliverOnMainQueue).start(error: { _ in + dismissStatus?() + + guard let strongSelf = self else { + return + } + strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) + }, completed: { + dismissStatus?() + + guard let strongSelf = self else { + return + } + strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) + })) + } + } else { + strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel) + } + } else { + strongSelf.state = strongSelf.state.withIsEditing(false) + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.scrollNode.view.setContentOffset(CGPoint(), animated: false) + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + UIView.transition(with: strongSelf.view, duration: 0.3, options: [.transitionCrossDissolve], animations: { + }, completion: nil) + strongSelf.controller?.navigationItem.setLeftBarButton(nil, animated: true) + } + case .select: + strongSelf.state = strongSelf.state.withSelectedMessageIds(Set()) + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring), additive: false) + } + strongSelf.chatInterfaceInteraction.selectionState = strongSelf.state.selectedMessageIds.flatMap { ChatInterfaceSelectionState(selectedIds: $0) } + strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true) + case .selectionDone: + strongSelf.state = strongSelf.state.withSelectedMessageIds(nil) + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring), additive: false) + } + strongSelf.chatInterfaceInteraction.selectionState = strongSelf.state.selectedMessageIds.flatMap { ChatInterfaceSelectionState(selectedIds: $0) } + strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true) + case .search: + strongSelf.activateSearch() + } + } + + self.dataDisposable = (peerInfoScreenData(context: context, peerId: peerId, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat) + |> deliverOnMainQueue).start(next: { [weak self] data in + guard let strongSelf = self else { + return + } + strongSelf.updateData(data) + }) + } + + deinit { + self.dataDisposable?.dispose() + self.hiddenMediaDisposable?.dispose() + self.activeActionDisposable.dispose() + self.resolveUrlDisposable.dispose() + self.hiddenAvatarRepresentationDisposable.dispose() + self.toggleShouldChannelMessagesSignaturesDisposable.dispose() + self.updateAvatarDisposable.dispose() + self.selectAddMemberDisposable.dispose() + self.addMemberDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + } + + private func updateData(_ data: PeerInfoScreenData) { + let previousData = self.data + var previousMemberCount: Int? + if let data = self.data { + if let members = data.members, case let .shortList(_, memberList) = members { + previousMemberCount = memberList.count + } + } + self.data = data + if previousData?.members?.membersContext !== data.members?.membersContext { + if let peer = data.peer, let _ = data.members { + self.groupMembersSearchContext = GroupMembersSearchContext(context: self.context, peerId: peer.id) + } else { + self.groupMembersSearchContext = nil + } + } + if let (layout, navigationHeight) = self.validLayout { + var updatedMemberCount: Int? + if let data = self.data { + if let members = data.members, case let .shortList(_, memberList) = members { + updatedMemberCount = memberList.count + } + } + + var membersUpdated = false + if let previousMemberCount = previousMemberCount, let updatedMemberCount = updatedMemberCount, previousMemberCount > updatedMemberCount { + membersUpdated = true + } + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: self.didSetReady && membersUpdated ? .animated(duration: 0.3, curve: .spring) : .immediate) + } + } + + func scrollToTop() { + if !self.paneContainerNode.scrollToTop() { + self.scrollNode.view.setContentOffset(CGPoint(), animated: true) + } + } + + private func expandTabs() { + if self.headerNode.isAvatarExpanded { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) + + self.headerNode.updateIsAvatarExpanded(false, transition: transition) + self.updateNavigationExpansionPresentation(isExpanded: false, animated: true) + + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition, additive: true) + } + } + + if let (layout, navigationHeight) = self.validLayout { + let contentOffset = self.scrollNode.view.contentOffset + let paneAreaExpansionFinalPoint: CGFloat = self.paneContainerNode.frame.minY - navigationHeight + if contentOffset.y < paneAreaExpansionFinalPoint - CGFloat.ulpOfOne { + self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: paneAreaExpansionFinalPoint), animated: true) + } + } + } + + @objc private func editingCancelPressed() { + self.headerNode.navigationButtonContainer.performAction?(.cancel) + } + + private func openMessage(id: MessageId) -> Bool { + guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { + return false + } + var foundGalleryMessage: Message? + if let searchContentNode = self.searchDisplayController?.contentNode as? ChatHistorySearchContainerNode { + if let galleryMessage = searchContentNode.messageForGallery(id) { + let _ = (self.context.account.postbox.transaction { transaction -> Void in + if transaction.getMessage(galleryMessage.id) == nil { + storeMessageFromSearch(transaction: transaction, message: galleryMessage) + } + }).start() + foundGalleryMessage = galleryMessage + } + } + if foundGalleryMessage == nil, let galleryMessage = self.paneContainerNode.findLoadedMessage(id: id) { + foundGalleryMessage = galleryMessage + } + + guard let galleryMessage = foundGalleryMessage else { + return false + } + self.view.endEditing(true) + + return self.context.sharedContext.openChatMessage(OpenChatMessageParams(context: self.context, message: galleryMessage, standalone: false, reverseMessageGalleryOrder: true, navigationController: navigationController, dismissInput: { [weak self] in + self?.view.endEditing(true) + }, present: { [weak self] c, a in + self?.controller?.present(c, in: .window(.root), with: a, blockInteraction: true) + }, transitionNode: { [weak self] messageId, media in + guard let strongSelf = self else { + return nil + } + return strongSelf.paneContainerNode.transitionNodeForGallery(messageId: messageId, media: media) + }, addToTransitionSurface: { [weak self] view in + guard let strongSelf = self else { + return + } + strongSelf.paneContainerNode.currentPane?.node.addToTransitionSurface(view: view) + }, openUrl: { [weak self] url in + self?.openUrl(url: url, external: false) + }, openPeer: { [weak self] peer, navigation in + self?.openPeer(peerId: peer.id, navigation: navigation) + }, callPeer: { peerId in + //self?.controllerInteraction?.callPeer(peerId) + }, enqueueMessage: { _ in + }, sendSticker: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in })) + } + + private func openUrl(url: String, external: Bool) { + let disposable = self.resolveUrlDisposable + + let resolvedUrl: Signal + if external { + resolvedUrl = .single(.externalUrl(url)) + } else { + resolvedUrl = self.context.sharedContext.resolveUrl(account: self.context.account, url: url) + } + + disposable.set((resolvedUrl + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, openPeer: { peerId, navigation in + self?.openPeer(peerId: peerId, navigation: navigation) + }, sendFile: nil, + sendSticker: nil, + present: { c, a in + self?.controller?.present(c, in: .window(.root), with: a) + }, dismissInput: { + self?.view.endEditing(true) + }, contentContext: nil) + })) + } + + private func openPeer(peerId: PeerId, navigation: ChatControllerInteractionNavigateToPeer) { + switch navigation { + case .default: + if let navigationController = self.controller?.navigationController as? NavigationController { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peerId), keepStack: .always)) + } + case let .chat(_, subject): + if let navigationController = self.controller?.navigationController as? NavigationController { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peerId), subject: subject, keepStack: .always)) + } + case .info: + self.resolveUrlDisposable.set((self.context.account.postbox.loadedPeerWithId(peerId) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] peer in + if let strongSelf = self, peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { + (strongSelf.controller?.navigationController as? NavigationController)?.pushViewController(infoController) + } + } + })) + case let .withBotStartPayload(startPayload): + if let navigationController = self.controller?.navigationController as? NavigationController { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peerId), botStart: startPayload)) + } + default: + break + } + } + + private func performButtonAction(key: PeerInfoHeaderButtonKey) { + guard let controller = self.controller else { + return + } + switch key { + case .message: + if let navigationController = controller.navigationController as? NavigationController { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.peerId))) + } + case .discussion: + if let cachedData = self.data?.cachedData as? CachedChannelData, let linkedDiscussionPeerId = cachedData.linkedDiscussionPeerId { + if let navigationController = controller.navigationController as? NavigationController { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(linkedDiscussionPeerId))) + } + } + case .call: + self.requestCall() + case .mute: + let muteInterval: Int32? + if let notificationSettings = self.data?.notificationSettings, case .muted = notificationSettings.muteState { + muteInterval = nil + } else { + muteInterval = Int32.max + } + let _ = updatePeerMuteSetting(account: self.context.account, peerId: self.peerId, muteInterval: muteInterval).start() + case .more: + guard let data = self.data, let peer = data.peer else { + return + } + let actionSheet = ActionSheetController(presentationData: self.presentationData) + let dismissAction: () -> Void = { [weak actionSheet] in + actionSheet?.dismissAnimated() + } + var items: [ActionSheetItem] = [] + if !peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: self.isOpenedFromChat).contains(.search) || self.headerNode.isAvatarExpanded { + items.append(ActionSheetButtonItem(title: presentationData.strings.ChatSearch_SearchPlaceholder, color: .accent, action: { [weak self] in + dismissAction() + self?.openChatWithMessageSearch() + })) + } + if let user = peer as? TelegramUser { + if let botInfo = user.botInfo { + if botInfo.flags.contains(.worksWithGroups) { + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_InviteBotToGroup, color: .accent, action: { [weak self] in + dismissAction() + self?.openAddBotToGroup() + })) + } + if user.username != nil { + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_ShareBot, color: .accent, action: { [weak self] in + dismissAction() + self?.openShareBot() + })) + } + + if let cachedData = data.cachedData as? CachedUserData, let botInfo = cachedData.botInfo { + for command in botInfo.commands { + if command.text == "settings" { + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_BotSettings, color: .accent, action: { [weak self] in + dismissAction() + self?.performBotCommand(command: .settings) + })) + } else if command.text == "help" { + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_BotHelp, color: .accent, action: { [weak self] in + dismissAction() + self?.performBotCommand(command: .help) + })) + } else if command.text == "privacy" { + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_BotPrivacy, color: .accent, action: { [weak self] in + dismissAction() + self?.performBotCommand(command: .privacy) + })) + } + } + } + } + + if user.botInfo == nil && data.isContact { + items.append(ActionSheetButtonItem(title: presentationData.strings.Profile_ShareContactButton, color: .accent, action: { [weak self] in + dismissAction() + guard let strongSelf = self else { + return + } + if let peer = strongSelf.data?.peer as? TelegramUser, let phone = peer.phone { + let contact = TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil) + let shareController = ShareController(context: strongSelf.context, subject: .media(.standalone(media: contact))) + strongSelf.controller?.present(shareController, in: .window(.root)) + } + })) + } + + if self.peerId.namespace == Namespaces.Peer.CloudUser && user.botInfo == nil && !user.flags.contains(.isSupport) { + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_StartSecretChat, color: .accent, action: { [weak self] in + dismissAction() + self?.openStartSecretChat() + })) + if data.isContact { + items.append(ActionSheetButtonItem(title: presentationData.strings.Conversation_BlockUser, color: .destructive, action: { [weak self] in + dismissAction() + self?.updateBlocked(block: true) + })) + } + } + } else if let channel = peer as? TelegramChannel { + var canReport = true + if channel.isVerified { + canReport = false + } + if channel.adminRights != nil { + canReport = false + } + if channel.flags.contains(.isCreator) { + canReport = false + } + if canReport { + items.append(ActionSheetButtonItem(title: presentationData.strings.ReportPeer_Report, color: .destructive, action: { [weak self] in + dismissAction() + self?.openReport(user: false) + })) + } + + switch channel.info { + case .broadcast: + if channel.flags.contains(.isCreator) { + items.append(ActionSheetButtonItem(title: presentationData.strings.ChannelInfo_DeleteChannel, color: .destructive, action: { [weak self] in + dismissAction() + self?.openDeletePeer() + })) + } else { + if !peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: self.isOpenedFromChat).contains(.leave) { + if case .member = channel.participationStatus { + items.append(ActionSheetButtonItem(title: presentationData.strings.Channel_LeaveChannel, color: .destructive, action: { [weak self] in + dismissAction() + self?.openLeavePeer() + })) + } + } + } + case .group: + if channel.flags.contains(.isCreator) { + items.append(ActionSheetButtonItem(title: presentationData.strings.ChannelInfo_DeleteGroup, color: .destructive, action: { [weak self] in + dismissAction() + self?.openDeletePeer() + })) + } else { + if case .member = channel.participationStatus { + items.append(ActionSheetButtonItem(title: presentationData.strings.Group_LeaveGroup, color: .destructive, action: { [weak self] in + dismissAction() + self?.openLeavePeer() + })) + } + } + } + } else if let group = peer as? TelegramGroup { + if case .Member = group.membership { + items.append(ActionSheetButtonItem(title: presentationData.strings.Group_LeaveGroup, color: .destructive, action: { [weak self] in + dismissAction() + self?.openLeavePeer() + })) + } + } + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + self.view.endEditing(true) + controller.present(actionSheet, in: .window(.root)) + case .addMember: + self.openAddMember() + case .search: + self.openChatWithMessageSearch() + case .leave: + self.openLeavePeer() + } + } + + private func openChatWithMessageSearch() { + if let navigationController = (self.controller?.navigationController as? NavigationController) { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.peerId), activateMessageSearch: true)) + } + } + + private func openStartSecretChat() { + let peerId = self.peerId + let _ = (self.context.account.postbox.transaction { transaction -> (Peer?, PeerId?) in + let peer = transaction.getPeer(peerId) + let filteredPeerIds = Array(transaction.getAssociatedPeerIds(peerId)).filter { $0.namespace == Namespaces.Peer.SecretChat } + var activeIndices: [ChatListIndex] = [] + for associatedId in filteredPeerIds { + if let state = (transaction.getPeer(associatedId) as? TelegramSecretChat)?.embeddedState { + switch state { + case .active, .handshake: + if let (_, index) = transaction.getPeerChatListIndex(associatedId) { + activeIndices.append(index) + } + default: + break + } + } + } + activeIndices.sort() + if let index = activeIndices.last { + return (peer, index.messageIndex.id.peerId) + } else { + return (peer, nil) + } + } + |> deliverOnMainQueue).start(next: { [weak self] peer, currentPeerId in + guard let strongSelf = self else { + return + } + if let currentPeerId = currentPeerId { + if let navigationController = (strongSelf.controller?.navigationController as? NavigationController) { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(currentPeerId))) + } + } else if let controller = strongSelf.controller { + let displayTitle = peer?.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder) ?? "" + controller.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.UserInfo_StartSecretChatConfirmation(displayTitle).0, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.UserInfo_StartSecretChatStart, action: { + guard let strongSelf = self else { + return + } + var createSignal = createSecretChat(account: strongSelf.context.account, peerId: peerId) + var cancelImpl: (() -> Void)? + let progressSignal = Signal { subscriber in + if let strongSelf = self { + let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + strongSelf.controller?.present(statusController, in: .window(.root)) + return ActionDisposable { [weak statusController] in + Queue.mainQueue().async() { + statusController?.dismiss() + } + } + } else { + return EmptyDisposable + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + createSignal = createSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + let createSecretChatDisposable = MetaDisposable() + cancelImpl = { + createSecretChatDisposable.set(nil) + } + + createSecretChatDisposable.set((createSignal + |> deliverOnMainQueue).start(next: { peerId in + guard let strongSelf = self else { + return + } + if let navigationController = (strongSelf.controller?.navigationController as? NavigationController) { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId))) + } + }, error: { _ in + guard let strongSelf = self else { + return + } + strongSelf.controller?.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + })) + })]), in: .window(.root)) + } + }) + } + + private func openUsername(value: String) { + let shareController = ShareController(context: self.context, subject: .url("https://t.me/\(value)")) + self.view.endEditing(true) + self.controller?.present(shareController, in: .window(.root)) + } + + private func requestCall() { + guard let peer = self.data?.peer as? TelegramUser, let cachedUserData = self.data?.cachedData as? CachedUserData else { + return + } + if cachedUserData.callsPrivate { + self.controller?.present(textAlertController(context: self.context, title: self.presentationData.strings.Call_ConnectionErrorTitle, text: self.presentationData.strings.Call_PrivacyErrorMessage(peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + return + } + + let callResult = self.context.sharedContext.callManager?.requestCall(account: self.context.account, peerId: peer.id, endCurrentIfAny: false) + if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { + if currentPeerId == peer.id { + self.context.sharedContext.navigateToCurrentCall() + } else { + let _ = (self.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in + return (transaction.getPeer(peer.id), transaction.getPeer(currentPeerId)) + } + |> deliverOnMainQueue).start(next: { [weak self] peer, current in + guard let strongSelf = self else { + return + } + if let peer = peer, let current = current { + strongSelf.controller?.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.Call_CallInProgressTitle, text: strongSelf.presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: { + guard let strongSelf = self else { + return + } + let _ = strongSelf.context.sharedContext.callManager?.requestCall(account: strongSelf.context.account, peerId: peer.id, endCurrentIfAny: true) + })]), in: .window(.root)) + } + }) + } + } + } + + private func openPhone(value: String) { + let _ = (getUserPeer(postbox: self.context.account.postbox, peerId: peerId) + |> deliverOnMainQueue).start(next: { [weak self] peer, _ in + guard let strongSelf = self else { + return + } + if let peer = peer as? TelegramUser, let peerPhoneNumber = peer.phone, formatPhoneNumber(value) == formatPhoneNumber(peerPhoneNumber) { + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) + let dismissAction: () -> Void = { [weak actionSheet] in + actionSheet?.dismissAnimated() + } + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.UserInfo_TelegramCall, action: { + dismissAction() + self?.requestCall() + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.UserInfo_PhoneCall, action: { + dismissAction() + + guard let strongSelf = self else { + return + } + strongSelf.context.sharedContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(value).replacingOccurrences(of: " ", with: ""))") + }), + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + strongSelf.view.endEditing(true) + strongSelf.controller?.present(actionSheet, in: .window(.root)) + } else { + strongSelf.context.sharedContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(value).replacingOccurrences(of: " ", with: ""))") + } + }) + } + + private func editingOpenNotificationSettings() { + let peerId = self.peerId + let _ = (self.context.account.postbox.transaction { transaction -> (TelegramPeerNotificationSettings, GlobalNotificationSettings) in + let peerSettings: TelegramPeerNotificationSettings = (transaction.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings) ?? TelegramPeerNotificationSettings.defaultSettings + let globalSettings: GlobalNotificationSettings = (transaction.getPreferencesEntry(key: PreferencesKeys.globalNotifications) as? GlobalNotificationSettings) ?? GlobalNotificationSettings.defaultSettings + return (peerSettings, globalSettings) + } + |> deliverOnMainQueue).start(next: { [weak self] peerSettings, globalSettings in + guard let strongSelf = self else { + return + } + let soundSettings: NotificationSoundSettings? + if case .default = peerSettings.messageSound { + soundSettings = NotificationSoundSettings(value: nil) + } else { + soundSettings = NotificationSoundSettings(value: peerSettings.messageSound) + } + let muteSettingsController = notificationMuteSettingsController(presentationData: strongSelf.presentationData, notificationSettings: globalSettings.effective.groupChats, soundSettings: nil, openSoundSettings: { + guard let strongSelf = self else { + return + } + let soundController = notificationSoundSelectionController(context: strongSelf.context, isModal: true, currentSound: peerSettings.messageSound, defaultSound: globalSettings.effective.groupChats.sound, completion: { sound in + guard let strongSelf = self else { + return + } + let _ = updatePeerNotificationSoundInteractive(account: strongSelf.context.account, peerId: strongSelf.peerId, sound: sound).start() + }) + soundController.navigationPresentation = .modal + strongSelf.controller?.push(soundController) + }, updateSettings: { value in + guard let strongSelf = self else { + return + } + let _ = updatePeerMuteSetting(account: strongSelf.context.account, peerId: strongSelf.peerId, muteInterval: value).start() + }) + strongSelf.view.endEditing(true) + strongSelf.controller?.present(muteSettingsController, in: .window(.root)) + }) + } + + private func editingOpenSoundSettings() { + let peerId = self.peerId + let _ = (self.context.account.postbox.transaction { transaction -> (TelegramPeerNotificationSettings, GlobalNotificationSettings) in + let peerSettings: TelegramPeerNotificationSettings = (transaction.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings) ?? TelegramPeerNotificationSettings.defaultSettings + let globalSettings: GlobalNotificationSettings = (transaction.getPreferencesEntry(key: PreferencesKeys.globalNotifications) as? GlobalNotificationSettings) ?? GlobalNotificationSettings.defaultSettings + return (peerSettings, globalSettings) + } + |> deliverOnMainQueue).start(next: { [weak self] peerSettings, globalSettings in + guard let strongSelf = self else { + return + } + let soundSettings: NotificationSoundSettings? + if case .default = peerSettings.messageSound { + soundSettings = NotificationSoundSettings(value: nil) + } else { + soundSettings = NotificationSoundSettings(value: peerSettings.messageSound) + } + + let soundController = notificationSoundSelectionController(context: strongSelf.context, isModal: true, currentSound: peerSettings.messageSound, defaultSound: globalSettings.effective.groupChats.sound, completion: { sound in + guard let strongSelf = self else { + return + } + let _ = updatePeerNotificationSoundInteractive(account: strongSelf.context.account, peerId: strongSelf.peerId, sound: sound).start() + }) + strongSelf.controller?.push(soundController) + }) + } + + private func editingToggleShowMessageText(value: Bool) { + let _ = (getUserPeer(postbox: self.context.account.postbox, peerId: self.peerId) + |> deliverOnMainQueue).start(next: { [weak self] peer, _ in + guard let strongSelf = self, let peer = peer else { + return + } + let _ = updatePeerDisplayPreviewsSetting(account: strongSelf.context.account, peerId: peer.id, displayPreviews: value ? .show : .hide).start() + }) + } + + private func requestDeleteContact() { + let actionSheet = ActionSheetController(presentationData: self.presentationData) + let dismissAction: () -> Void = { [weak actionSheet] in + actionSheet?.dismissAnimated() + } + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.UserInfo_DeleteContact, color: .destructive, action: { [weak self] in + dismissAction() + guard let strongSelf = self else { + return + } + let _ = (getUserPeer(postbox: strongSelf.context.account.postbox, peerId: strongSelf.peerId) + |> deliverOnMainQueue).start(next: { peer, _ in + guard let peer = peer, let strongSelf = self else { + return + } + let deleteContactFromDevice: Signal + if let contactDataManager = strongSelf.context.sharedContext.contactDataManager { + deleteContactFromDevice = contactDataManager.deleteContactWithAppSpecificReference(peerId: peer.id) + } else { + deleteContactFromDevice = .complete() + } + + var deleteSignal = deleteContactPeerInteractively(account: strongSelf.context.account, peerId: peer.id) + |> then(deleteContactFromDevice) + + let progressSignal = Signal { subscriber in + guard let strongSelf = self else { + return EmptyDisposable + } + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + strongSelf.controller?.present(statusController, in: .window(.root)) + return ActionDisposable { [weak statusController] in + Queue.mainQueue().async() { + statusController?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + deleteSignal = deleteSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + + strongSelf.activeActionDisposable.set((deleteSignal + |> deliverOnMainQueue).start(completed: { + self?.controller?.dismiss() + })) + + deleteSendMessageIntents(peerId: strongSelf.peerId) + }) + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + self.view.endEditing(true) + self.controller?.present(actionSheet, in: .window(.root)) + } + + private func openChat() { + if let navigationController = self.controller?.navigationController as? NavigationController { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.peerId))) + } + } + + private func openAddContact() { + let _ = (getUserPeer(postbox: self.context.account.postbox, peerId: self.peerId) + |> deliverOnMainQueue).start(next: { [weak self] peer, _ in + guard let strongSelf = self, let peer = peer else { + return + } + openAddPersonContactImpl(context: strongSelf.context, peerId: peer.id, pushController: { c in + self?.controller?.push(c) + }, present: { c, a in + self?.controller?.present(c, in: .window(.root), with: a) + }) + }) + } + + private func updateBlocked(block: Bool) { + let _ = (getUserPeer(postbox: self.context.account.postbox, peerId: self.peerId) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] peer, _ in + guard let strongSelf = self, let peer = peer else { + return + } + + let presentationData = strongSelf.presentationData + if let peer = peer as? TelegramUser, let _ = peer.botInfo { + strongSelf.activeActionDisposable.set(requestUpdatePeerIsBlocked(account: strongSelf.context.account, peerId: peer.id, isBlocked: block).start()) + if !block { + let _ = enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: [.message(text: "/start", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() + if let navigationController = strongSelf.controller?.navigationController as? NavigationController { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id))) + } + } + } else { + if block { + let presentationData = strongSelf.presentationData + let actionSheet = ActionSheetController(presentationData: presentationData) + let dismissAction: () -> Void = { [weak actionSheet] in + actionSheet?.dismissAnimated() + } + var reportSpam = false + var deleteChat = false + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: presentationData.strings.UserInfo_BlockConfirmationTitle(peer.compactDisplayTitle).0), + ActionSheetButtonItem(title: presentationData.strings.UserInfo_BlockActionTitle(peer.compactDisplayTitle).0, color: .destructive, action: { + dismissAction() + guard let strongSelf = self else { + return + } + + strongSelf.activeActionDisposable.set(requestUpdatePeerIsBlocked(account: strongSelf.context.account, peerId: peer.id, isBlocked: true).start()) + if deleteChat { + let _ = removePeerChat(account: strongSelf.context.account, peerId: strongSelf.peerId, reportChatSpam: reportSpam).start() + (strongSelf.controller?.navigationController as? NavigationController)?.popToRoot(animated: true) + } else if reportSpam { + let _ = reportPeer(account: strongSelf.context.account, peerId: strongSelf.peerId, reason: .spam).start() + } + + deleteSendMessageIntents(peerId: strongSelf.peerId) + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + strongSelf.view.endEditing(true) + strongSelf.controller?.present(actionSheet, in: .window(.root)) + } else { + let text: String + if block { + text = presentationData.strings.UserInfo_BlockConfirmation(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).0 + } else { + text = presentationData.strings.UserInfo_UnblockConfirmation(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).0 + } + strongSelf.controller?.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Yes, action: { + guard let strongSelf = self else { + return + } + strongSelf.activeActionDisposable.set(requestUpdatePeerIsBlocked(account: strongSelf.context.account, peerId: peer.id, isBlocked: block).start()) + })]), in: .window(.root)) + } + } + }) + } + + private func openReport(user: Bool) { + guard let controller = self.controller else { + return + } + self.view.endEditing(true) + + let options: [PeerReportOption] + if user { + options = [.spam, .violence, .pornography, .childAbuse] + } else { + options = [.spam, .violence, .pornography, .childAbuse, .copyright, .other] + } + controller.present(peerReportOptionsController(context: self.context, subject: .peer(self.peerId), options: options, present: { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + }, push: { [weak controller] c in + controller?.push(c) + }, completion: { _ in }), in: .window(.root)) + } + + private func openEncryptionKey() { + guard let data = self.data, let peer = data.peer, let encryptionKeyFingerprint = data.encryptionKeyFingerprint else { + return + } + self.controller?.push(SecretChatKeyController(context: self.context, fingerprint: encryptionKeyFingerprint, peer: peer)) + } + + private func openShareBot() { + let _ = (getUserPeer(postbox: self.context.account.postbox, peerId: self.peerId) + |> deliverOnMainQueue).start(next: { [weak self] peer, _ in + guard let strongSelf = self else { + return + } + if let peer = peer as? TelegramUser, let username = peer.username { + let shareController = ShareController(context: strongSelf.context, subject: .url("https://t.me/\(username)")) + strongSelf.view.endEditing(true) + strongSelf.controller?.present(shareController, in: .window(.root)) + } + }) + } + + private func openAddBotToGroup() { + guard let controller = self.controller else { + return + } + context.sharedContext.openResolvedUrl(.groupBotStart(peerId: peerId, payload: ""), context: self.context, urlContext: .generic, navigationController: controller.navigationController as? NavigationController, openPeer: { id, navigation in + }, sendFile: nil, + sendSticker: nil, + present: { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + }, dismissInput: { [weak controller] in + controller?.view.endEditing(true) + }, contentContext: nil) + } + + private func performBotCommand(command: PeerInfoBotCommand) { + let _ = (self.context.account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let strongSelf = self else { + return + } + let text: String + switch command { + case .settings: + text = "/settings" + case .privacy: + text = "/privacy" + case .help: + text = "/help" + } + let _ = enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: [.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() + + if let navigationController = strongSelf.controller?.navigationController as? NavigationController { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId))) + } + }) + } + + private func editingOpenPublicLinkSetup() { + self.controller?.push(channelVisibilityController(context: self.context, peerId: self.peerId, mode: .generic, upgradedToSupergroup: { _, f in f() })) + } + + private func editingOpenDiscussionGroupSetup() { + guard let data = self.data, let peer = data.peer else { + return + } + self.controller?.push(channelDiscussionGroupSetupController(context: self.context, peerId: peer.id)) + } + + private func editingToggleMessageSignatures(value: Bool) { + self.toggleShouldChannelMessagesSignaturesDisposable.set(toggleShouldChannelMessagesSignatures(account: self.context.account, peerId: self.peerId, enabled: value).start()) + } + + private func openParticipantsSection(section: PeerInfoParticipantsSection) { + guard let data = self.data, let peer = data.peer else { + return + } + switch section { + case .members: + self.controller?.push(channelMembersController(context: self.context, peerId: self.peerId)) + case .admins: + if peer is TelegramGroup { + self.controller?.push(channelAdminsController(context: self.context, peerId: self.peerId)) + } else if peer is TelegramChannel { + self.controller?.push(channelAdminsController(context: self.context, peerId: self.peerId)) + } + case .banned: + self.controller?.push(channelBlacklistController(context: self.context, peerId: self.peerId)) + } + } + + private func editingOpenPreHistorySetup() { + guard let data = self.data, let peer = data.peer else { + return + } + self.controller?.push(groupPreHistorySetupController(context: self.context, peerId: peer.id, upgradedToSupergroup: { _, f in f() })) + } + + private func openPermissions() { + guard let data = self.data, let peer = data.peer else { + return + } + self.controller?.push(channelPermissionsController(context: self.context, peerId: peer.id)) + } + + private func editingOpenStickerPackSetup() { + guard let data = self.data, let peer = data.peer, let cachedData = data.cachedData as? CachedChannelData else { + return + } + self.controller?.push(groupStickerPackSetupController(context: self.context, peerId: peer.id, currentPackInfo: cachedData.stickerPack)) + } + + private func openLocation() { + guard let data = self.data, let peer = data.peer, let cachedData = data.cachedData as? CachedChannelData, let location = cachedData.peerGeoLocation else { + return + } + let context = self.context + let presentationData = self.presentationData + let mapMedia = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, geoPlace: nil, venue: MapVenue(title: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), address: location.address, provider: nil, id: nil, type: nil), liveBroadcastingTimeout: nil) + let locationController = legacyLocationController(message: nil, mapMedia: mapMedia, context: context, openPeer: { _ in }, sendLiveLocation: { _, _ in }, stopLiveLocation: {}, openUrl: { url in + context.sharedContext.applicationBindings.openUrl(url) + }) + self.controller?.push(locationController) + } + + private func editingOpenSetupLocation() { + guard let data = self.data, let peer = data.peer else { + return + } + let presentationData = self.presentationData + let locationController = legacyLocationPickerController(context: self.context, selfPeer: peer, peer: peer, sendLocation: { [weak self] coordinate, _, address in + guard let strongSelf = self else { + return + } + let addressSignal: Signal + if let address = address { + addressSignal = .single(address) + } else { + addressSignal = reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) + |> map { placemark in + if let placemark = placemark { + return placemark.fullAddress + } else { + return "\(coordinate.latitude), \(coordinate.longitude)" + } + } + } + + let context = strongSelf.context + let _ = (addressSignal + |> mapToSignal { address -> Signal in + return updateChannelGeoLocation(postbox: context.account.postbox, network: context.account.network, channelId: peer.id, coordinate: (coordinate.latitude, coordinate.longitude), address: address) + } + |> deliverOnMainQueue).start(error: { errror in + guard let strongSelf = self else { + return + } + strongSelf.controller?.present(textAlertController(context: context, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }) + }, sendLiveLocation: { _, _ in }, theme: presentationData.theme, customLocationPicker: true, presentationCompleted: { + }) + self.controller?.push(locationController) + } + + private func openPeerInfo(peer: Peer) { + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { + (self.controller?.navigationController as? NavigationController)?.pushViewController(infoController) + } + } + + private func performMemberAction(member: PeerInfoMember, action: PeerInfoMemberAction) { + guard let data = self.data, let peer = data.peer else { + return + } + switch action { + case .promote: + if case let .channelMember(channelMember) = member { + self.controller?.push(channelAdminController(context: self.context, peerId: peer.id, adminId: member.id, initialParticipant: channelMember.participant, updated: { _ in + }, upgradedToSupergroup: { _, f in f() }, transferedOwnership: { _ in })) + } + case .restrict: + if case let .channelMember(channelMember) = member { + self.controller?.push(channelBannedMemberController(context: self.context, peerId: peer.id, memberId: member.id, initialParticipant: channelMember.participant, updated: { _ in + }, upgradedToSupergroup: { _, f in f() })) + } + case .remove: + data.members?.membersContext.removeMember(memberId: member.id) + } + } + + private func openPeerInfoContextMenu(subject: PeerInfoContextSubject, sourceNode: ASDisplayNode) { + guard let data = self.data, let peer = data.peer, let controller = self.controller else { + return + } + switch subject { + case .bio: + var text: String? + if let cachedData = data.cachedData as? CachedUserData { + text = cachedData.about + } else if let cachedData = data.cachedData as? CachedGroupData { + text = cachedData.about + } else if let cachedData = data.cachedData as? CachedChannelData { + text = cachedData.about + } + if let text = text, !text.isEmpty { + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { + UIPasteboard.general.string = text + })]) + controller.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak sourceNode] in + if let controller = self?.controller, let sourceNode = sourceNode { + return (sourceNode, sourceNode.bounds.insetBy(dx: 0.0, dy: -2.0), controller.displayNode, controller.view.bounds) + } else { + return nil + } + })) + } + case let .phone(phone): + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { + UIPasteboard.general.string = phone + })]) + controller.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak sourceNode] in + if let controller = self?.controller, let sourceNode = sourceNode { + return (sourceNode, sourceNode.bounds.insetBy(dx: 0.0, dy: -2.0), controller.displayNode, controller.view.bounds) + } else { + return nil + } + })) + case .link: + if let addressName = peer.addressName { + let text: String + if peer is TelegramChannel { + text = "https://t.me/\(addressName)" + } else { + text = "@" + addressName + } + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { + UIPasteboard.general.string = text + })]) + controller.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak sourceNode] in + if let controller = self?.controller, let sourceNode = sourceNode { + return (sourceNode, sourceNode.bounds.insetBy(dx: 0.0, dy: -2.0), controller.displayNode, controller.view.bounds) + } else { + return nil + } + })) + } + } + } + + private func performBioLinkAction(action: TextLinkItemActionType, item: TextLinkItem) { + guard let data = self.data, let peer = data.peer, let controller = self.controller else { + return + } + self.context.sharedContext.handleTextLinkAction(context: self.context, peerId: peer.id, navigateDisposable: self.resolveUrlDisposable, controller: controller, action: action, itemLink: item) + } + + private func requestLayout() { + self.headerNode.requestUpdateLayout?() + } + + private func openDeletePeer() { + let peerId = self.peerId + let _ = (self.context.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let strongSelf = self, let peer = peer else { + return + } + var isGroup = false + if let channel = peer as? TelegramChannel { + if case .group = channel.info { + isGroup = true + } + } else if peer is TelegramGroup { + isGroup = true + } + let presentationData = strongSelf.presentationData + let actionSheet = ActionSheetController(presentationData: presentationData) + let dismissAction: () -> Void = { [weak actionSheet] in + actionSheet?.dismissAnimated() + } + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: isGroup ? presentationData.strings.ChannelInfo_DeleteGroupConfirmation : presentationData.strings.ChannelInfo_DeleteChannelConfirmation), + ActionSheetButtonItem(title: isGroup ? presentationData.strings.ChannelInfo_DeleteGroup : presentationData.strings.ChannelInfo_DeleteChannel, color: .destructive, action: { + dismissAction() + self?.deletePeerChat(peer: peer, globally: true) + }), + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + strongSelf.view.endEditing(true) + strongSelf.controller?.present(actionSheet, in: .window(.root)) + }) + } + + private func openLeavePeer() { + let peerId = self.peerId + let _ = (self.context.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let strongSelf = self, let peer = peer else { + return + } + var isGroup = false + if let channel = peer as? TelegramChannel { + if case .group = channel.info { + isGroup = true + } + } else if peer is TelegramGroup { + isGroup = true + } + let presentationData = strongSelf.presentationData + let actionSheet = ActionSheetController(presentationData: presentationData) + let dismissAction: () -> Void = { [weak actionSheet] in + actionSheet?.dismissAnimated() + } + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: isGroup ? presentationData.strings.Group_LeaveGroup : presentationData.strings.Channel_LeaveChannel, color: .destructive, action: { + dismissAction() + self?.deletePeerChat(peer: peer, globally: false) + }), + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + strongSelf.view.endEditing(true) + strongSelf.controller?.present(actionSheet, in: .window(.root)) + }) + } + + private func deletePeerChat(peer: Peer, globally: Bool) { + guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { + return + } + guard let tabController = navigationController.viewControllers.first as? TabBarController else { + return + } + for childController in tabController.controllers { + if let chatListController = childController as? ChatListController { + chatListController.maybeAskForPeerChatRemoval(peer: RenderedPeer(peer: peer), deleteGloballyIfPossible: globally, completion: { [weak navigationController] deleted in + if deleted { + navigationController?.popToRoot(animated: true) + } + }, removed: { + }) + break + } + } + } + + private func openAvatarForEditing() { + guard let peer = self.data?.peer, canEditPeerInfo(peer: peer) else { + return + } + + let peerId = self.peerId + let _ = (self.context.account.postbox.transaction { transaction -> (Peer?, SearchBotsConfiguration) in + return (transaction.getPeer(peerId), currentSearchBotsConfiguration(transaction: transaction)) + } + |> deliverOnMainQueue).start(next: { [weak self] peer, searchBotsConfiguration in + guard let strongSelf = self, let peer = peer else { + return + } + + let presentationData = strongSelf.presentationData + + 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) + + strongSelf.view.endEditing(true) + strongSelf.controller?.present(legacyController, in: .window(.root)) + + var hasPhotos = false + if !peer.profileImageRepresentations.isEmpty { + hasPhotos = true + } + + let completedImpl: (UIImage) -> Void = { image in + guard let strongSelf = self, let data = image.jpegData(compressionQuality: 0.6) else { + return + } + + let resource = LocalFileMediaResource(fileId: arc4random64()) + strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource) + + strongSelf.state = strongSelf.state.withUpdatingAvatar(.image(representation)) + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + + let postbox = strongSelf.context.account.postbox + strongSelf.updateAvatarDisposable.set((updatePeerPhoto(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, stateManager: strongSelf.context.account.stateManager, accountPeerId: strongSelf.context.account.peerId, peerId: strongSelf.peerId, photo: uploadedPeerPhoto(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, resource: resource), mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) + |> deliverOnMainQueue).start(next: { result in + guard let strongSelf = self else { + return + } + switch result { + case .complete: + strongSelf.state = strongSelf.state.withUpdatingAvatar(nil) + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + case .progress: + break + } + })) + } + + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos, hasViewButton: false, personalPhoto: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)! + let _ = strongSelf.currentAvatarMixin.swap(mixin) + mixin.requestSearchController = { assetsController in + guard let strongSelf = self else { + return + } + let controller = WebSearchController(context: strongSelf.context, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), completion: { result in + assetsController?.dismiss() + completedImpl(result) + })) + strongSelf.controller?.present(controller, in: .window(.root)) + } + mixin.didFinishWithImage = { image in + if let image = image { + completedImpl(image) + } + } + mixin.didFinishWithDelete = { + guard let strongSelf = self else { + return + } + + let _ = strongSelf.currentAvatarMixin.swap(nil) + if let profileImage = peer.smallProfileImage { + strongSelf.state = strongSelf.state.withUpdatingAvatar(.none) + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + } + let postbox = strongSelf.context.account.postbox + strongSelf.updateAvatarDisposable.set((updatePeerPhoto(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, stateManager: strongSelf.context.account.stateManager, accountPeerId: strongSelf.context.account.peerId, peerId: strongSelf.peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) + |> deliverOnMainQueue).start(next: { result in + guard let strongSelf = self else { + return + } + switch result { + case .complete: + strongSelf.state = strongSelf.state.withUpdatingAvatar(nil) + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + case .progress: + break + } + })) + } + mixin.didDismiss = { [weak legacyController] in + guard let strongSelf = self else { + return + } + let _ = strongSelf.currentAvatarMixin.swap(nil) + legacyController?.dismiss() + } + let menuController = mixin.present() + if let menuController = menuController { + menuController.customRemoveFromParentViewController = { [weak legacyController] in + legacyController?.dismiss() + } + } + }) + } + + private func openAddMember() { + guard let data = self.data, let groupPeer = data.peer else { + return + } + + let members: Promise<[PeerId]> = Promise() + if groupPeer.id.namespace == Namespaces.Peer.CloudChannel { + /*var membersDisposable: Disposable? + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { listState in + members.set(.single(listState.list.map {$0.peer.id})) + membersDisposable?.dispose() + }) + membersDisposable = disposable*/ + members.set(.single([])) + } else { + members.set(.single([])) + } + + let _ = (members.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] recentIds in + guard let strongSelf = self else { + return + } + var createInviteLinkImpl: (() -> Void)? + var confirmationImpl: ((PeerId) -> Signal)? + var options: [ContactListAdditionalOption] = [] + let presentationData = strongSelf.presentationData + + var canCreateInviteLink = false + if let group = groupPeer as? TelegramGroup { + switch group.role { + case .creator, .admin: + canCreateInviteLink = true + default: + break + } + } else if let channel = groupPeer as? TelegramChannel { + if channel.hasPermission(.inviteMembers) { + if channel.flags.contains(.isCreator) || (channel.adminRights != nil && channel.username == nil) { + canCreateInviteLink = true + } + } + } + + if canCreateInviteLink { + options.append(ContactListAdditionalOption(title: presentationData.strings.GroupInfo_InviteByLink, icon: .generic(UIImage(bundleImageName: "Contact List/LinkActionIcon")!), action: { + createInviteLinkImpl?() + })) + } + + let contactsController: ViewController + if groupPeer.id.namespace == Namespaces.Peer.CloudGroup { + contactsController = strongSelf.context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(context: strongSelf.context, autoDismiss: false, title: { $0.GroupInfo_AddParticipantTitle }, options: options, confirmation: { peer in + if let confirmationImpl = confirmationImpl, case let .peer(peer, _, _) = peer { + return confirmationImpl(peer.id) + } else { + return .single(false) + } + })) + contactsController.navigationPresentation = .modal + } else { + contactsController = strongSelf.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: strongSelf.context, mode: .peerSelection(searchChatList: false, searchGroups: false), options: options, filters: [.excludeSelf, .disable(recentIds)])) + contactsController.navigationPresentation = .modal + } + + let context = strongSelf.context + confirmationImpl = { [weak contactsController] peerId in + return context.account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue + |> mapToSignal { peer in + let result = ValuePromise() + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + if let contactsController = contactsController { + let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.GroupInfo_AddParticipantConfirmation(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).0, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: { + result.set(false) + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { + result.set(true) + }) + ]) + contactsController.present(alertController, in: .window(.root)) + } + + return result.get() + } + } + + let addMember: (ContactListPeer) -> Signal = { memberPeer -> Signal in + if case let .peer(selectedPeer, _, _) = memberPeer { + let memberId = selectedPeer.id + if groupPeer.id.namespace == Namespaces.Peer.CloudChannel { + return context.peerChannelMemberCategoriesContextsManager.addMember(account: context.account, peerId: groupPeer.id, memberId: memberId) + |> map { _ -> Void in + return Void() + } + |> `catch` { _ -> Signal in + return .complete() + } + } else { + return addGroupMember(account: context.account, peerId: groupPeer.id, memberId: memberId) + |> deliverOnMainQueue + |> `catch` { error -> Signal in + switch error { + case .generic: + return .complete() + case .privacy: + let _ = (context.account.postbox.loadedPeerWithId(memberId) + |> deliverOnMainQueue).start(next: { peer in + self?.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(peer.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }) + return .complete() + case .tooManyChannels: + let _ = (context.account.postbox.loadedPeerWithId(memberId) + |> deliverOnMainQueue).start(next: { peer in + self?.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }) + return .complete() + case .groupFull: + let signal = convertGroupToSupergroup(account: context.account, peerId: groupPeer.id) + |> map(Optional.init) + |> `catch` { error -> Signal in + switch error { + case .tooManyChannels: + Queue.mainQueue().async { + self?.controller?.push(oldChannelsController(context: context, intent: .upgrade)) + } + default: + break + } + return .single(nil) + } + |> mapToSignal { upgradedPeerId -> Signal in + guard let upgradedPeerId = upgradedPeerId else { + return .single(nil) + } + return context.peerChannelMemberCategoriesContextsManager.addMember(account: context.account, peerId: upgradedPeerId, memberId: memberId) + |> `catch` { _ -> Signal in + return .complete() + } + |> mapToSignal { _ -> Signal in + return .complete() + } + |> then(.single(upgradedPeerId)) + } + |> deliverOnMainQueue + |> mapToSignal { _ -> Signal in + return .complete() + } + return signal + } + } + } + } else { + return .complete() + } + } + + let addMembers: ([ContactListPeerId]) -> Signal = { members -> Signal in + let memberIds = members.compactMap { contact -> PeerId? in + switch contact { + case let .peer(peerId): + return peerId + default: + return nil + } + } + return context.account.postbox.multiplePeersView(memberIds) + |> take(1) + |> deliverOnMainQueue + |> mapError { _ in return .generic} + |> mapToSignal { view -> Signal in + if memberIds.count == 1 { + return context.peerChannelMemberCategoriesContextsManager.addMember(account: context.account, peerId: groupPeer.id, memberId: memberIds[0]) + |> map { _ -> Void in + return Void() + } + } else { + return context.peerChannelMemberCategoriesContextsManager.addMembers(account: context.account, peerId: groupPeer.id, memberIds: memberIds) |> map { _ in + } + } + } + } + + createInviteLinkImpl = { [weak contactsController] in + guard let strongSelf = self else { + return + } + let mode: ChannelVisibilityControllerMode + if groupPeer.addressName != nil { + mode = .generic + } else { + mode = .privateLink + } + let visibilityController = channelVisibilityController(context: strongSelf.context, peerId: groupPeer.id, mode: mode, upgradedToSupergroup: { _, f in f() }, onDismissRemoveController: contactsController) + //visibilityController.navigationPresentation = .modal + + contactsController?.push(visibilityController) + + /*if let navigationController = strongSelf.controller?.navigationController as? NavigationController { + var controllers = navigationController.viewControllers + if let contactsController = contactsController { + controllers.removeAll(where: { $0 === contactsController }) + } + controllers.append(visibilityController) + navigationController.setViewControllers(controllers, animated: true) + }*/ + } + + strongSelf.controller?.push(contactsController) + let selectAddMemberDisposable = strongSelf.selectAddMemberDisposable + let addMemberDisposable = strongSelf.addMemberDisposable + if let contactsController = contactsController as? ContactSelectionController { + selectAddMemberDisposable.set((contactsController.result + |> deliverOnMainQueue).start(next: { [weak contactsController] memberPeer in + guard let memberPeer = memberPeer else { + return + } + + contactsController?.displayProgress = true + addMemberDisposable.set((addMember(memberPeer) + |> deliverOnMainQueue).start(completed: { + contactsController?.dismiss() + })) + })) + contactsController.dismissed = { + selectAddMemberDisposable.set(nil) + addMemberDisposable.set(nil) + } + } + if let contactsController = contactsController as? ContactMultiselectionController { + selectAddMemberDisposable.set((contactsController.result + |> deliverOnMainQueue).start(next: { [weak contactsController] peers in + contactsController?.displayProgress = true + addMemberDisposable.set((addMembers(peers) + |> deliverOnMainQueue).start(error: { error in + if peers.count == 1, case .restricted = error { + switch peers[0] { + case let .peer(peerId): + let _ = (context.account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue).start(next: { peer in + self?.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(peer.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }) + default: + break + } + } else if case .tooMuchJoined = error { + self?.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + + contactsController?.dismiss() + },completed: { + contactsController?.dismiss() + })) + })) + contactsController.dismissed = { + selectAddMemberDisposable.set(nil) + addMemberDisposable.set(nil) + } + } + }) + } + + private func deleteMessages(messageIds: Set?) { + if let messageIds = messageIds ?? self.state.selectedMessageIds, !messageIds.isEmpty { + self.activeActionDisposable.set((self.context.sharedContext.chatAvailableMessageActions(postbox: self.context.account.postbox, accountPeerId: self.context.account.peerId, messageIds: messageIds) + |> deliverOnMainQueue).start(next: { [weak self] actions in + if let strongSelf = self, let peer = strongSelf.data?.peer, !actions.options.isEmpty { + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) + var items: [ActionSheetItem] = [] + var personalPeerName: String? + var isChannel = false + if let user = peer as? TelegramUser { + personalPeerName = user.compactDisplayTitle + } else if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + isChannel = true + } + + if actions.options.contains(.deleteGlobally) { + let globalTitle: String + if isChannel { + globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe + } else if let personalPeerName = personalPeerName { + globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).0 + } else { + globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone + } + items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone) + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start() + } + })) + } + if actions.options.contains(.deleteLocally) { + var localOptionText = strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe + if strongSelf.context.account.peerId == strongSelf.peerId { + if messageIds.count == 1 { + localOptionText = strongSelf.presentationData.strings.Conversation_Moderate_Delete + } else { + localOptionText = strongSelf.presentationData.strings.Conversation_DeleteManyMessages + } + } + items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone) + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forLocalPeer).start() + } + })) + } + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.view.endEditing(true) + strongSelf.controller?.present(actionSheet, in: .window(.root)) + } + })) + } + } + + func forwardMessages(messageIds: Set?) { + if let messageIds = messageIds ?? self.state.selectedMessageIds, !messageIds.isEmpty { + let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.onlyWriteable, .excludeDisabled])) + peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peerId in + if let strongSelf = self, let _ = peerSelectionController { + if peerId == strongSelf.context.account.peerId { + strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone) + + let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messageIds.map { id -> EnqueueMessage in + return .forward(source: id, grouping: .auto, attributes: []) + }) + |> deliverOnMainQueue).start(next: { [weak self] messageIds in + if let strongSelf = self { + let signals: [Signal] = messageIds.compactMap({ id -> Signal? in + guard let id = id else { + return nil + } + return strongSelf.context.account.pendingMessageManager.pendingMessageStatus(id) + |> mapToSignal { status, _ -> Signal in + if status != nil { + return .never() + } else { + return .single(true) + } + } + |> take(1) + }) + strongSelf.activeActionDisposable.set((combineLatest(signals) + |> deliverOnMainQueue).start(completed: { + guard let strongSelf = self else { + return + } + strongSelf.controller?.present(OverlayStatusController(theme: strongSelf.presentationData.theme, type: .success), in: .window(.root)) + })) + } + }) + if let peerSelectionController = peerSelectionController { + peerSelectionController.dismiss() + } + } else { + let _ = (strongSelf.context.account.postbox.transaction({ transaction -> Void in + transaction.updatePeerChatInterfaceState(peerId, update: { currentState in + if let currentState = currentState as? ChatInterfaceState { + return currentState.withUpdatedForwardMessageIds(Array(messageIds)) + } else { + return ChatInterfaceState().withUpdatedForwardMessageIds(Array(messageIds)) + } + }) + }) |> deliverOnMainQueue).start(completed: { + if let strongSelf = self { + strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone) + + let ready = ValuePromise() + strongSelf.activeActionDisposable.set((ready.get() |> take(1) |> deliverOnMainQueue).start(next: { _ in + if let peerSelectionController = peerSelectionController { + peerSelectionController.dismiss() + } + })) + + (strongSelf.controller?.navigationController as? NavigationController)?.replaceTopController(ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(peerId)), animated: false, ready: ready) + } + }) + } + } + } + self.controller?.push(peerSelectionController) + } + } + + private func activateSearch() { + guard let (layout, navigationBarHeight) = self.validLayout else { + return + } + + if let _ = self.searchDisplayController { + return + } + + if let currentPaneKey = self.paneContainerNode.currentPaneKey, case .members = currentPaneKey { + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, placeholder: self.presentationData.strings.Common_Search, contentNode: ChannelMembersSearchContainerNode(context: self.context, peerId: self.peerId, mode: .searchMembers, filters: [], searchContext: self.groupMembersSearchContext, openPeer: { [weak self] peer, participant in + self?.openPeer(peerId: peer.id, navigation: .info) + }, updateActivity: { _ in + }, pushController: { [weak self] c in + self?.controller?.push(c) + }), cancel: { [weak self] in + self?.deactivateSearch() + }) + } else { + var tagMask: MessageTags = .file + if let currentPaneKey = self.paneContainerNode.currentPaneKey { + switch currentPaneKey { + case .links: + tagMask = .webPage + case .music: + tagMask = .music + default: + break + } + } + + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, placeholder: self.presentationData.strings.Common_Search, contentNode: ChatHistorySearchContainerNode(context: self.context, peerId: self.peerId, tagMask: tagMask, interfaceInteraction: self.chatInterfaceInteraction), cancel: { [weak self] in + self?.deactivateSearch() + }) + } + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) + if let navigationBar = self.controller?.navigationBar { + transition.updateAlpha(node: navigationBar, alpha: 0.0) + } + + self.searchDisplayController?.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.searchDisplayController?.activate(insertSubnode: { [weak self] subnode, isSearchBar in + if let strongSelf = self, let navigationBar = strongSelf.controller?.navigationBar { + strongSelf.insertSubnode(subnode, belowSubnode: navigationBar) + } + }, placeholder: nil) + + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + } + } + + private func deactivateSearch() { + guard let searchDisplayController = self.searchDisplayController else { + return + } + self.searchDisplayController = nil + searchDisplayController.deactivate(placeholder: nil) + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .easeInOut) + if let navigationBar = self.controller?.navigationBar { + transition.updateAlpha(node: navigationBar, alpha: 1.0) + } + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + + self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor + + self.updateNavigationExpansionPresentation(isExpanded: self.headerNode.isAvatarExpanded, animated: false) + + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + } + } + + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition, additive: Bool = false) { + self.validLayout = (layout, navigationHeight) + + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: transition) + if !searchDisplayController.isDeactivating { + //vanillaInsets.top += (layout.statusBarHeight ?? 0.0) - navigationBarHeightDelta + } + } + + self.ignoreScrolling = true + + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + let sectionSpacing: CGFloat = 24.0 + + var contentHeight: CGFloat = 0.0 + + let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, notificationSettings: self.data?.notificationSettings, statusData: self.data?.status, isContact: self.data?.isContact ?? false, state: self.state, transition: transition, additive: additive) + let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: headerHeight)) + if additive { + transition.updateFrameAdditive(node: self.headerNode, frame: headerFrame) + } else { + transition.updateFrame(node: self.headerNode, frame: headerFrame) + } + if !self.isMediaOnly { + contentHeight += headerHeight + contentHeight += sectionSpacing + } else { + contentHeight += navigationHeight + } + + var validRegularSections: [AnyHashable] = [] + if !self.isMediaOnly { + for (sectionId, sectionItems) in infoItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, nearbyPeer: self.nearbyPeer, callMessages: self.callMessages) { + validRegularSections.append(sectionId) + + let sectionNode: PeerInfoScreenItemSectionContainerNode + if let current = self.regularSections[sectionId] { + sectionNode = current + } else { + sectionNode = PeerInfoScreenItemSectionContainerNode() + self.regularSections[sectionId] = sectionNode + self.scrollNode.addSubnode(sectionNode) + } + + let sectionHeight = sectionNode.update(width: layout.size.width, presentationData: self.presentationData, items: sectionItems, transition: transition) + let sectionFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: sectionHeight)) + if additive { + transition.updateFrameAdditive(node: sectionNode, frame: sectionFrame) + } else { + transition.updateFrame(node: sectionNode, frame: sectionFrame) + } + + transition.updateAlpha(node: sectionNode, alpha: self.state.isEditing ? 0.0 : 1.0) + if !sectionHeight.isZero && !self.state.isEditing { + contentHeight += sectionHeight + contentHeight += sectionSpacing + } + } + var removeRegularSections: [AnyHashable] = [] + for (sectionId, sectionNode) in self.regularSections { + if !validRegularSections.contains(sectionId) { + removeRegularSections.append(sectionId) + } + } + for sectionId in removeRegularSections { + if let sectionNode = self.regularSections.removeValue(forKey: sectionId) { + sectionNode.removeFromSupernode() + } + } + + var validEditingSections: [AnyHashable] = [] + for (sectionId, sectionItems) in editingItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction) { + validEditingSections.append(sectionId) + + var wasAdded = false + let sectionNode: PeerInfoScreenItemSectionContainerNode + if let current = self.editingSections[sectionId] { + sectionNode = current + } else { + wasAdded = true + sectionNode = PeerInfoScreenItemSectionContainerNode() + self.editingSections[sectionId] = sectionNode + self.scrollNode.addSubnode(sectionNode) + } + + let sectionHeight = sectionNode.update(width: layout.size.width, presentationData: self.presentationData, items: sectionItems, transition: transition) + let sectionFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: sectionHeight)) + + if wasAdded { + sectionNode.frame = sectionFrame + sectionNode.alpha = self.state.isEditing ? 1.0 : 0.0 + } else { + if additive { + transition.updateFrameAdditive(node: sectionNode, frame: sectionFrame) + } else { + transition.updateFrame(node: sectionNode, frame: sectionFrame) + } + transition.updateAlpha(node: sectionNode, alpha: self.state.isEditing ? 1.0 : 0.0) + } + if !sectionHeight.isZero && self.state.isEditing { + contentHeight += sectionHeight + contentHeight += sectionSpacing + } + } + var removeEditingSections: [AnyHashable] = [] + for (sectionId, sectionNode) in self.editingSections { + if !validEditingSections.contains(sectionId) { + removeEditingSections.append(sectionId) + } + } + for sectionId in removeEditingSections { + if let sectionNode = self.editingSections.removeValue(forKey: sectionId) { + sectionNode.removeFromSupernode() + } + } + } + + let paneContainerSize = CGSize(width: layout.size.width, height: layout.size.height - navigationHeight) + var restoreContentOffset: CGPoint? + if additive { + restoreContentOffset = self.scrollNode.view.contentOffset + } + + let paneContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: paneContainerSize) + if self.state.isEditing || (self.data?.availablePanes ?? []).isEmpty { + transition.updateAlpha(node: self.paneContainerNode, alpha: 0.0) + } else { + contentHeight += layout.size.height - navigationHeight + transition.updateAlpha(node: self.paneContainerNode, alpha: 1.0) + } + + if let selectedMessageIds = self.state.selectedMessageIds { + var wasAdded = false + let selectionPanelNode: PeerInfoSelectionPanelNode + if let current = self.paneContainerNode.selectionPanelNode { + selectionPanelNode = current + } else { + wasAdded = true + selectionPanelNode = PeerInfoSelectionPanelNode(context: self.context, peerId: self.peerId, deleteMessages: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.deleteMessages(messageIds: nil) + }, shareMessages: { [weak self] in + guard let strongSelf = self, let messageIds = strongSelf.state.selectedMessageIds, !messageIds.isEmpty else { + return + } + let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Message] in + var messages: [Message] = [] + for id in messageIds { + if let message = transaction.getMessage(id) { + messages.append(message) + } + } + return messages + } + |> deliverOnMainQueue).start(next: { messages in + if let strongSelf = self, !messages.isEmpty { + strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone) + + let shareController = ShareController(context: strongSelf.context, subject: .messages(messages.sorted(by: { lhs, rhs in + return lhs.index < rhs.index + })), externalShare: true, immediateExternalShare: true) + strongSelf.view.endEditing(true) + strongSelf.controller?.present(shareController, in: .window(.root)) + } + }) + }, forwardMessages: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.forwardMessages(messageIds: nil) + }, reportMessages: { [weak self] in + guard let strongSelf = self, let messageIds = strongSelf.state.selectedMessageIds, !messageIds.isEmpty else { + return + } + strongSelf.view.endEditing(true) + strongSelf.controller?.present(peerReportOptionsController(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), present: { c, a in + self?.controller?.present(c, in: .window(.root), with: a) + }, push: { c in + self?.controller?.push(c) + }, completion: { _ in }), in: .window(.root)) + }) + self.paneContainerNode.selectionPanelNode = selectionPanelNode + self.paneContainerNode.addSubnode(selectionPanelNode) + } + selectionPanelNode.selectionPanel.selectedMessages = selectedMessageIds + let panelHeight = selectionPanelNode.update(layout: layout, presentationData: self.presentationData, transition: wasAdded ? .immediate : transition) + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: paneContainerSize.height - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight)) + if wasAdded { + selectionPanelNode.frame = panelFrame + transition.animatePositionAdditive(node: selectionPanelNode, offset: CGPoint(x: 0.0, y: panelHeight)) + } else { + transition.updateFrame(node: selectionPanelNode, frame: panelFrame) + } + } else if let selectionPanelNode = self.paneContainerNode.selectionPanelNode { + self.paneContainerNode.selectionPanelNode = nil + transition.updateFrame(node: selectionPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: selectionPanelNode.bounds.size), completion: { [weak selectionPanelNode] _ in + selectionPanelNode?.removeFromSupernode() + }) + } + + self.scrollNode.view.contentSize = CGSize(width: layout.size.width, height: contentHeight) + if let restoreContentOffset = restoreContentOffset { + self.scrollNode.view.contentOffset = restoreContentOffset + } + + if additive { + transition.updateFrameAdditive(node: self.paneContainerNode, frame: paneContainerFrame) + } else { + transition.updateFrame(node: self.paneContainerNode, frame: paneContainerFrame) + } + + self.ignoreScrolling = false + self.updateNavigation(transition: transition, additive: additive) + + if !self.didSetReady && self.data != nil { + self.didSetReady = true + let avatarReady = self.headerNode.avatarListNode.isReady.get() + let combinedSignal = combineLatest(queue: .mainQueue(), + avatarReady, + self.paneContainerNode.isReady.get() + ) + |> map { lhs, rhs in + return lhs && rhs + } + self._ready.set(combinedSignal + |> filter { $0 } + |> take(1)) + } + } + + private func updateNavigation(transition: ContainedViewLayoutTransition, additive: Bool) { + let offsetY = self.scrollNode.view.contentOffset.y + + if self.state.isEditing || offsetY <= 50.0 || self.paneContainerNode.alpha.isZero { + if !self.scrollNode.view.bounces { + self.scrollNode.view.bounces = true + self.scrollNode.view.alwaysBounceVertical = true + } + } else { + if self.scrollNode.view.bounces { + self.scrollNode.view.bounces = false + self.scrollNode.view.alwaysBounceVertical = false + } + } + + if let (layout, navigationHeight) = self.validLayout { + if !additive { + self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, contentOffset: self.isMediaOnly ? 212.0 : offsetY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, notificationSettings: self.data?.notificationSettings, statusData: self.data?.status, isContact: self.data?.isContact ?? false, state: self.state, transition: transition, additive: additive) + } + + let paneAreaExpansionDistance: CGFloat = 32.0 + var paneAreaExpansionDelta = (self.paneContainerNode.frame.minY - navigationHeight) - self.scrollNode.view.contentOffset.y + paneAreaExpansionDelta = max(0.0, min(paneAreaExpansionDelta, paneAreaExpansionDistance)) + + let paneAreaExpansionFraction: CGFloat = 1.0 - paneAreaExpansionDelta / paneAreaExpansionDistance + + let effectiveAreaExpansionFraction: CGFloat + if self.state.isEditing { + effectiveAreaExpansionFraction = 0.0 + } else { + effectiveAreaExpansionFraction = paneAreaExpansionFraction + } + + transition.updateAlpha(node: self.headerNode.separatorNode, alpha: 1.0 - effectiveAreaExpansionFraction) + + let visibleHeight = self.scrollNode.view.contentOffset.y + self.scrollNode.view.bounds.height - self.paneContainerNode.frame.minY + + var bottomInset = layout.intrinsicInsets.bottom + if let selectionPanelNode = self.paneContainerNode.selectionPanelNode { + bottomInset = max(bottomInset, selectionPanelNode.bounds.height) + } + + self.paneContainerNode.update(size: self.paneContainerNode.bounds.size, sideInset: layout.safeInsets.left, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: paneAreaExpansionFraction, presentationData: self.presentationData, data: self.data, transition: transition) + self.headerNode.navigationButtonContainer.frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: layout.statusBarHeight ?? 0.0), size: CGSize(width: layout.size.width - layout.safeInsets.left * 2.0, height: 44.0)) + self.headerNode.navigationButtonContainer.isWhite = self.headerNode.isAvatarExpanded + + var navigationButtons: [PeerInfoHeaderNavigationButtonSpec] = [] + if self.state.isEditing { + navigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .done, isForExpandedView: false)) + } else { + if peerInfoCanEdit(peer: self.data?.peer, cachedData: self.data?.cachedData) { + navigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .edit, isForExpandedView: false)) + } + if self.state.selectedMessageIds == nil { + if let currentPaneKey = self.paneContainerNode.currentPaneKey { + switch currentPaneKey { + case .files, .music, .links, .members: + navigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .search, isForExpandedView: true)) + default: + break + } + switch currentPaneKey { + case .media, .files, .music, .links, .voice: + //navigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .select, isForExpandedView: true)) + break + default: + break + } + } + } else { + navigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .selectionDone, isForExpandedView: true)) + } + } + self.headerNode.navigationButtonContainer.update(size: CGSize(width: layout.size.width - layout.safeInsets.left * 2.0, height: 44.0), presentationData: self.presentationData, buttons: navigationButtons, expandFraction: effectiveAreaExpansionFraction, transition: transition) + } + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.canAddVelocity = true + self.canOpenAvatarByDragging = self.headerNode.isAvatarExpanded + self.paneContainerNode.currentPane?.node.cancelPreviewGestures() + } + + private var previousVelocityM1: CGFloat = 0.0 + private var previousVelocity: CGFloat = 0.0 + private var canAddVelocity: Bool = false + + private var canOpenAvatarByDragging = false + + private let velocityKey: String = encodeText("`wfsujdbmWfmpdjuz", -1) + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if self.ignoreScrolling { + return + } + self.updateNavigation(transition: .immediate, additive: false) + + if !self.state.isEditing { + if self.canAddVelocity { + self.previousVelocityM1 = self.previousVelocity + if let value = (scrollView.value(forKey: self.velocityKey) as? NSNumber)?.doubleValue { + self.previousVelocity = CGFloat(value) + } + } + + let offsetY = self.scrollNode.view.contentOffset.y + var shouldBeExpanded: Bool? + if offsetY <= -32.0 && scrollView.isDragging && scrollView.isTracking { + if let peer = self.data?.peer, peer.smallProfileImage != nil { + shouldBeExpanded = true + + if self.canOpenAvatarByDragging && self.headerNode.isAvatarExpanded && offsetY <= -32.0 { + self.canOpenAvatarByDragging = false + self.headerNode.initiateAvatarExpansion() + } + } + } else if offsetY >= 1.0 { + shouldBeExpanded = false + self.canOpenAvatarByDragging = false + } + if let shouldBeExpanded = shouldBeExpanded, shouldBeExpanded != self.headerNode.isAvatarExpanded { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) + + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + if shouldBeExpanded { + self.hapticFeedback?.impact() + } else { + self.hapticFeedback?.tap() + } + + self.headerNode.updateIsAvatarExpanded(shouldBeExpanded, transition: transition) + self.updateNavigationExpansionPresentation(isExpanded: shouldBeExpanded, animated: true) + + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition, additive: true) + } + } + } + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + guard let (_, navigationHeight) = self.validLayout else { + return + } + + let paneAreaExpansionFinalPoint: CGFloat = self.paneContainerNode.frame.minY - navigationHeight + if abs(scrollView.contentOffset.y - paneAreaExpansionFinalPoint) < .ulpOfOne { + self.paneContainerNode.currentPane?.node.transferVelocity(self.previousVelocityM1) + } + } + + private func updateNavigationExpansionPresentation(isExpanded: Bool, animated: Bool) { + if let controller = self.controller { + controller.statusBar.updateStatusBarStyle(isExpanded ? .White : self.presentationData.theme.rootController.statusBarStyle.style, animated: animated) + + if animated { + UIView.transition(with: controller.controllerNode.headerNode.navigationButtonContainer.view, duration: 0.3, options: [.transitionCrossDissolve], animations: { + }, completion: nil) + } + + let baseNavigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData) + let navigationBarPresentationData = NavigationBarPresentationData( + theme: NavigationBarTheme( + buttonColor: isExpanded ? .white : baseNavigationBarPresentationData.theme.buttonColor, + disabledButtonColor: baseNavigationBarPresentationData.theme.disabledButtonColor, + primaryTextColor: baseNavigationBarPresentationData.theme.primaryTextColor, + backgroundColor: .clear, + separatorColor: .clear, + badgeBackgroundColor: baseNavigationBarPresentationData.theme.badgeBackgroundColor, + badgeStrokeColor: baseNavigationBarPresentationData.theme.badgeStrokeColor, + badgeTextColor: baseNavigationBarPresentationData.theme.badgeTextColor + ), strings: baseNavigationBarPresentationData.strings) + + if let navigationBar = controller.navigationBar { + if animated { + UIView.transition(with: navigationBar.view, duration: 0.3, options: [.transitionCrossDissolve], animations: { + }, completion: nil) + } + navigationBar.updatePresentationData(navigationBarPresentationData) + } + } + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + guard let (_, navigationHeight) = self.validLayout else { + return + } + if !self.state.isEditing { + if targetContentOffset.pointee.y < 212.0 { + if targetContentOffset.pointee.y < 212.0 / 2.0 { + targetContentOffset.pointee.y = 0.0 + self.canAddVelocity = false + self.previousVelocity = 0.0 + self.previousVelocityM1 = 0.0 + } else { + targetContentOffset.pointee.y = 212.0 + self.canAddVelocity = false + self.previousVelocity = 0.0 + self.previousVelocityM1 = 0.0 + } + } + let paneAreaExpansionDistance: CGFloat = 32.0 + let paneAreaExpansionFinalPoint: CGFloat = self.paneContainerNode.frame.minY - navigationHeight + if targetContentOffset.pointee.y > paneAreaExpansionFinalPoint - paneAreaExpansionDistance && targetContentOffset.pointee.y < paneAreaExpansionFinalPoint { + targetContentOffset.pointee.y = paneAreaExpansionFinalPoint + self.canAddVelocity = false + self.previousVelocity = 0.0 + self.previousVelocityM1 = 0.0 + } + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + var currentParent: UIView? = result + var enableScrolling = true + while true { + if currentParent == nil || currentParent === self.view { + break + } + if let scrollView = currentParent as? UIScrollView { + if scrollView === self.scrollNode.view { + break + } + if scrollView.isDecelerating && scrollView.contentOffset.y < -scrollView.contentInset.top { + return self.scrollNode.view + } + } else if let listView = currentParent as? ListViewBackingView, let listNode = listView.target { + if listNode.scroller.isDecelerating && listNode.scroller.contentOffset.y < listNode.scroller.contentInset.top { + return self.scrollNode.view + } + } + currentParent = currentParent?.superview + } + return result + } +} + +public final class PeerInfoScreen: ViewController { + private let context: AccountContext + private let peerId: PeerId + private let avatarInitiallyExpanded: Bool + private let isOpenedFromChat: Bool + private let nearbyPeer: Bool + private let callMessages: [Message] + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + fileprivate var controllerNode: PeerInfoScreenNode { + return self.displayNode as! PeerInfoScreenNode + } + + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + + private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? + + public init(context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeer: Bool, callMessages: [Message]) { + self.context = context + self.peerId = peerId + self.avatarInitiallyExpanded = avatarInitiallyExpanded + self.isOpenedFromChat = isOpenedFromChat + self.nearbyPeer = nearbyPeer + self.callMessages = callMessages + + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let baseNavigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData) + super.init(navigationBarPresentationData: NavigationBarPresentationData( + theme: NavigationBarTheme( + buttonColor: avatarInitiallyExpanded ? .white : baseNavigationBarPresentationData.theme.buttonColor, + disabledButtonColor: baseNavigationBarPresentationData.theme.disabledButtonColor, + primaryTextColor: baseNavigationBarPresentationData.theme.primaryTextColor, + backgroundColor: .clear, + separatorColor: .clear, + badgeBackgroundColor: baseNavigationBarPresentationData.theme.badgeBackgroundColor, + badgeStrokeColor: baseNavigationBarPresentationData.theme.badgeStrokeColor, + badgeTextColor: baseNavigationBarPresentationData.theme.badgeTextColor + ), strings: baseNavigationBarPresentationData.strings)) + self.navigationBar?.makeCustomTransitionNode = { [weak self] other, isInteractive in + guard let strongSelf = self else { + return nil + } + if strongSelf.navigationItem.leftBarButtonItem != nil { + return nil + } + if other.item?.leftBarButtonItem != nil { + return nil + } + if strongSelf.controllerNode.scrollNode.view.contentOffset.y > .ulpOfOne { + return nil + } + if isInteractive && strongSelf.controllerNode.headerNode.isAvatarExpanded { + return nil + } + if other.contentNode != nil { + return nil + } + if let tag = other.userInfo as? PeerInfoNavigationSourceTag, tag.peerId == peerId { + return PeerInfoNavigationTransitionNode(screenNode: strongSelf.controllerNode, presentationData: strongSelf.presentationData, headerNode: strongSelf.controllerNode.headerNode) + } + return nil + } + + self.statusBar.statusBarStyle = avatarInitiallyExpanded ? .White : self.presentationData.theme.rootController.statusBarStyle.style + + self.scrollToTop = { [weak self] in + self?.controllerNode.scrollToTop() + } + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.controllerNode.updatePresentationData(strongSelf.presentationData) + } + } + }) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + override public func loadDisplayNode() { + self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeer: self.nearbyPeer, callMessages: self.callMessages) + + self._ready.set(self.controllerNode.ready.get()) + + super.displayNodeDidLoad() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let (layout, navigationHeight) = self.validLayout { + self.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + } + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.validLayout = (layout, navigationHeight) + + self.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition) + } +} + +private func getUserPeer(postbox: Postbox, peerId: PeerId) -> Signal<(Peer?, CachedPeerData?), NoError> { + return postbox.transaction { transaction -> (Peer?, CachedPeerData?) in + guard let peer = transaction.getPeer(peerId) else { + return (nil, nil) + } + var resultPeer: Peer? + if let peer = peer as? TelegramSecretChat { + resultPeer = transaction.getPeer(peer.regularPeerId) + } else { + resultPeer = peer + } + return (resultPeer, resultPeer.flatMap({ transaction.getPeerCachedData(peerId: $0.id) })) + } +} + +final class PeerInfoNavigationSourceTag { + let peerId: PeerId + + init(peerId: PeerId) { + self.peerId = peerId + } +} + +private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavigationTransitionNode { + private let screenNode: PeerInfoScreenNode + private let presentationData: PresentationData + + private var topNavigationBar: NavigationBar? + private var bottomNavigationBar: NavigationBar? + private var reverseFraction: Bool = false + + private let headerNode: PeerInfoHeaderNode + + private var previousBackButtonArrow: ASDisplayNode? + private var previousBackButton: ASDisplayNode? + private var currentBackButtonArrow: ASDisplayNode? + private var previousBackButtonBadge: ASDisplayNode? + private var currentBackButton: ASDisplayNode? + + private var previousTitleNode: (ASDisplayNode, TextNode)? + private var previousStatusNode: (ASDisplayNode, ASDisplayNode)? + + private var didSetup: Bool = false + + init(screenNode: PeerInfoScreenNode, presentationData: PresentationData, headerNode: PeerInfoHeaderNode) { + self.screenNode = screenNode + self.presentationData = presentationData + self.headerNode = headerNode + + super.init() + + self.addSubnode(headerNode) + } + + func setup(topNavigationBar: NavigationBar, bottomNavigationBar: NavigationBar) { + if let _ = bottomNavigationBar.userInfo as? PeerInfoNavigationSourceTag { + self.topNavigationBar = topNavigationBar + self.bottomNavigationBar = bottomNavigationBar + } else { + self.topNavigationBar = bottomNavigationBar + self.bottomNavigationBar = topNavigationBar + self.reverseFraction = true + } + + topNavigationBar.isHidden = true + bottomNavigationBar.isHidden = true + + if let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar { + if let previousBackButtonArrow = bottomNavigationBar.makeTransitionBackArrowNode(accentColor: self.presentationData.theme.rootController.navigationBar.accentTextColor) { + self.previousBackButtonArrow = previousBackButtonArrow + self.addSubnode(previousBackButtonArrow) + } + if let previousBackButton = bottomNavigationBar.makeTransitionBackButtonNode(accentColor: self.presentationData.theme.rootController.navigationBar.accentTextColor) { + self.previousBackButton = previousBackButton + self.addSubnode(previousBackButton) + } + if self.screenNode.headerNode.isAvatarExpanded, let currentBackButtonArrow = topNavigationBar.makeTransitionBackArrowNode(accentColor: self.screenNode.headerNode.isAvatarExpanded ? .white : self.presentationData.theme.rootController.navigationBar.accentTextColor) { + self.currentBackButtonArrow = currentBackButtonArrow + self.addSubnode(currentBackButtonArrow) + } + if let previousBackButtonBadge = bottomNavigationBar.makeTransitionBadgeNode() { + self.previousBackButtonBadge = previousBackButtonBadge + self.addSubnode(previousBackButtonBadge) + } + if let currentBackButton = topNavigationBar.makeTransitionBackButtonNode(accentColor: self.screenNode.headerNode.isAvatarExpanded ? .white : self.presentationData.theme.rootController.navigationBar.accentTextColor) { + self.currentBackButton = currentBackButton + self.addSubnode(currentBackButton) + } + if let previousTitleView = bottomNavigationBar.titleView as? ChatTitleView { + let previousTitleNode = previousTitleView.titleNode.makeCopy() + let previousTitleContainerNode = ASDisplayNode() + previousTitleContainerNode.addSubnode(previousTitleNode) + previousTitleNode.frame = previousTitleNode.frame.offsetBy(dx: -previousTitleNode.frame.width / 2.0, dy: -previousTitleNode.frame.height / 2.0) + self.previousTitleNode = (previousTitleContainerNode, previousTitleNode) + self.addSubnode(previousTitleContainerNode) + + let previousStatusNode = previousTitleView.activityNode.makeCopy() + let previousStatusContainerNode = ASDisplayNode() + previousStatusContainerNode.addSubnode(previousStatusNode) + previousStatusNode.frame = previousStatusNode.frame.offsetBy(dx: -previousStatusNode.frame.width / 2.0, dy: -previousStatusNode.frame.height / 2.0) + self.previousStatusNode = (previousStatusContainerNode, previousStatusNode) + self.addSubnode(previousStatusContainerNode) + } + } + } + + func update(containerSize: CGSize, fraction: CGFloat, transition: ContainedViewLayoutTransition) { + guard let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar else { + return + } + + let fraction = self.reverseFraction ? (1.0 - fraction) : fraction + + if let previousBackButtonArrow = self.previousBackButtonArrow { + let previousBackButtonArrowFrame = bottomNavigationBar.backButtonArrow.view.convert(bottomNavigationBar.backButtonArrow.view.bounds, to: bottomNavigationBar.view) + previousBackButtonArrow.frame = previousBackButtonArrowFrame + } + + if let previousBackButton = self.previousBackButton { + let previousBackButtonFrame = bottomNavigationBar.backButtonNode.view.convert(bottomNavigationBar.backButtonNode.view.bounds, to: bottomNavigationBar.view) + previousBackButton.frame = previousBackButtonFrame + transition.updateAlpha(node: previousBackButton, alpha: fraction) + } + + if let currentBackButtonArrow = self.currentBackButtonArrow { + let currentBackButtonArrowFrame = topNavigationBar.backButtonArrow.view.convert(topNavigationBar.backButtonArrow.view.bounds, to: topNavigationBar.view) + currentBackButtonArrow.frame = currentBackButtonArrowFrame + + transition.updateAlpha(node: currentBackButtonArrow, alpha: 1.0 - fraction) + if let previousBackButtonArrow = self.previousBackButtonArrow { + transition.updateAlpha(node: previousBackButtonArrow, alpha: fraction) + } + } + + if let previousBackButtonBadge = self.previousBackButtonBadge { + let previousBackButtonBadgeFrame = bottomNavigationBar.badgeNode.view.convert(bottomNavigationBar.badgeNode.view.bounds, to: bottomNavigationBar.view) + previousBackButtonBadge.frame = previousBackButtonBadgeFrame + + transition.updateAlpha(node: previousBackButtonBadge, alpha: fraction) + } + + if let currentBackButton = self.currentBackButton { + let currentBackButtonFrame = topNavigationBar.backButtonNode.view.convert(topNavigationBar.backButtonNode.view.bounds, to: topNavigationBar.view) + //transition.updateFrame(node: currentBackButton, frame: currentBackButtonFrame.offsetBy(dx: fraction * 12.0, dy: 0.0)) + + transition.updateAlpha(node: currentBackButton, alpha: (1.0 - fraction)) + } + + if let previousTitleView = bottomNavigationBar.titleView as? ChatTitleView, let _ = (bottomNavigationBar.rightButtonNode.singleCustomNode as? ChatAvatarNavigationNode)?.avatarNode, let (previousTitleContainerNode, previousTitleNode) = self.previousTitleNode, let (previousStatusContainerNode, previousStatusNode) = self.previousStatusNode { + let previousTitleFrame = previousTitleView.titleNode.view.convert(previousTitleView.titleNode.bounds, to: bottomNavigationBar.view) + let previousStatusFrame = previousTitleView.activityNode.view.convert(previousTitleView.activityNode.bounds, to: bottomNavigationBar.view) + + self.headerNode.navigationTransition = PeerInfoHeaderNavigationTransition(sourceNavigationBar: bottomNavigationBar, sourceTitleView: previousTitleView, sourceTitleFrame: previousTitleFrame, sourceSubtitleFrame: previousStatusFrame, fraction: fraction) + if let (layout, navigationHeight) = self.screenNode.validLayout { + self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, contentOffset: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, notificationSettings: self.screenNode.data?.notificationSettings, statusData: self.screenNode.data?.status, isContact: self.screenNode.data?.isContact ?? false, state: self.screenNode.state, transition: transition, additive: false) + } + + let titleScale = (fraction * previousTitleNode.bounds.height + (1.0 - fraction) * self.headerNode.titleNodeRawContainer.bounds.height) / previousTitleNode.bounds.height + let subtitleScale = max(0.01, min(10.0, (fraction * previousStatusNode.bounds.height + (1.0 - fraction) * self.headerNode.subtitleNodeRawContainer.bounds.height) / previousStatusNode.bounds.height)) + + transition.updateFrame(node: previousTitleContainerNode, frame: CGRect(origin: self.headerNode.titleNodeRawContainer.frame.center, size: CGSize())) + transition.updateFrame(node: previousTitleNode, frame: CGRect(origin: CGPoint(x: -previousTitleFrame.width / 2.0, y: -previousTitleFrame.height / 2.0), size: previousTitleFrame.size)) + transition.updateFrame(node: previousStatusContainerNode, frame: CGRect(origin: self.headerNode.subtitleNodeRawContainer.frame.center, size: CGSize())) + transition.updateFrame(node: previousStatusNode, frame: CGRect(origin: CGPoint(x: -previousStatusFrame.size.width / 2.0, y: -previousStatusFrame.size.height / 2.0), size: previousStatusFrame.size)) + + transition.updateSublayerTransformScale(node: previousTitleContainerNode, scale: titleScale) + transition.updateSublayerTransformScale(node: previousStatusContainerNode, scale: subtitleScale) + + transition.updateAlpha(node: self.headerNode.titleNode, alpha: (1.0 - fraction)) + transition.updateAlpha(node: previousTitleNode, alpha: fraction) + transition.updateAlpha(node: self.headerNode.subtitleNode, alpha: (1.0 - fraction)) + transition.updateAlpha(node: previousStatusNode, alpha: fraction) + + transition.updateAlpha(node: self.headerNode.navigationButtonContainer, alpha: (1.0 - fraction)) + } + } + + func restore() { + guard let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar else { + return + } + + topNavigationBar.isHidden = false + bottomNavigationBar.isHidden = false + self.headerNode.navigationTransition = nil + self.screenNode.insertSubnode(self.headerNode, aboveSubnode: self.screenNode.scrollNode) + } +} + +private func encodeText(_ string: String, _ key: Int) -> String { + var result = "" + for c in string.unicodeScalars { + result.append(Character(UnicodeScalar(UInt32(Int(c.value) + key))!)) + } + return result +} + +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() { + self.controller.didAppearInContextPreview() + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift index 653c05a846..d2ff44fa00 100644 --- a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift +++ b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift @@ -133,47 +133,56 @@ public class PeerMediaCollectionController: TelegramBaseController { }, sendSticker: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in })) } return false - }, openPeer: { [weak self] id, navigation, _ in - if let strongSelf = self, let id = id, let navigationController = strongSelf.navigationController as? NavigationController { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(id))) - } - }, openPeerMention: { _ in - }, openMessageContextMenu: { [weak self] message, _, _, _, _ in + }, openPeer: { [weak self] id, navigation, _ in + if let strongSelf = self, let id = id, let navigationController = strongSelf.navigationController as? NavigationController { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(id))) + } + }, openPeerMention: { _ in + }, openMessageContextMenu: { [weak self] message, _, _, _, _ in + guard let strongSelf = self else { + return + } + let items = (chatAvailableMessageActionsImpl(postbox: strongSelf.context.account.postbox, accountPeerId: strongSelf.context.account.peerId, messageIds: [message.id]) + |> deliverOnMainQueue).start(next: { actions in var messageIds = Set() messageIds.insert(message.id) if let strongSelf = self, strongSelf.isNodeLoaded { if let message = strongSelf.mediaCollectionDisplayNode.messageForGallery(message.id)?.message { - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.SharedMedia_ViewInChat, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), subject: .message(message.id))) - } - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuForward, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.forwardMessages(messageIds) - } - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuDelete, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.deleteMessages(messageIds) - } - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) + var items: [ActionSheetButtonItem] = [] + + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.SharedMedia_ViewInChat, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), subject: .message(message.id))) + } + })) + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuForward, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.forwardMessages(messageIds) + } + })) + if actions.options.contains(.deleteLocally) || actions.options.contains(.deleteGlobally) { + items.append( ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuDelete, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.deleteMessages(messageIds) + } + })) + } + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() }) ])]) strongSelf.mediaCollectionDisplayNode.view.endEditing(true) strongSelf.present(actionSheet, in: .window(.root)) } } - }, openMessageContextActions: { [weak self] message, node, rect, gesture in + }) + }, openMessageContextActions: { [weak self] message, node, rect, gesture in guard let strongSelf = self else { gesture?.cancel() return @@ -236,7 +245,7 @@ public class PeerMediaCollectionController: TelegramBaseController { c.dismiss(completion: { if let strongSelf = self { strongSelf.updateInterfaceState(animated: true, { $0.withoutSelectionState() }) - let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: Array(messageIds), type: .forEveryone).start() + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start() } }) }))) @@ -255,7 +264,7 @@ public class PeerMediaCollectionController: TelegramBaseController { c.dismiss(completion: { if let strongSelf = self { strongSelf.updateInterfaceState(animated: true, { $0.withoutSelectionState() }) - let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: Array(messageIds), type: .forLocalPeer).start() + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forLocalPeer).start() } }) }))) @@ -273,7 +282,7 @@ public class PeerMediaCollectionController: TelegramBaseController { switch previewData { case let .gallery(gallery): gallery.setHintWillBePresentedInPreviewingContext(true) - let contextController = ContextController(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node)), items: items, reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node)), items: items, reactionItems: [], gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) case .instantPage: break @@ -304,7 +313,7 @@ public class PeerMediaCollectionController: TelegramBaseController { (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(id.peerId), subject: .message(id))) } } - }, clickThroughMessage: { [weak self] in + }, tapMessage: nil, clickThroughMessage: { [weak self] in self?.view.endEditing(true) }, toggleMessagesSelection: { [weak self] ids, value in if let strongSelf = self, strongSelf.isNodeLoaded { @@ -319,7 +328,7 @@ public class PeerMediaCollectionController: TelegramBaseController { }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in - }, openUrl: { [weak self] url, _, external in + }, openUrl: { [weak self] url, _, external, _ in self?.openUrl(url, external: external ?? false) }, shareCurrentLocation: { }, shareAccountContact: { @@ -354,7 +363,7 @@ public class PeerMediaCollectionController: TelegramBaseController { case let .url(url): let canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1 let openText = canOpenIn ? strongSelf.presentationData.strings.Conversation_FileOpenIn : strongSelf.presentationData.strings.Conversation_LinkDialogOpen - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: url), ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in @@ -384,7 +393,7 @@ public class PeerMediaCollectionController: TelegramBaseController { } }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -403,7 +412,8 @@ public class PeerMediaCollectionController: TelegramBaseController { }, requestRedeliveryOfFailedMessages: { _ in }, addContact: { _ in }, rateCall: { _, _ in - }, requestSelectMessagePollOption: { _, _ in + }, requestSelectMessagePollOptions: { _, _ in + }, requestOpenMessagePollResults: { _, _ in }, openAppStorePage: { }, displayMessageTooltip: { _, _, _, _ in }, seekToTimecode: { _, _, _ in @@ -414,6 +424,9 @@ public class PeerMediaCollectionController: TelegramBaseController { }, updateMessageReaction: { _, _ in }, openMessageReactions: { _ in }, displaySwipeToReplyHint: { + }, dismissReplyMarkupMessage: { _ in + }, openMessagePollResults: { _, _ in + }, openPollCreation: { _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, @@ -526,7 +539,7 @@ public class PeerMediaCollectionController: TelegramBaseController { }, openLinkEditing: { }, reportPeerIrrelevantGeoLocation: { }, displaySlowmodeTooltip: { _, _ in - }, displaySendMessageOptions: { + }, displaySendMessageOptions: { _, _ in }, openScheduledMessages: { }, displaySearchResultsTooltip: { _, _ in }, statuses: nil) @@ -745,8 +758,8 @@ public class PeerMediaCollectionController: TelegramBaseController { strongSelf.navigationActionDisposable.set((strongSelf.context.account.postbox.loadedPeerWithId(peerId) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peer in - if let strongSelf = self, peer.restrictionText(platform: "ios") == nil { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + if let strongSelf = self, peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) } } @@ -765,7 +778,7 @@ public class PeerMediaCollectionController: TelegramBaseController { self?.present(c, in: .window(.root), with: a) }, dismissInput: { self?.view.endEditing(true) - }) + }, contentContext: nil) } })) } @@ -861,7 +874,7 @@ public class PeerMediaCollectionController: TelegramBaseController { if !messageIds.isEmpty { self.messageContextDisposable.set((combineLatest(self.context.sharedContext.chatAvailableMessageActions(postbox: self.context.account.postbox, accountPeerId: self.context.account.peerId, messageIds: messageIds), self.peer.get() |> take(1)) |> deliverOnMainQueue).start(next: { [weak self] actions, peer in if let strongSelf = self, let peer = peer, !actions.options.isEmpty { - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] var personalPeerName: String? var isChannel = false @@ -884,7 +897,7 @@ public class PeerMediaCollectionController: TelegramBaseController { actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateInterfaceState(animated: true, { $0.withoutSelectionState() }) - let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: Array(messageIds), type: .forEveryone).start() + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start() } })) } @@ -901,12 +914,12 @@ public class PeerMediaCollectionController: TelegramBaseController { actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateInterfaceState(animated: true, { $0.withoutSelectionState() }) - let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: Array(messageIds), type: .forLocalPeer).start() + let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forLocalPeer).start() } })) } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) diff --git a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionControllerNode.swift b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionControllerNode.swift index f83818d8fd..a500d42003 100644 --- a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionControllerNode.swift @@ -29,7 +29,7 @@ private func historyNodeImplForMode(_ mode: PeerMediaCollectionMode, context: Ac } return node case .file: - let node = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: .file, subject: messageId.flatMap { .message($0) }, controllerInteraction: controllerInteraction, selectedMessages: selectedMessages, mode: .list(search: true, reversed: false)) + let node = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: .file, subject: messageId.flatMap { .message($0) }, controllerInteraction: controllerInteraction, selectedMessages: selectedMessages, mode: .list(search: true, reversed: false, displayHeaders: .all)) node.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor node.didEndScrolling = { [weak node] in guard let node = node else { @@ -40,7 +40,7 @@ private func historyNodeImplForMode(_ mode: PeerMediaCollectionMode, context: Ac node.preloadPages = true return node case .music: - let node = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: .music, subject: messageId.flatMap { .message($0) }, controllerInteraction: controllerInteraction, selectedMessages: selectedMessages, mode: .list(search: true, reversed: false)) + let node = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: .music, subject: messageId.flatMap { .message($0) }, controllerInteraction: controllerInteraction, selectedMessages: selectedMessages, mode: .list(search: true, reversed: false, displayHeaders: .all)) node.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor node.didEndScrolling = { [weak node] in guard let node = node else { @@ -51,7 +51,7 @@ private func historyNodeImplForMode(_ mode: PeerMediaCollectionMode, context: Ac node.preloadPages = true return node case .webpage: - let node = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: .webPage, subject: messageId.flatMap { .message($0) }, controllerInteraction: controllerInteraction, selectedMessages: selectedMessages, mode: .list(search: true, reversed: false)) + let node = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: .webPage, subject: messageId.flatMap { .message($0) }, controllerInteraction: controllerInteraction, selectedMessages: selectedMessages, mode: .list(search: true, reversed: false, displayHeaders: .all)) node.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor node.didEndScrolling = { [weak node] in guard let node = node else { @@ -155,7 +155,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { self.historyEmptyNode = PeerMediaCollectionEmptyNode(mode: self.mediaCollectionInterfaceState.mode, theme: self.presentationData.theme, strings: self.presentationData.strings) self.historyEmptyNode.isHidden = true - self.chatPresentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: .defaultValue, fontSize: self.presentationData.fontSize, accountPeerId: context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(self.peerId), isScheduledMessages: false) + self.chatPresentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: .defaultValue, fontSize: self.presentationData.listsFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(self.peerId), isScheduledMessages: false) super.init() @@ -258,7 +258,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { if let selectionPanel = self.selectionPanel { selectionPanel.selectedMessages = selectionState.selectedIds - let panelHeight = selectionPanel.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: 0.0, transition: transition, interfaceState: interfaceState, metrics: layout.metrics) + let panelHeight = selectionPanel.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: 0.0, isSecondary: false, transition: transition, interfaceState: interfaceState, metrics: layout.metrics) transition.updateFrame(node: selectionPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight))) if let selectionPanelSeparatorNode = self.selectionPanelSeparatorNode { transition.updateFrame(node: selectionPanelSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - panelHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) @@ -273,12 +273,12 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { self.addSubnode(selectionPanelBackgroundNode) self.selectionPanelBackgroundNode = selectionPanelBackgroundNode - let selectionPanel = ChatMessageSelectionInputPanelNode(theme: self.chatPresentationInterfaceState.theme, strings: self.chatPresentationInterfaceState.strings) + let selectionPanel = ChatMessageSelectionInputPanelNode(theme: self.chatPresentationInterfaceState.theme, strings: self.chatPresentationInterfaceState.strings, peerMedia: true) selectionPanel.context = self.context selectionPanel.backgroundColor = self.presentationData.theme.chat.inputPanel.panelBackgroundColor selectionPanel.interfaceInteraction = self.interfaceInteraction selectionPanel.selectedMessages = selectionState.selectedIds - let panelHeight = selectionPanel.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: 0.0, transition: .immediate, interfaceState: interfaceState, metrics: layout.metrics) + let panelHeight = selectionPanel.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: 0.0, isSecondary: false, transition: .immediate, interfaceState: interfaceState, metrics: layout.metrics) self.selectionPanel = selectionPanel self.addSubnode(selectionPanel) @@ -312,21 +312,6 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { } } - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - let previousBounds = self.historyNode.bounds self.historyNode.bounds = CGRect(x: previousBounds.origin.x, y: previousBounds.origin.y, width: layout.size.width, height: layout.size.height) self.historyNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) @@ -336,21 +321,15 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { self.historyEmptyNode.updateLayout(size: layout.size, insets: vanillaInsets, transition: transition, interfaceState: mediaCollectionInterfaceState) transition.updateFrame(node: self.historyEmptyNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } var additionalBottomInset: CGFloat = 0.0 if let selectionPanel = self.selectionPanel { additionalBottomInset = selectionPanel.bounds.size.height } + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) listViewTransaction(ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: insets.top, left: - insets.right + layout.safeInsets.right, bottom: insets.bottom + additionalBottomInset, right: insets.left + layout.safeInsets.right), duration: duration, curve: listViewCurve)) + insets.right + layout.safeInsets.right, bottom: insets.bottom + additionalBottomInset, right: insets.left + layout.safeInsets.right), duration: duration, curve: curve)) if let (candidateHistoryNode, _) = self.candidateHistoryNode { let previousBounds = candidateHistoryNode.bounds @@ -358,7 +337,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { candidateHistoryNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) (candidateHistoryNode as! ChatHistoryNode).updateLayout(transition: transition, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: insets.top, left: - insets.right + layout.safeInsets.right, bottom: insets.bottom + additionalBottomInset, right: insets.left + layout.safeInsets.left), duration: duration, curve: listViewCurve)) + insets.right + layout.safeInsets.right, bottom: insets.bottom + additionalBottomInset, right: insets.left + layout.safeInsets.left), duration: duration, curve: curve)) } } @@ -529,14 +508,14 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { return nil } - func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? { + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if let searchContentNode = self.searchDisplayController?.contentNode as? ChatHistorySearchContainerNode { if let transitionNode = searchContentNode.transitionNodeForGallery(messageId: messageId, media: media) { return transitionNode } } - var transitionNode: (ASDisplayNode, () -> (UIView?, UIView?))? + var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? self.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { if let result = itemNode.transitionNode(id: messageId, media: media) { diff --git a/submodules/TelegramUI/TelegramUI/PeerSelectionController.swift b/submodules/TelegramUI/TelegramUI/PeerSelectionController.swift index feb988676a..994cab2b6b 100644 --- a/submodules/TelegramUI/TelegramUI/PeerSelectionController.swift +++ b/submodules/TelegramUI/TelegramUI/PeerSelectionController.swift @@ -22,6 +22,8 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon public var peerSelected: ((PeerId) -> Void)? private let filter: ChatListNodePeersFilter + private let attemptSelection: ((Peer) -> Void)? + public var inProgress: Bool = false { didSet { if self.inProgress != oldValue { @@ -58,6 +60,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon self.filter = params.filter self.hasContactSelector = params.hasContactSelector self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + self.attemptSelection = params.attemptSelection super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) @@ -139,6 +142,12 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon } } + self.peerSelectionNode.requestOpenDisabledPeer = { [weak self] peer in + if let strongSelf = self { + strongSelf.attemptSelection?(peer) + } + } + self.peerSelectionNode.requestOpenPeerFromSearch = { [weak self] peer in if let strongSelf = self { let storedPeer = strongSelf.context.account.postbox.transaction { transaction -> Void in diff --git a/submodules/TelegramUI/TelegramUI/PeerSelectionControllerNode.swift b/submodules/TelegramUI/TelegramUI/PeerSelectionControllerNode.swift index a8236e890c..45938193a8 100644 --- a/submodules/TelegramUI/TelegramUI/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/PeerSelectionControllerNode.swift @@ -47,6 +47,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { var requestActivateSearch: (() -> Void)? var requestDeactivateSearch: (() -> Void)? var requestOpenPeer: ((PeerId) -> Void)? + var requestOpenDisabledPeer: ((Peer) -> Void)? var requestOpenPeerFromSearch: ((Peer) -> Void)? var requestOpenMessageFromSearch: ((Peer, MessageId) -> Void)? @@ -84,8 +85,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.segmentedControlNode = nil } - - self.chatListNode = ChatListNode(context: context, groupId: .root, controlsHistoryPreload: false, mode: .peers(filter: filter), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations) + self.chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, controlsHistoryPreload: false, mode: .peers(filter: filter), theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations) super.init() @@ -103,6 +103,10 @@ final class PeerSelectionControllerNode: ASDisplayNode { self?.requestOpenPeer?(peerId) } + self.chatListNode.disabledPeerSelected = { [weak self] peer in + self?.requestOpenDisabledPeer?(peer) + } + self.chatListNode.contentOffsetChanged = { [weak self] offset in self?.contentOffsetChanged?(offset) } @@ -145,7 +149,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { private func updateThemeAndStrings() { self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.searchDisplayController?.updatePresentationData(self.presentationData) - self.chatListNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations) + self.chatListNode.updateThemeAndStrings(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations) self.toolbarBackgroundNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor self.toolbarSeparatorNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor @@ -183,29 +187,8 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.chatListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.chatListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: listViewCurve) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: curve) self.chatListNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) @@ -233,6 +216,8 @@ final class PeerSelectionControllerNode: ASDisplayNode { if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { requestOpenPeerFromSearch(peer) } + }, openDisabledPeer: { [weak self] peer in + self?.requestOpenDisabledPeer?(peer) }, openRecentPeerOptions: { _ in }, openMessage: { [weak self] peer, messageId in if let requestOpenMessageFromSearch = self?.requestOpenMessageFromSearch { diff --git a/submodules/TelegramUI/TelegramUI/PollResultsController.swift b/submodules/TelegramUI/TelegramUI/PollResultsController.swift new file mode 100644 index 0000000000..4a7e627a71 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PollResultsController.swift @@ -0,0 +1,382 @@ +import Foundation +import Postbox +import SyncCore +import TelegramCore +import SwiftSignalKit +import TelegramPresentationData +import AccountContext +import ItemListUI +import Display +import ItemListPeerItem +import ItemListPeerActionItem + +private let collapsedResultCount: Int = 10 +private let collapsedInitialLimit: Int = 14 + +private final class PollResultsControllerArguments { + let context: AccountContext + let collapseOption: (Data) -> Void + let expandOption: (Data) -> Void + let openPeer: (RenderedPeer) -> Void + + init(context: AccountContext, collapseOption: @escaping (Data) -> Void, expandOption: @escaping (Data) -> Void, openPeer: @escaping (RenderedPeer) -> Void) { + self.context = context + self.collapseOption = collapseOption + self.expandOption = expandOption + self.openPeer = openPeer + } +} + +private enum PollResultsSection { + case text + case option(Int) + + var rawValue: Int32 { + switch self { + case .text: + return 0 + case let .option(index): + return 1 + Int32(index) + } + } +} + +private enum PollResultsEntryId: Hashable { + case text + case optionPeer(Int, Int) + case optionExpand(Int) +} + +private enum PollResultsItemTag: ItemListItemTag, Equatable { + case firstOptionPeer(opaqueIdentifier: Data) + + func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? PollResultsItemTag, self == other { + return true + } else { + return false + } + } +} + +private enum PollResultsEntry: ItemListNodeEntry { + case text(String) + case optionPeer(optionId: Int, index: Int, peer: RenderedPeer, optionText: String, optionAdditionalText: String, optionCount: Int32, optionExpanded: Bool, opaqueIdentifier: Data, shimmeringAlternation: Int?, isFirstInOption: Bool) + case optionExpand(optionId: Int, opaqueIdentifier: Data, text: String, enabled: Bool) + + var section: ItemListSectionId { + switch self { + case .text: + return PollResultsSection.text.rawValue + case let .optionPeer(optionPeer): + return PollResultsSection.option(optionPeer.optionId).rawValue + case let .optionExpand(optionExpand): + return PollResultsSection.option(optionExpand.optionId).rawValue + } + } + + var stableId: PollResultsEntryId { + switch self { + case .text: + return .text + case let .optionPeer(optionPeer): + return .optionPeer(optionPeer.optionId, optionPeer.index) + case let .optionExpand(optionExpand): + return .optionExpand(optionExpand.optionId) + } + } + + static func <(lhs: PollResultsEntry, rhs: PollResultsEntry) -> Bool { + switch lhs { + case .text: + switch rhs { + case .text: + return false + default: + return true + } + case let .optionPeer(lhsOptionPeer): + switch rhs { + case .text: + return false + case let .optionPeer(rhsOptionPeer): + if lhsOptionPeer.optionId == rhsOptionPeer.optionId { + return lhsOptionPeer.index < rhsOptionPeer.index + } else { + return lhsOptionPeer.optionId < rhsOptionPeer.optionId + } + case let .optionExpand(rhsOptionExpand): + if lhsOptionPeer.optionId == rhsOptionExpand.optionId { + return true + } else { + return lhsOptionPeer.optionId < rhsOptionExpand.optionId + } + } + case let .optionExpand(lhsOptionExpand): + switch rhs { + case .text: + return false + case let .optionPeer(rhsOptionPeer): + if lhsOptionExpand.optionId == rhsOptionPeer.optionId { + return false + } else { + return lhsOptionExpand.optionId < rhsOptionPeer.optionId + } + case let .optionExpand(rhsOptionExpand): + if lhsOptionExpand.optionId == rhsOptionExpand.optionId { + return false + } else { + return lhsOptionExpand.optionId < rhsOptionExpand.optionId + } + } + } + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! PollResultsControllerArguments + switch self { + case let .text(text): + return ItemListTextItem(presentationData: presentationData, text: .large(text), sectionId: self.section) + case let .optionPeer(optionId, _, peer, optionText, optionAdditionalText, optionCount, optionExpanded, opaqueIdentifier, shimmeringAlternation, isFirstInOption): + let header = ItemListPeerItemHeader(theme: presentationData.theme, strings: presentationData.strings, text: optionText, additionalText: optionAdditionalText, actionTitle: optionExpanded ? presentationData.strings.PollResults_Collapse : presentationData.strings.MessagePoll_VotedCount(optionCount), id: Int64(optionId), action: optionExpanded ? { + arguments.collapseOption(opaqueIdentifier) + } : nil) + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .dayFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: ""), nameDisplayOrder: .firstLast, context: arguments.context, peer: peer.peers[peer.peerId]!, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: shimmeringAlternation == nil, sectionId: self.section, action: { + arguments.openPeer(peer) + }, setPeerIdWithRevealedOptions: { _, _ in + }, removePeer: { _ in + }, noInsets: true, tag: isFirstInOption ? PollResultsItemTag.firstOptionPeer(opaqueIdentifier: opaqueIdentifier) : nil, header: header, shimmering: shimmeringAlternation.flatMap { ItemListPeerItemShimmering(alternationIndex: $0) }) + case let .optionExpand(_, opaqueIdentifier, text, enabled): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(presentationData.theme), title: text, sectionId: self.section, editing: false, action: enabled ? { + arguments.expandOption(opaqueIdentifier) + } : nil) + } + } +} + +private struct PollResultsControllerState: Equatable { + var expandedOptions: [Data: Int] = [:] +} + +private func pollResultsControllerEntries(presentationData: PresentationData, poll: TelegramMediaPoll, state: PollResultsControllerState, resultsState: PollResultsState) -> [PollResultsEntry] { + var entries: [PollResultsEntry] = [] + + var isEmpty = false + for (_, optionState) in resultsState.options { + if !optionState.hasLoadedOnce { + isEmpty = true + break + } + } + + entries.append(.text(poll.text)) + + var optionVoterCount: [Int: Int32] = [:] + let totalVoterCount = poll.results.totalVoters ?? 0 + var optionPercentage: [Int] = [] + + if totalVoterCount != 0 { + if let voters = poll.results.voters, let totalVoters = poll.results.totalVoters { + for i in 0 ..< poll.options.count { + inner: for optionVoters in voters { + if optionVoters.opaqueIdentifier == poll.options[i].opaqueIdentifier { + optionVoterCount[i] = optionVoters.count + break inner + } + } + } + } + + optionPercentage = countNicePercent(votes: (0 ..< poll.options.count).map({ Int(optionVoterCount[$0] ?? 0) }), total: Int(totalVoterCount)) + } + + for i in 0 ..< poll.options.count { + let percentage = optionPercentage.count > i ? optionPercentage[i] : 0 + let option = poll.options[i] + let optionTextHeader = option.text.uppercased() + let optionAdditionalTextHeader = " — \(percentage)%" + if isEmpty { + if let voterCount = optionVoterCount[i], voterCount != 0 { + let displayCount: Int + if Int(voterCount) > collapsedInitialLimit { + displayCount = collapsedResultCount + } else { + displayCount = Int(voterCount) + } + for peerIndex in 0 ..< displayCount { + let fakeUser = TelegramUser(id: PeerId(namespace: -1, id: 0), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + let peer = RenderedPeer(peer: fakeUser) + entries.append(.optionPeer(optionId: i, index: peerIndex, peer: peer, optionText: optionTextHeader, optionAdditionalText: optionAdditionalTextHeader, optionCount: voterCount, optionExpanded: false, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: peerIndex % 2, isFirstInOption: peerIndex == 0)) + } + if displayCount < Int(voterCount) { + let remainingCount = Int(voterCount) - displayCount + entries.append(.optionExpand(optionId: i, opaqueIdentifier: option.opaqueIdentifier, text: presentationData.strings.PollResults_ShowMore(Int32(remainingCount)), enabled: false)) + } + } + } else { + if let optionState = resultsState.options[option.opaqueIdentifier], !optionState.peers.isEmpty { + var hasMore = false + let optionExpandedAtCount = state.expandedOptions[option.opaqueIdentifier] + + let peers = optionState.peers + let count = optionState.count + + let displayCount: Int + if peers.count > collapsedInitialLimit + 1 { + if optionExpandedAtCount != nil { + displayCount = peers.count + } else { + displayCount = collapsedResultCount + } + } else { + if let optionExpandedAtCount = optionExpandedAtCount { + if optionExpandedAtCount == collapsedInitialLimit + 1 && optionState.canLoadMore { + displayCount = collapsedResultCount + } else { + displayCount = peers.count + } + } else { + if !optionState.canLoadMore { + displayCount = peers.count + } else { + displayCount = collapsedResultCount + } + } + } + + var peerIndex = 0 + inner: for peer in peers { + if peerIndex >= displayCount { + break inner + } + entries.append(.optionPeer(optionId: i, index: peerIndex, peer: peer, optionText: optionTextHeader, optionAdditionalText: optionAdditionalTextHeader, optionCount: Int32(count), optionExpanded: optionExpandedAtCount != nil, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: nil, isFirstInOption: peerIndex == 0)) + peerIndex += 1 + } + + let remainingCount = count - peerIndex + if remainingCount > 0 { + entries.append(.optionExpand(optionId: i, opaqueIdentifier: option.opaqueIdentifier, text: presentationData.strings.PollResults_ShowMore(Int32(remainingCount)), enabled: true)) + } + } + } + } + + return entries +} + +public func pollResultsController(context: AccountContext, messageId: MessageId, poll: TelegramMediaPoll, focusOnOptionWithOpaqueIdentifier: Data? = nil) -> ViewController { + let statePromise = ValuePromise(PollResultsControllerState(), ignoreRepeated: true) + let stateValue = Atomic(value: PollResultsControllerState()) + let updateState: ((PollResultsControllerState) -> PollResultsControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var pushControllerImpl: ((ViewController) -> Void)? + var dismissImpl: (() -> Void)? + + let actionsDisposable = DisposableSet() + + let resultsContext = PollResultsContext(account: context.account, messageId: messageId, poll: poll) + + let arguments = PollResultsControllerArguments(context: context, + collapseOption: { optionId in + updateState { state in + var state = state + state.expandedOptions.removeValue(forKey: optionId) + return state + } + }, expandOption: { optionId in + let _ = (resultsContext.state + |> take(1) + |> deliverOnMainQueue).start(next: { [weak resultsContext] state in + if let optionState = state.options[optionId] { + updateState { state in + var state = state + state.expandedOptions[optionId] = optionState.peers.count + return state + } + + if optionState.canLoadMore { + resultsContext?.loadMore(optionOpaqueIdentifier: optionId) + } + } + }) + }, openPeer: { peer in + if let peer = peer.peers[peer.peerId] { + if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { + pushControllerImpl?(controller) + } + } + }) + + let previousWasEmpty = Atomic(value: nil) + + let signal = combineLatest(queue: .mainQueue(), + context.sharedContext.presentationData, + statePromise.get(), + resultsContext.state + ) + |> map { presentationData, state, resultsState -> (ItemListControllerState, (ItemListNodeState, Any)) in + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Close), style: .regular, enabled: true, action: { + dismissImpl?() + }) + + var isEmpty = false + for (_, optionState) in resultsState.options { + if !optionState.hasLoadedOnce { + isEmpty = true + break + } + } + + let previousWasEmptyValue = previousWasEmpty.swap(isEmpty) + + var totalVoters: Int32 = 0 + if let totalVotersValue = poll.results.totalVoters { + totalVoters = totalVotersValue + } + + let entries = pollResultsControllerEntries(presentationData: presentationData, poll: poll, state: state, resultsState: resultsState) + + var initialScrollToItem: ListViewScrollToItem? + if let focusOnOptionWithOpaqueIdentifier = focusOnOptionWithOpaqueIdentifier, previousWasEmptyValue == nil { + var isFirstOption = true + loop: for i in 0 ..< entries.count { + switch entries[i] { + case let .optionPeer(optionPeer): + if optionPeer.opaqueIdentifier == focusOnOptionWithOpaqueIdentifier { + if !isFirstOption { + initialScrollToItem = ListViewScrollToItem(index: i, position: .top(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Down) + } + break loop + } + isFirstOption = false + default: + break + } + } + } + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .textWithSubtitle(presentationData.strings.PollResults_Title, presentationData.strings.MessagePoll_VotedCount(totalVoters)), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, focusItemTag: nil, emptyStateItem: nil, initialScrollToItem: initialScrollToItem, crossfadeState: previousWasEmptyValue != nil && previousWasEmptyValue == true && isEmpty == false, animateChanges: false) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(context: context, state: signal) + controller.navigationPresentation = .modal + pushControllerImpl = { [weak controller] c in + controller?.push(c) + } + dismissImpl = { [weak controller] in + controller?.dismiss() + } + controller.isOpaqueWhenInOverlay = true + controller.blocksBackgroundWhenInOverlay = true + + return controller +} + diff --git a/submodules/TelegramUI/TelegramUI/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/TelegramUI/ReplyAccessoryPanelNode.swift index d211cf6809..9fd3dc2846 100644 --- a/submodules/TelegramUI/TelegramUI/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/ReplyAccessoryPanelNode.swift @@ -79,7 +79,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { authorName = author.displayTitle(strings: strings, displayOrder: nameDisplayOrder) } if let message = message { - (text, _) = descriptionStringForMessage(message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: context.account.peerId) + (text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: context.account.peerId) } var updatedMediaReference: AnyMediaReference? @@ -145,7 +145,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { let isMedia: Bool if let message = message { - switch messageContentKind(message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: context.account.peerId) { + switch messageContentKind(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: context.account.peerId) { case .text: isMedia = false default: @@ -228,7 +228,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { let textRightInset: CGFloat = 20.0 let closeButtonSize = CGSize(width: 44.0, height: bounds.height) - let closeButtonFrame = CGRect(origin: CGPoint(x: bounds.width - rightInset - closeButtonSize.width + 12.0, y: 2.0), size: closeButtonSize) + let closeButtonFrame = CGRect(origin: CGPoint(x: bounds.width - rightInset - closeButtonSize.width + 16.0, y: 2.0), size: closeButtonSize) self.closeButton.frame = closeButtonFrame self.actionArea.frame = CGRect(origin: CGPoint(x: leftInset, y: 2.0), size: CGSize(width: closeButtonFrame.minX - leftInset, height: bounds.height)) diff --git a/submodules/TelegramUI/TelegramUI/Resources/Animations/anim_qr.json b/submodules/TelegramUI/TelegramUI/Resources/Animations/anim_qr.json new file mode 100644 index 0000000000..a82fb40bf9 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/Resources/Animations/anim_qr.json @@ -0,0 +1 @@ +{"v":"5.5.8","fr":60,"ip":0,"op":360,"w":1080,"h":600,"nm":"Animation Lottie","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Done bones","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":306,"s":[0]},{"t":307,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[44.923,28.135,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":307,"s":[{"i":[[-1.89,-1.89],[1.89,-1.89],[0,0],[1.89,1.89],[-1.89,1.89],[0,0]],"o":[[1.89,1.89],[0,0],[-1.89,1.89],[-1.89,-1.89],[0,0],[1.89,-1.89]],"v":[[-14.484,14.361],[-14.484,21.201],[-14.549,21.391],[-21.389,21.391],[-21.389,14.551],[-21.324,14.361]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":318,"s":[{"i":[[-1.89,-1.89],[1.89,-1.89],[0,0],[1.89,1.89],[-1.89,1.89],[0,0]],"o":[[1.89,1.89],[0,0],[-1.89,1.89],[-1.89,-1.89],[0,0],[1.89,-1.89]],"v":[[25.733,-25.797],[25.733,-18.957],[-14.549,21.391],[-21.389,21.391],[-21.389,14.551],[18.893,-25.797]],"c":true}]},{"t":325,"s":[{"i":[[-1.89,-1.89],[1.89,-1.89],[0,0],[1.89,1.89],[-1.89,1.89],[0,0]],"o":[[1.89,1.89],[0,0],[-1.89,1.89],[-1.89,-1.89],[0,0],[1.89,-1.89]],"v":[[21.391,-21.389],[21.391,-14.549],[-14.549,21.391],[-21.389,21.391],[-21.389,14.551],[14.551,-21.389]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.992156862745,0.992156862745,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":297,"op":3897,"st":297,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Done bones 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":296,"s":[0]},{"t":297,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[18.397,37.547,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.65,"y":0},"t":297,"s":[{"i":[[-1.89,1.89],[-1.89,-1.89],[0,0],[1.89,-1.89],[1.89,1.89],[0,0]],"o":[[1.89,-1.89],[0,0],[1.89,1.89],[-1.89,1.89],[0,0],[-1.89,-1.89]],"v":[[-11.977,-11.977],[-5.137,-11.977],[-5.017,-11.992],[-5.017,-5.142],[-11.867,-5.142],[-11.977,-5.137]],"c":true}]},{"t":307,"s":[{"i":[[-1.89,1.89],[-1.89,-1.89],[0,0],[1.89,-1.89],[1.89,1.89],[0,0]],"o":[[1.89,-1.89],[0,0],[1.89,1.89],[-1.89,1.89],[0,0],[-1.89,-1.89]],"v":[[-11.977,-11.977],[-5.137,-11.977],[11.983,5.133],[11.983,11.983],[5.133,11.983],[-11.977,-5.137]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.992156862745,0.992156862745,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":297,"op":3897,"st":297,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Logo","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":152,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":176,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":284,"s":[100]},{"t":285,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":152,"s":[309.679,171.796,0],"to":[-0.371,0.29,0],"ti":[0.371,-0.29,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":176,"s":[307.45,173.539,0],"to":[-0.371,0.29,0],"ti":[0,0,0]},{"t":190,"s":[307.45,173.539,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.827,0.827,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":152,"s":[0,0,100]},{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":182,"s":[110,110,100]},{"i":{"x":[0.662,0.662,0.833],"y":[1,1,1]},"o":{"x":[0.283,0.283,0.167],"y":[0,0,0]},"t":192,"s":[95,95,100]},{"t":200,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-5.19,2.15],[-1.34,0.02],[-0.43,-0.34],[-0.05,-0.27],[0.04,-0.49],[1.18,-6.29],[0.95,-0.09],[2.01,1.3],[3.04,2],[-2.01,2.07],[-0.17,0.75],[0.21,0.18],[0.22,-0.05],[0,0],[1.73,0.04],[1.81,0.58],[-0.15,1.23]],"o":[[0,0],[9.89,-4.1],[0.3,0],[0.36,0.29],[0.04,0.28],[-0.54,5.62],[-0.5,2.66],[-2.07,0.19],[-3.13,-2.05],[-3.51,-2.31],[0.52,-0.55],[0.03,-0.1],[-0.21,-0.19],[-0.47,0.1],[0,0],[-1.27,-0.03],[-2.23,-0.73],[0.12,-0.96]],"v":[[-16.667,-2.222],[4.083,-11.142],[17.363,-15.982],[18.743,-15.572],[19.253,-14.612],[19.313,-13.212],[15.273,12.328],[12.843,15.968],[7.203,13.298],[-0.737,7.968],[0.033,2.318],[9.833,-7.242],[9.673,-7.872],[8.933,-7.942],[-6.077,1.958],[-9.937,3.378],[-15.467,2.078],[-19.317,-0.262]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 3","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Oval for Logo","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":152,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":176,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":284,"s":[100]},{"t":285,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[309.23,171.681,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.827,0.827,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":152,"s":[0,0,100]},{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":176,"s":[110,110,100]},{"i":{"x":[0.662,0.662,0.833],"y":[1,1,1]},"o":{"x":[0.283,0.283,0.167],"y":[0,0,0]},"t":188,"s":[95,95,100]},{"t":196,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[72,72],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.31372498157,0.654901960784,0.917646998985,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Color 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":240,"s":[0]},{"t":250,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[297.586,77.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":302,"s":[100,100,100]},{"t":322,"s":[86,86,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-23.586,-4.5],[23.584,-4.5],[27.584,-0.5],[27.584,0.5],[23.584,4.5],[-23.586,4.5],[-27.586,0.5],[-27.586,-0.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":240,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":250,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 32","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":240,"op":361,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Color 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":240,"s":[0]},{"t":250,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[369,150.552,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0]],"v":[[10,-16.552],[14,-12.552],[14,-11.552],[10,-7.552],[4,-7.552],[4,12.548],[0,16.548],[-1,16.548],[-1.12,16.548],[-10,16.548],[-14,12.548],[-14,2.548],[-10,-1.452],[-5,-1.452],[-5,-12.552],[-1,-16.552]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":240,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":250,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":240,"op":361,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Color 3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":240,"s":[0]},{"t":250,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[387.25,212.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":317,"s":[308.68,170.23,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0]],"v":[[15.75,-11],[19.75,-7],[19.75,7],[15.75,11],[14.75,11],[10.75,7],[10.75,-2],[-15.75,-2],[-19.75,-6],[-19.75,-7],[-15.75,-11]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":240,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":250,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":240,"op":361,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Color 4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":240,"s":[0]},{"t":250,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[256,190.052,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21]],"o":[[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0]],"v":[[-9,-18.052],[-5,-22.052],[5,-22.052],[9,-18.052],[9,-8.052],[5,-4.052],[0,-4.052],[0,13.048],[5,13.048],[9,17.048],[9,18.048],[5,22.048],[-5,22.048],[-9,18.048]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":240,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":250,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":240,"op":361,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Color 5","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":240,"s":[0]},{"t":250,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[238,244,0],"to":[0,0,0],"ti":[0,0,0]},{"t":313,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[6.08,0],[0,0],[0,-6.08],[0,0],[-6.08,0],[0,0],[0,6.08],[0,0]],"o":[[0,0],[-6.08,0],[0,0],[0,6.08],[0,0],[6.08,0],[0,0],[0,-6.08]],"v":[[14,-25],[-14,-25],[-25,-14],[-25,14],[-14,25],[14,25],[25,14],[25,-14]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,-2.14],[0,0],[0,0],[2.14,0],[0,0],[0,0],[0,2.14],[0,0],[0,0],[-2.14,0]],"o":[[0,0],[2.12,0.11],[0,0],[0,0],[-0.11,2.12],[0,0],[0,0],[-2.12,-0.11],[0,0],[0,0],[0.11,-2.12],[0,0]],"v":[[14,-18],[14.2,-18],[18,-14],[18,14],[18,14.2],[14,18],[-14,18],[-14.2,18],[-18,14],[-18,-14],[-18,-14.2],[-14,-18]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":240,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":250,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":240,"op":361,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Color 6","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":240,"s":[0]},{"t":250,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[240,139,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":240,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":250,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 35","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":240,"op":361,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Color 7","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":240,"s":[0]},{"t":250,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[238,244,0],"to":[0,0,0],"ti":[0,0,0]},{"t":313,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-8,-12],[8,-12],[12,-8],[12,8],[8,12],[-8,12],[-12,8],[-12,-8]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":240,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":250,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":240,"op":361,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Color 8","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":14,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":240,"s":[0]},{"t":250,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":148,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[341.5,260,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[341.5,260,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[308.68,170.23,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-5,-9],[5,-9],[9,-5],[9,5],[5,9],[-5,9],[-9,5],[-9,-5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.313725490196,0.654901960784,0.917647058824,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":240,"op":361,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Color 9","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":14,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":255,"s":[0]},{"t":265,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":142,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[238,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[238,98,0],"to":[0,0,0],"ti":[0,0,0]},{"t":313,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-8,-12],[8,-12],[12,-8],[12,8],[8,12],[-8,12],[-12,8],[-12,-8]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":255,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":265,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":1.435,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 5","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":255,"op":361,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Color 10","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":255,"s":[0]},{"t":265,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[238,98,0],"to":[0,0,0],"ti":[0,0,0]},{"t":313,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[6.08,0],[0,0],[0,-6.08],[0,0],[-6.08,0],[0,0],[0,6.08],[0,0]],"o":[[0,0],[-6.08,0],[0,0],[0,6.08],[0,0],[6.08,0],[0,0],[0,-6.08]],"v":[[14,-25],[-14,-25],[-25,-14],[-25,14],[-14,25],[14,25],[25,14],[25,-14]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,-2.14],[0,0],[0,0],[2.14,0],[0,0],[0,0],[0,2.14],[0,0],[0,0],[-2.14,0]],"o":[[0,0],[2.12,0.11],[0,0],[0,0],[-0.11,2.12],[0,0],[0,0],[-2.12,-0.11],[0,0],[0,0],[0.11,-2.12],[0,0]],"v":[[14,-18],[14.2,-18],[18,-14],[18,14],[18,14.2],[14,18],[-14,18],[-14.2,18],[-18,14],[-18,-14],[-18,-14.2],[-14,-18]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":255,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":265,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":255,"op":361,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Color 11","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":255,"s":[0]},{"t":265,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[282,121,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-5,-9],[5,-9],[9,-5],[9,5],[5,9],[-5,9],[-9,5],[-9,-5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":255,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":265,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 26","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":255,"op":361,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Color 12","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":255,"s":[0]},{"t":265,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[258,139,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[308.18,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":255,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":265,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 20","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":255,"op":361,"st":0,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Color 13","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":255,"s":[0]},{"t":265,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[375,231,0],"to":[0,0,0],"ti":[0,0,0]},{"t":320,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":255,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":265,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 37","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":255,"op":361,"st":0,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Color 14","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":255,"s":[0]},{"t":265,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[329.999,106.552,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0]],"v":[[7.001,-16.552],[11.001,-12.552],[11.001,12.548],[7.001,16.548],[6.001,16.548],[2.001,12.548],[2.001,-7.552],[-6.999,-7.552],[-10.999,-11.552],[-10.999,-12.552],[-6.999,-16.552]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":255,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":265,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":255,"op":361,"st":0,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Color 15","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":255,"s":[0]},{"t":265,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[280.999,225.752,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0]],"v":[[5.001,-9.752],[9.001,-5.752],[9.001,-4.752],[5.001,-0.752],[0.001,-0.752],[0.001,5.748],[-3.999,9.748],[-4.999,9.748],[-8.999,5.748],[-8.999,-5.752],[-4.999,-9.752]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":255,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":265,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":255,"op":361,"st":0,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"Color 16","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":255,"s":[0]},{"t":265,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[372,247.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":318,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-11,-4.5],[11,-4.5],[15,-0.5],[15,0.5],[11,4.5],[-11,4.5],[-15,0.5],[-15,-0.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":255,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":265,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":1.297,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":255,"op":361,"st":0,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"Color 17","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":143,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[382,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[382,98,0],"to":[0,0,0],"ti":[0,0,0]},{"t":314,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-8,-12],[8,-12],[12,-8],[12,8],[8,12],[-8,12],[-12,8],[-12,-8]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"Color 18","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":142,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[382,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[382,98,0],"to":[0,0,0],"ti":[0,0,0]},{"t":313,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[6.08,0],[0,0],[0,-6.08],[0,0],[-6.08,0],[0,0],[0,6.08],[0,0]],"o":[[0,0],[-6.08,0],[0,0],[0,6.08],[0,0],[6.08,0],[0,0],[0,-6.08]],"v":[[14,-25],[-14,-25],[-25,-14],[-25,14],[-14,25],[14,25],[25,14],[25,-14]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,-2.14],[0,0],[0,0],[2.14,0],[0,0],[0,0],[0,2.14],[0,0],[0,0],[-2.14,0]],"o":[[0,0],[2.12,0.11],[0,0],[0,0],[-0.11,2.12],[0,0],[0,0],[-2.12,-0.11],[0,0],[0,0],[0.11,-2.12],[0,0]],"v":[[14,-18],[14.2,-18],[18,-14],[18,14],[18,14.2],[14,18],[-14,18],[-14.2,18],[-18,14],[-18,-14],[-18,-14.2],[-14,-18]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":"Color 19","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[286,95,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[286,95,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 21","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"Color 20","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.681,170.929,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[249,148,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[249,148,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.681,170.929,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 25","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":"Color 21","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[233,180,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[233,180,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 19","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":24,"ty":4,"nm":"Color 22","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[308.18,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[384,223,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[384,223,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[308.18,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 14","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":25,"ty":4,"nm":"Color 23","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":148,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[343.5,78.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[343.5,78.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.2,0],[0,0],[0,-2.2],[0,0],[2.2,0],[0,0],[0,2.2],[0,0]],"o":[[0,0],[2.2,0],[0,0],[0,2.2],[0,0],[-2.2,0],[0,0],[0,-2.2]],"v":[[-1.51,-5.5],[1.51,-5.5],[5.5,-1.51],[5.5,1.51],[1.51,5.5],[-1.51,5.5],[-5.5,1.51],[-5.5,-1.51]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 27","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":26,"ty":4,"nm":"Color 24","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":149,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[401.5,183.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[401.5,183.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":320,"s":[308.68,170.23,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.2,0],[0,0],[0,-2.2],[0,0],[2.2,0],[0,0],[0,2.2],[0,0]],"o":[[0,0],[2.2,0],[0,0],[0,2.2],[0,0],[-2.2,0],[0,0],[0,-2.2]],"v":[[-1.51,-5.5],[1.51,-5.5],[5.5,-1.51],[5.5,1.51],[1.51,5.5],[-1.51,5.5],[-5.5,1.51],[-5.5,-1.51]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 24","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":27,"ty":4,"nm":"Color 25","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[276.5,248.517,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[276.5,248.517,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.2,0],[0,0],[0,-2.21],[0,0],[2.2,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.2,0],[0,0],[0,2.21],[0,0],[-2.2,0],[0,0],[0,-2.21]],"v":[[-1.51,-5.517],[1.51,-5.517],[5.5,-1.517],[5.5,1.513],[1.51,5.513],[-1.51,5.513],[-5.5,1.513],[-5.5,-1.517]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 34","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":28,"ty":4,"nm":"Color 26","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[312.5,112,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[312.5,112,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0.11,2.12],[0,0],[0,0],[-2.12,0.11],[0,0],[0,0],[-0.11,-2.12],[0,0],[0,0],[0,0],[-0.11,-2.12],[0,0],[0,0],[2.12,-0.11],[0,0],[0,0]],"o":[[0,0],[-2.14,0],[0,0],[0,0],[0,-2.14],[0,0],[0,0],[2.14,0],[0,0],[0,0],[0,0],[2.14,0],[0,0],[0,0],[0,2.14],[0,0],[0,0],[0,0]],"v":[[6.5,11],[-6.5,11],[-10.5,7.2],[-10.5,7],[-10.5,-7],[-6.7,-11],[-6.5,-11],[-5.5,-11],[-1.5,-7.2],[-1.5,-7],[-1.5,2],[6.5,2],[10.5,5.8],[10.5,6],[10.5,7],[6.7,11],[6.5,11],[-6.5,11]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":29,"ty":4,"nm":"Color 27","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":148,"s":[308.68,170.232,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[224,151.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[224,151.552,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[308.68,170.232,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0]],"v":[[-6,-16.552],[-2,-12.552],[-2,-3.832],[7,-3.832],[11,0.168],[11,1.168],[7,5.168],[-2,5.168],[-2,12.548],[-6,16.548],[-7,16.548],[-11,12.548],[-11,-12.552],[-7,-16.552]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":30,"ty":4,"nm":"Color 28","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":148,"s":[309.68,171.682,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[398,151.828,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[398,151.828,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.68,171.682,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[2.12,-0.11],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21]],"o":[[0,2.15],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0]],"v":[[9,12.548],[5.2,16.548],[5,16.548],[-5,16.548],[-9,12.548],[-9,11.548],[-5,7.548],[0,7.548],[0,-12.552],[4,-16.552],[5,-16.552],[9,-12.552]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":31,"ty":4,"nm":"Color 29","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":148,"s":[309.68,171.682,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[223,195.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[223,195.552,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.68,171.682,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0]],"o":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"v":[[-6,16.548],[-10,12.548],[-10,-12.552],[-6,-16.552],[-5,-16.552],[-1,-12.552],[-1,-1.452],[6,-1.452],[10,2.548],[10,12.548],[6,16.548]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":32,"ty":4,"nm":"Color 30","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[372.5,183.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[372.5,183.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[308.68,170.23,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-10.5,-4.5],[10.5,-4.5],[14.5,-0.5],[14.5,0.5],[10.5,4.5],[-10.5,4.5],[-14.5,0.5],[-14.5,-0.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 40","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":33,"ty":4,"nm":"Color 31","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[313.498,227.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[313.498,227.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.11,2.12],[0,0],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0]],"o":[[-2.14,0],[0,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"v":[[-9.498,9.5],[-13.498,5.7],[-13.498,5.5],[-13.498,4.5],[-9.498,0.5],[2.462,0.5],[2.462,-5.5],[6.462,-9.5],[9.502,-9.5],[13.502,-5.5],[13.502,5.5],[9.502,9.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":284,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":34,"ty":4,"nm":"Color 32","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[346.5,218,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[346.5,218,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[2.12,-0.11],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21]],"o":[[0,2.14],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0]],"v":[[10.5,7],[6.7,11],[6.5,11],[-6.5,11],[-10.5,7],[-10.5,6],[-6.5,2],[1.5,2],[1.5,-7],[5.5,-11],[6.5,-11],[10.5,-7]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":35,"ty":4,"nm":"Color 33","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":147,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[296.5,260,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[296.5,260,0],"to":[0,0,0],"ti":[0,0,0]},{"t":318,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":302,"s":[100,100,100]},{"t":322,"s":[90,90,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0]],"v":[[22.5,-9],[26.5,-5],[26.5,-4],[22.5,0],[16.5,0],[16.5,5],[12.5,9],[-22.5,9],[-26.5,5],[-26.5,4],[-22.5,0],[-1.5,0],[-1.5,-5],[2.5,-9]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":36,"ty":4,"nm":"Color 34","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":14,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[349,135,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[349,135,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 36","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":37,"ty":4,"nm":"Color 35","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0]},{"t":280,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":143,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[384.501,255.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[384.501,255.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":314,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,2.21],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0]],"o":[[-2.21,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"v":[[-18.499,13.5],[-22.499,9.5],[-18.499,5.5],[13.501,5.5],[13.501,-9.5],[17.501,-13.5],[18.501,-13.5],[22.501,-9.5],[22.501,9.5],[18.501,13.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":270,"s":[0.313725490196,0.654901960784,0.917647058824,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[0.313725501299,0.654901981354,0.917647063732,1]},{"t":292,"s":[0.313725490196,0.654901960784,0.917647058824,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":361,"st":0,"bm":0},{"ddd":0,"ind":38,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":143,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[382,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[382,98,0],"to":[0,0,0],"ti":[0,0,0]},{"t":314,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-8,-12],[8,-12],[12,-8],[12,8],[8,12],[-8,12],[-12,8],[-12,-8]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":39,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":142,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[382,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[382,98,0],"to":[0,0,0],"ti":[0,0,0]},{"t":313,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[6.08,0],[0,0],[0,-6.08],[0,0],[-6.08,0],[0,0],[0,6.08],[0,0]],"o":[[0,0],[-6.08,0],[0,0],[0,6.08],[0,0],[6.08,0],[0,0],[0,-6.08]],"v":[[14,-25],[-14,-25],[-25,-14],[-25,14],[-14,25],[14,25],[25,14],[25,-14]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,-2.14],[0,0],[0,0],[2.14,0],[0,0],[0,0],[0,2.14],[0,0],[0,0],[-2.14,0]],"o":[[0,0],[2.12,0.11],[0,0],[0,0],[-0.11,2.12],[0,0],[0,0],[-2.12,-0.11],[0,0],[0,0],[0.11,-2.12],[0,0]],"v":[[14,-18],[14.2,-18],[18,-14],[18,14],[18,14.2],[14,18],[-14,18],[-14.2,18],[-18,14],[-18,-14],[-18,-14.2],[-14,-18]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":40,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":142,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[238,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[238,98,0],"to":[0,0,0],"ti":[0,0,0]},{"t":313,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-8,-12],[8,-12],[12,-8],[12,8],[8,12],[-8,12],[-12,8],[-12,-8]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":1.435,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 5","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":41,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":142,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[238,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[238,98,0],"to":[0,0,0],"ti":[0,0,0]},{"t":313,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[6.08,0],[0,0],[0,-6.08],[0,0],[-6.08,0],[0,0],[0,6.08],[0,0]],"o":[[0,0],[-6.08,0],[0,0],[0,6.08],[0,0],[6.08,0],[0,0],[0,-6.08]],"v":[[14,-25],[-14,-25],[-25,-14],[-25,14],[-14,25],[14,25],[25,14],[25,-14]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,-2.14],[0,0],[0,0],[2.14,0],[0,0],[0,0],[0,2.14],[0,0],[0,0],[-2.14,0]],"o":[[0,0],[2.12,0.11],[0,0],[0,0],[-0.11,2.12],[0,0],[0,0],[-2.12,-0.11],[0,0],[0,0],[0.11,-2.12],[0,0]],"v":[[14,-18],[14.2,-18],[18,-14],[18,14],[18,14.2],[14,18],[-14,18],[-14.2,18],[-18,14],[-18,-14],[-18,-14.2],[-14,-18]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":42,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":142,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[238,244,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[238,244,0],"to":[0,0,0],"ti":[0,0,0]},{"t":313,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[6.08,0],[0,0],[0,-6.08],[0,0],[-6.08,0],[0,0],[0,6.08],[0,0]],"o":[[0,0],[-6.08,0],[0,0],[0,6.08],[0,0],[6.08,0],[0,0],[0,-6.08]],"v":[[14,-25],[-14,-25],[-25,-14],[-25,14],[-14,25],[14,25],[25,14],[25,-14]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,-2.14],[0,0],[0,0],[2.14,0],[0,0],[0,0],[0,2.14],[0,0],[0,0],[-2.14,0]],"o":[[0,0],[2.12,0.11],[0,0],[0,0],[-0.11,2.12],[0,0],[0,0],[-2.12,-0.11],[0,0],[0,0],[0.11,-2.12],[0,0]],"v":[[14,-18],[14.2,-18],[18,-14],[18,14],[18,14.2],[14,18],[-14,18],[-14.2,18],[-18,14],[-18,-14],[-18,-14.2],[-14,-18]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":43,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":142,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[238,244,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[238,244,0],"to":[0,0,0],"ti":[0,0,0]},{"t":313,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-8,-12],[8,-12],[12,-8],[12,8],[8,12],[-8,12],[-12,8],[-12,-8]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":44,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":148,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[341.5,260,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[341.5,260,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[308.68,170.23,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-5,-9],[5,-9],[9,-5],[9,5],[5,9],[-5,9],[-9,5],[-9,-5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":45,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[282,121,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[282,121,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-5,-9],[5,-9],[9,-5],[9,5],[5,9],[-5,9],[-9,5],[-9,-5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 26","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":46,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[286,95,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[286,95,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 21","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":47,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[240,139,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[240,139,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 35","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":48,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[308.18,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[258,139,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[258,139,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[308.18,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 20","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":49,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.681,170.929,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[249,148,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[249,148,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.681,170.929,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 25","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":50,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[349,135,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[349,135,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 36","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":51,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[233,180,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[233,180,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 19","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":52,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":149,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[375,231,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[375,231,0],"to":[0,0,0],"ti":[0,0,0]},{"t":320,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 37","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":53,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[308.18,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[384,223,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[384,223,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[308.18,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 14","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":54,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":148,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[343.5,78.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[343.5,78.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.2,0],[0,0],[0,-2.2],[0,0],[2.2,0],[0,0],[0,2.2],[0,0]],"o":[[0,0],[2.2,0],[0,0],[0,2.2],[0,0],[-2.2,0],[0,0],[0,-2.2]],"v":[[-1.51,-5.5],[1.51,-5.5],[5.5,-1.51],[5.5,1.51],[1.51,5.5],[-1.51,5.5],[-5.5,1.51],[-5.5,-1.51]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 27","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":55,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":149,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[401.5,183.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[401.5,183.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":320,"s":[308.68,170.23,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.2,0],[0,0],[0,-2.2],[0,0],[2.2,0],[0,0],[0,2.2],[0,0]],"o":[[0,0],[2.2,0],[0,0],[0,2.2],[0,0],[-2.2,0],[0,0],[0,-2.2]],"v":[[-1.51,-5.5],[1.51,-5.5],[5.5,-1.51],[5.5,1.51],[1.51,5.5],[-1.51,5.5],[-5.5,1.51],[-5.5,-1.51]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 24","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":56,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[276.5,248.517,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[276.5,248.517,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.2,0],[0,0],[0,-2.21],[0,0],[2.2,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.2,0],[0,0],[0,2.21],[0,0],[-2.2,0],[0,0],[0,-2.21]],"v":[[-1.51,-5.517],[1.51,-5.517],[5.5,-1.517],[5.5,1.513],[1.51,5.513],[-1.51,5.513],[-5.5,1.513],[-5.5,-1.517]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 34","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":57,"ty":4,"nm":"Path 32","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":148,"s":[309.681,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[297.586,77.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[297.586,77.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":302,"s":[100,100,100]},{"t":322,"s":[86,86,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-23.586,-4.5],[23.584,-4.5],[27.584,-0.5],[27.584,0.5],[23.584,4.5],[-23.586,4.5],[-27.586,0.5],[-27.586,-0.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 32","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":58,"ty":4,"nm":"Path 31","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[312.5,112,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[312.5,112,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0.11,2.12],[0,0],[0,0],[-2.12,0.11],[0,0],[0,0],[-0.11,-2.12],[0,0],[0,0],[0,0],[-0.11,-2.12],[0,0],[0,0],[2.12,-0.11],[0,0],[0,0]],"o":[[0,0],[-2.14,0],[0,0],[0,0],[0,-2.14],[0,0],[0,0],[2.14,0],[0,0],[0,0],[0,0],[2.14,0],[0,0],[0,0],[0,2.14],[0,0],[0,0],[0,0]],"v":[[6.5,11],[-6.5,11],[-10.5,7.2],[-10.5,7],[-10.5,-7],[-6.7,-11],[-6.5,-11],[-5.5,-11],[-1.5,-7.2],[-1.5,-7],[-1.5,2],[6.5,2],[10.5,5.8],[10.5,6],[10.5,7],[6.7,11],[6.5,11],[-6.5,11]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":59,"ty":4,"nm":"Path 30","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":148,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[329.999,106.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[329.999,106.552,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0]],"v":[[7.001,-16.552],[11.001,-12.552],[11.001,12.548],[7.001,16.548],[6.001,16.548],[2.001,12.548],[2.001,-7.552],[-6.999,-7.552],[-10.999,-11.552],[-10.999,-12.552],[-6.999,-16.552]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":60,"ty":4,"nm":"Path 29","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":148,"s":[308.68,170.232,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[224,151.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[224,151.552,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[308.68,170.232,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0]],"v":[[-6,-16.552],[-2,-12.552],[-2,-3.832],[7,-3.832],[11,0.168],[11,1.168],[7,5.168],[-2,5.168],[-2,12.548],[-6,16.548],[-7,16.548],[-11,12.548],[-11,-12.552],[-7,-16.552]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":61,"ty":4,"nm":"Path 28","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[369,150.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[369,150.552,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0]],"v":[[10,-16.552],[14,-12.552],[14,-11.552],[10,-7.552],[4,-7.552],[4,12.548],[0,16.548],[-1,16.548],[-1.12,16.548],[-10,16.548],[-14,12.548],[-14,2.548],[-10,-1.452],[-5,-1.452],[-5,-12.552],[-1,-16.552]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":62,"ty":4,"nm":"Path 27","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":148,"s":[309.68,171.682,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[398,151.828,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[398,151.828,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.68,171.682,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[2.12,-0.11],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21]],"o":[[0,2.15],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0]],"v":[[9,12.548],[5.2,16.548],[5,16.548],[-5,16.548],[-9,12.548],[-9,11.548],[-5,7.548],[0,7.548],[0,-12.552],[4,-16.552],[5,-16.552],[9,-12.552]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":63,"ty":4,"nm":"Path 26","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":148,"s":[309.68,171.682,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[223,195.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[223,195.552,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.68,171.682,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0]],"o":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"v":[[-6,16.548],[-10,12.548],[-10,-12.552],[-6,-16.552],[-5,-16.552],[-1,-12.552],[-1,-1.452],[6,-1.452],[10,2.548],[10,12.548],[6,16.548]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":64,"ty":4,"nm":"Path 25","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[256,190.052,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[256,190.052,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21]],"o":[[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0]],"v":[[-9,-18.052],[-5,-22.052],[5,-22.052],[9,-18.052],[9,-8.052],[5,-4.052],[0,-4.052],[0,13.048],[5,13.048],[9,17.048],[9,18.048],[5,22.048],[-5,22.048],[-9,18.048]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":65,"ty":4,"nm":"Path 24","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[372.5,183.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[372.5,183.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[308.68,170.23,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-10.5,-4.5],[10.5,-4.5],[14.5,-0.5],[14.5,0.5],[10.5,4.5],[-10.5,4.5],[-14.5,0.5],[-14.5,-0.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 40","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":66,"ty":4,"nm":"Path 23","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[280.999,225.752,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[280.999,225.752,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0]],"v":[[5.001,-9.752],[9.001,-5.752],[9.001,-4.752],[5.001,-0.752],[0.001,-0.752],[0.001,5.748],[-3.999,9.748],[-4.999,9.748],[-8.999,5.748],[-8.999,-5.752],[-4.999,-9.752]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":67,"ty":4,"nm":"Path 22","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[313.498,227.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[313.498,227.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.11,2.12],[0,0],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0]],"o":[[-2.14,0],[0,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"v":[[-9.498,9.5],[-13.498,5.7],[-13.498,5.5],[-13.498,4.5],[-9.498,0.5],[2.462,0.5],[2.462,-5.5],[6.462,-9.5],[9.502,-9.5],[13.502,-5.5],[13.502,5.5],[9.502,9.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":68,"ty":4,"nm":"Path 21","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[346.5,218,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[346.5,218,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[2.12,-0.11],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21]],"o":[[0,2.14],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0]],"v":[[10.5,7],[6.7,11],[6.5,11],[-6.5,11],[-10.5,7],[-10.5,6],[-6.5,2],[1.5,2],[1.5,-7],[5.5,-11],[6.5,-11],[10.5,-7]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":69,"ty":4,"nm":"Path 20","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":146,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[387.25,212.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[387.25,212.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":317,"s":[308.68,170.23,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0]],"v":[[15.75,-11],[19.75,-7],[19.75,7],[15.75,11],[14.75,11],[10.75,7],[10.75,-2],[-15.75,-2],[-19.75,-6],[-19.75,-7],[-15.75,-11]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":70,"ty":4,"nm":"Path 19","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":147,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[296.5,260,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[296.5,260,0],"to":[0,0,0],"ti":[0,0,0]},{"t":318,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":302,"s":[100,100,100]},{"t":322,"s":[90,90,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0]],"v":[[22.5,-9],[26.5,-5],[26.5,-4],[22.5,0],[16.5,0],[16.5,5],[12.5,9],[-22.5,9],[-26.5,5],[-26.5,4],[-22.5,0],[-1.5,0],[-1.5,-5],[2.5,-9]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":71,"ty":4,"nm":"Path 18","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":147,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[372,247.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[372,247.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":318,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-11,-4.5],[11,-4.5],[15,-0.5],[15,0.5],[11,4.5],[-11,4.5],[-15,0.5],[-15,-0.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":1.297,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":72,"ty":4,"nm":"Path 17","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":143,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[384.501,255.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[384.501,255.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":314,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,2.21],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0]],"o":[[-2.21,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"v":[[-18.499,13.5],[-22.499,9.5],[-18.499,5.5],[13.501,5.5],[13.501,-9.5],[17.501,-13.5],[18.501,-13.5],[22.501,-9.5],[22.501,9.5],[18.501,13.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":302,"st":0,"bm":0}]},{"id":"comp_2","layers":[{"ddd":0,"ind":1,"ty":0,"nm":"QR","refId":"comp_3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":19,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":145,"s":[100]},{"t":146,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":14,"s":[310.143,246.14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":26,"s":[310.143,178.64,0],"to":[0,0,0],"ti":[0,0,0]},{"t":29,"s":[310.143,178.64,0]}],"ix":2},"a":{"a":0,"k":[310,179,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.827,0.827,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":11,"s":[0,0,100]},{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":32,"s":[110,110,100]},{"i":{"x":[0.662,0.662,0.833],"y":[1,1,1]},"o":{"x":[0.283,0.283,0.167],"y":[0,0,0]},"t":42,"s":[95,95,100]},{"t":50,"s":[100,100,100]}],"ix":6}},"ao":0,"w":620,"h":358,"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Laptop","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[310.002,179.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,6.25],[-7.46,0],[-9.09,0],[0,-7.125],[0,0],[0,0],[0,0],[-0.31,-3.06],[3.18,-0.44],[0,0],[0,0],[26.45,-0.8],[0,0],[44.99,-0.21],[0,0],[0,0],[0,0],[23.17,0.62],[0,0],[24.11,2.19],[0,0],[-0.34,3.29],[-3.08,0],[0,0],[0,0]],"o":[[0,-7.812],[6.04,0],[8.133,0],[0,6],[0,0],[0,0],[3.08,0],[0.33,3.22],[0,0],[0,0],[-23.36,2.12],[0,0],[-23.771,0.61],[0,0],[0,0],[0,0],[-44.6,-0.22],[0,0],[-27.41,-0.78],[0,0],[-3.3,-0.34],[0.32,-3.06],[0,0],[0,0],[0,0]],"v":[[-309.615,153.562],[-290.615,146.621],[289.181,146.625],[309.584,153.562],[309.561,164.375],[216.153,159],[303.843,159],[309.594,164.401],[304.683,170.94],[304.463,170.97],[283.663,173.1],[208.943,177.48],[201.693,177.68],[98.553,178.91],[76.213,179],[-76.207,179],[-102.407,178.89],[-204.057,177.62],[-206.377,177.55],[-283.657,173.1],[-304.457,170.97],[-309.567,164.401],[-303.847,159],[-215.847,159],[-309.564,164.468]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[0,5.625],[-8.039,0],[-8.181,0],[0,-7.737],[0,0],[0,0],[0,0],[-0.31,-3.06],[3.18,-0.44],[0,0],[0,0],[26.45,-0.8],[0,0],[44.99,-0.21],[0,0],[0,0],[0,0],[23.17,0.62],[0,0],[24.11,2.19],[0,0],[-0.34,3.29],[-3.08,0],[0,0],[0,0]],"o":[[0,-8.356],[5.436,0],[8.645,0],[0,5.4],[0,0],[0,0],[3.08,0],[0.33,3.22],[0,0],[0,0],[-23.36,2.12],[0,0],[-23.771,0.61],[0,0],[0,0],[0,0],[-44.6,-0.22],[0,0],[-27.41,-0.78],[0,0],[-3.3,-0.34],[0.32,-3.06],[0,0],[0,0],[0,0]],"v":[[-300.407,122.706],[-280.738,114.059],[279.478,114.062],[300.494,122.706],[287.416,132.479],[216.153,159],[303.843,159],[309.594,164.404],[304.683,170.94],[304.463,170.97],[283.663,173.1],[208.943,177.48],[201.693,177.68],[98.553,178.91],[76.213,179],[-76.207,179],[-102.407,178.89],[-204.057,177.62],[-206.377,177.55],[-283.657,173.1],[-304.457,170.97],[-309.567,164.401],[-303.847,159],[-215.847,159],[-287.459,132.471]],"c":true}]},{"t":30,"s":[{"i":[[0,0],[-13.25,0],[0,0],[0,-13.25],[0,0],[0,0],[0,0],[-0.31,-3.06],[3.18,-0.44],[0,0],[0,0],[26.45,-0.8],[0,0],[44.99,-0.21],[0,0],[0,0],[0,0],[23.17,0.62],[0,0],[24.11,2.19],[0,0],[-0.34,3.29],[-3.08,0],[0,0],[0,0]],"o":[[0,-13.25],[0,0],[13.25,0],[0,0],[0,0],[0,0],[3.08,0],[0.33,3.22],[0,0],[0,0],[-23.36,2.12],[0,0],[-23.77,0.61],[0,0],[0,0],[0,0],[-44.6,-0.22],[0,0],[-27.41,-0.78],[0,0],[-3.3,-0.34],[0.32,-3.06],[0,0],[0,0],[0,0]],"v":[[-215.847,-155],[-191.847,-179],[192.153,-179],[216.153,-155],[216.369,-106.207],[216.153,159],[303.843,159],[309.594,164.405],[304.683,170.94],[304.463,170.97],[283.663,173.1],[208.943,177.48],[201.693,177.68],[98.553,178.91],[76.213,179],[-76.207,179],[-102.407,178.89],[-204.057,177.62],[-206.377,177.55],[-283.657,173.1],[-304.457,170.97],[-309.567,164.401],[-303.847,159],[-215.847,159],[-215.631,-106.285]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[-0.059,0],[0,0],[0,0],[-5.92,0.016],[0,0],[-4.904,0],[0,0],[-0.046,0],[5.983,0]],"o":[[-5.98,0],[-0.121,0],[0,0],[4.815,0.031],[0,0],[5.939,0],[0,0],[0,0],[-0.14,0],[0,0]],"v":[[-283.681,152.892],[-300.893,152.898],[-263.947,152.862],[-212.81,152.891],[-197.645,152.891],[197.206,152.935],[212.884,152.937],[263.994,152.941],[300.681,152.906],[283.703,152.938]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[0,0],[0.016,-3.294],[0,0],[0,0],[-5.957,-0.016],[0,0],[-4.429,0.594],[0,0],[0,2.869],[5.94,0.16]],"o":[[-5.98,0],[0,2.964],[0,0],[4.333,0.626],[0,0],[5.943,0],[0,0],[0,0],[0,-2.989],[0,0]],"v":[[-274.597,120.902],[-291.177,123.133],[-257.922,137.911],[-211.89,155.298],[-197.174,156.426],[196.728,156.918],[212.018,155.791],[258.087,138.152],[291.017,123.141],[274.649,120.944]],"c":true}]},{"t":30,"s":[{"i":[[0,0],[0.16,-5.94],[0,0],[0,0],[-5.94,-0.16],[0,0],[-0.16,5.94],[0,0],[0,0],[5.94,0.16]],"o":[[-5.98,0],[0,0],[0,0],[0,5.98],[0,0],[5.98,0],[0,0],[0,0],[0,-5.98],[0,0]],"v":[[-192.847,-167],[-203.847,-156.29],[-203.724,-34.898],[-203.847,139],[-193.137,150],[193.153,150],[204.153,139.29],[204.93,-34.328],[204.153,-156],[193.443,-167]],"c":true}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3630,"st":30,"bm":0}]},{"id":"comp_3","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Logo","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":14,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":130,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":155,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":179,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":302,"s":[100]},{"t":303,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[309.2,173.538,0],"to":[-0.292,0,0],"ti":[0.292,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[307.45,173.538,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[307.45,173.538,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[307.45,173.538,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":155,"s":[307.45,173.538,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":184,"s":[307.45,173.538,0],"to":[0.292,0,0],"ti":[-0.292,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":297,"s":[309.2,173.538,0],"to":[0,0,0],"ti":[0,0,0]},{"t":303,"s":[309.2,173.538,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.827,0.827,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":6,"s":[0,0,100]},{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":40,"s":[110,110,100]},{"i":{"x":[0.662,0.662,0.833],"y":[1,1,1]},"o":{"x":[0.283,0.283,0.167],"y":[0,0,0]},"t":50,"s":[95,95,100]},{"i":{"x":[0.662,0.662,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":58,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,17.667]},"o":{"x":[0.197,0.197,0.197],"y":[0,0,0]},"t":130,"s":[100,100,100]},{"i":{"x":[0.843,0.843,0.843],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,16.667]},"t":136,"s":[0,0,100]},{"i":{"x":[0.827,0.827,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":155,"s":[0,0,100]},{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":184,"s":[110,110,100]},{"i":{"x":[0.662,0.662,0.833],"y":[1,1,1]},"o":{"x":[0.283,0.283,0.167],"y":[0,0,0]},"t":194,"s":[95,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":202,"s":[100,100,100]},{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":286,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.283,0.283,0.167],"y":[0,0,0]},"t":295,"s":[110,110,100]},{"t":303,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-5.19,2.15],[-1.34,0.02],[-0.43,-0.34],[-0.05,-0.27],[0.04,-0.49],[1.18,-6.29],[0.95,-0.09],[2.01,1.3],[3.04,2],[-2.01,2.07],[-0.17,0.75],[0.21,0.18],[0.22,-0.05],[0,0],[1.73,0.04],[1.81,0.58],[-0.15,1.23]],"o":[[0,0],[9.89,-4.1],[0.3,0],[0.36,0.29],[0.04,0.28],[-0.54,5.62],[-0.5,2.66],[-2.07,0.19],[-3.13,-2.05],[-3.51,-2.31],[0.52,-0.55],[0.03,-0.1],[-0.21,-0.19],[-0.47,0.1],[0,0],[-1.27,-0.03],[-2.23,-0.73],[0.12,-0.96]],"v":[[-16.667,-2.222],[4.083,-11.142],[17.363,-15.982],[18.743,-15.572],[19.253,-14.612],[19.313,-13.212],[15.273,12.328],[12.843,15.968],[7.203,13.298],[-0.737,7.968],[0.033,2.318],[9.833,-7.242],[9.673,-7.872],[8.933,-7.942],[-6.077,1.958],[-9.937,3.378],[-15.467,2.078],[-19.317,-0.262]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.992156862745,0.992156862745,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 3","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Oval for Logo","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":14,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":130,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":155,"s":[0]},{"t":179,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[309.23,171.681,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.827,0.827,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":6,"s":[0,0,100]},{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":40,"s":[110,110,100]},{"i":{"x":[0.662,0.662,0.833],"y":[1,1,1]},"o":{"x":[0.283,0.283,0.167],"y":[0,0,0]},"t":52,"s":[95,95,100]},{"i":{"x":[0.662,0.662,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":60,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,17.667]},"o":{"x":[0.197,0.197,0.197],"y":[0,0,0]},"t":130,"s":[100,100,100]},{"i":{"x":[0.843,0.843,0.843],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,16.667]},"t":136,"s":[0,0,100]},{"i":{"x":[0.827,0.827,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":155,"s":[0,0,100]},{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":179,"s":[110,110,100]},{"i":{"x":[0.662,0.662,0.833],"y":[1,1,1]},"o":{"x":[0.283,0.283,0.167],"y":[0,0,0]},"t":191,"s":[95,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":199,"s":[100,100,100]},{"i":{"x":[0.674,0.674,0.667],"y":[1,1,1]},"o":{"x":[0.312,0.312,0.333],"y":[0,0,0]},"t":283,"s":[100,100,100]},{"i":{"x":[0.805,0.805,0.667],"y":[1,1,1]},"o":{"x":[0.325,0.325,0.333],"y":[0,0,0]},"t":293,"s":[110,110,100]},{"i":{"x":[0.746,0.746,0.667],"y":[0.999,0.999,1]},"o":{"x":[0.274,0.274,0.333],"y":[0,0,0]},"t":301,"s":[70,70,100]},{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":311,"s":[210,210,100]},{"i":{"x":[0.662,0.662,0.833],"y":[1,1,1]},"o":{"x":[0.283,0.283,0.167],"y":[0,0,0]},"t":321,"s":[195,195,100]},{"t":329,"s":[200,200,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[72,72],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.31372498157,0.654901960784,0.917646998985,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":-3,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[382,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[382,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":146,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[382,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[382,98,0],"to":[0,0,0],"ti":[0,0,0]},{"t":314,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-8,-12],[8,-12],[12,-8],[12,8],[8,12],[-8,12],[-12,8],[-12,-8]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":-4,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[382,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[382,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":145,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[382,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[382,98,0],"to":[0,0,0],"ti":[0,0,0]},{"t":313,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[6.08,0],[0,0],[0,-6.08],[0,0],[-6.08,0],[0,0],[0,6.08],[0,0]],"o":[[0,0],[-6.08,0],[0,0],[0,6.08],[0,0],[6.08,0],[0,0],[0,-6.08]],"v":[[14,-25],[-14,-25],[-25,-14],[-25,14],[-14,25],[14,25],[25,14],[25,-14]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,-2.14],[0,0],[0,0],[2.14,0],[0,0],[0,0],[0,2.14],[0,0],[0,0],[-2.14,0]],"o":[[0,0],[2.12,0.11],[0,0],[0,0],[-0.11,2.12],[0,0],[0,0],[-2.12,-0.11],[0,0],[0,0],[0.11,-2.12],[0,0]],"v":[[14,-18],[14.2,-18],[18,-14],[18,14],[18,14.2],[14,18],[-14,18],[-14.2,18],[-18,14],[-18,-14],[-18,-14.2],[-14,-18]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":-4,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[238,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[238,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":145,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[238,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[238,98,0],"to":[0,0,0],"ti":[0,0,0]},{"t":313,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-8,-12],[8,-12],[12,-8],[12,8],[8,12],[-8,12],[-12,8],[-12,-8]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 5","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":-4,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[238,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[238,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":145,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[238,98,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[238,98,0],"to":[0,0,0],"ti":[0,0,0]},{"t":313,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[6.08,0],[0,0],[0,-6.08],[0,0],[-6.08,0],[0,0],[0,6.08],[0,0]],"o":[[0,0],[-6.08,0],[0,0],[0,6.08],[0,0],[6.08,0],[0,0],[0,-6.08]],"v":[[14,-25],[-14,-25],[-25,-14],[-25,14],[-14,25],[14,25],[25,14],[25,-14]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,-2.14],[0,0],[0,0],[2.14,0],[0,0],[0,0],[0,2.14],[0,0],[0,0],[-2.14,0]],"o":[[0,0],[2.12,0.11],[0,0],[0,0],[-0.11,2.12],[0,0],[0,0],[-2.12,-0.11],[0,0],[0,0],[0.11,-2.12],[0,0]],"v":[[14,-18],[14.2,-18],[18,-14],[18,14],[18,14.2],[14,18],[-14,18],[-14.2,18],[-18,14],[-18,-14],[-18,-14.2],[-14,-18]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":-4,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[238,244,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[238,244,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":145,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[238,244,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[238,244,0],"to":[0,0,0],"ti":[0,0,0]},{"t":313,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[6.08,0],[0,0],[0,-6.08],[0,0],[-6.08,0],[0,0],[0,6.08],[0,0]],"o":[[0,0],[-6.08,0],[0,0],[0,6.08],[0,0],[6.08,0],[0,0],[0,-6.08]],"v":[[14,-25],[-14,-25],[-25,-14],[-25,14],[-14,25],[14,25],[25,14],[25,-14]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,-2.14],[0,0],[0,0],[2.14,0],[0,0],[0,0],[0,2.14],[0,0],[0,0],[-2.14,0]],"o":[[0,0],[2.12,0.11],[0,0],[0,0],[-0.11,2.12],[0,0],[0,0],[-2.12,-0.11],[0,0],[0,0],[0.11,-2.12],[0,0]],"v":[[14,-18],[14.2,-18],[18,-14],[18,14],[18,14.2],[14,18],[-14,18],[-14.2,18],[-18,14],[-18,-14],[-18,-14.2],[-14,-18]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":-4,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[238,244,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[238,244,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":145,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[238,244,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[238,244,0],"to":[0,0,0],"ti":[0,0,0]},{"t":313,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-8,-12],[8,-12],[12,-8],[12,8],[8,12],[-8,12],[-12,8],[-12,-8]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[341.5,260,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[341.5,260,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[341.5,260,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[341.5,260,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[308.68,170.23,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-5,-9],[5,-9],[9,-5],[9,5],[5,9],[-5,9],[-9,5],[-9,-5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[282,121,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[282,121,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":154,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[282,121,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[282,121,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-5,-9],[5,-9],[9,-5],[9,5],[5,9],[-5,9],[-9,5],[-9,-5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 26","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[286,95,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[286,95,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":154,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[286,95,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[286,95,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 21","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[240,139,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[240,139,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":154,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[240,139,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[240,139,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 35","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[308.18,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[258,139,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[258,139,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[308.18,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":154,"s":[308.18,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[258,139,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[258,139,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[308.18,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 20","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[309.681,170.929,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[249,148,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[249,148,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.681,170.929,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":154,"s":[309.681,170.929,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[249,148,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[249,148,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.681,170.929,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 25","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[349,135,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[349,135,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":154,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[349,135,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[349,135,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 36","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[233,180,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[233,180,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":154,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[233,180,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[233,180,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 19","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[375,231,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[375,231,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":152,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[375,231,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[375,231,0],"to":[0,0,0],"ti":[0,0,0]},{"t":320,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 37","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[308.18,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[384,223,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[384,223,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[308.18,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":154,"s":[308.18,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[384,223,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[384,223,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[308.18,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-1,-5],[1,-5],[5,-1],[5,1],[1,5],[-1,5],[-5,1],[-5,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 14","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[343.5,78.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[343.5,78.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[343.5,78.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[343.5,78.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.2,0],[0,0],[0,-2.2],[0,0],[2.2,0],[0,0],[0,2.2],[0,0]],"o":[[0,0],[2.2,0],[0,0],[0,2.2],[0,0],[-2.2,0],[0,0],[0,-2.2]],"v":[[-1.51,-5.5],[1.51,-5.5],[5.5,-1.51],[5.5,1.51],[1.51,5.5],[-1.51,5.5],[-5.5,1.51],[-5.5,-1.51]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 27","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[401.5,183.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[401.5,183.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":152,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[401.5,183.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[401.5,183.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":320,"s":[308.68,170.23,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.2,0],[0,0],[0,-2.2],[0,0],[2.2,0],[0,0],[0,2.2],[0,0]],"o":[[0,0],[2.2,0],[0,0],[0,2.2],[0,0],[-2.2,0],[0,0],[0,-2.2]],"v":[[-1.51,-5.5],[1.51,-5.5],[5.5,-1.51],[5.5,1.51],[1.51,5.5],[-1.51,5.5],[-5.5,1.51],[-5.5,-1.51]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 24","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[276.5,248.517,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[276.5,248.517,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":154,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[276.5,248.517,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[276.5,248.517,0],"to":[0,0,0],"ti":[0,0,0]},{"t":320,"s":[309.68,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.2,0],[0,0],[0,-2.21],[0,0],[2.2,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.2,0],[0,0],[0,2.21],[0,0],[-2.2,0],[0,0],[0,-2.21]],"v":[[-1.51,-5.517],[1.51,-5.517],[5.5,-1.517],[5.5,1.513],[1.51,5.513],[-1.51,5.513],[-5.5,1.513],[-5.5,-1.517]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 34","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[309.681,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[297.586,77.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[297.586,77.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.681,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.681,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[297.586,77.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[297.586,77.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":302,"s":[100,100,100]},{"t":322,"s":[86,86,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-23.586,-4.5],[23.584,-4.5],[27.584,-0.5],[27.584,0.5],[23.584,4.5],[-23.586,4.5],[-27.586,0.5],[-27.586,-0.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 32","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[312.5,112,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[312.5,112,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":154,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[312.5,112,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[312.5,112,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0.11,2.12],[0,0],[0,0],[-2.12,0.11],[0,0],[0,0],[-0.11,-2.12],[0,0],[0,0],[0,0],[-0.11,-2.12],[0,0],[0,0],[2.12,-0.11],[0,0],[0,0]],"o":[[0,0],[-2.14,0],[0,0],[0,0],[0,-2.14],[0,0],[0,0],[2.14,0],[0,0],[0,0],[0,0],[2.14,0],[0,0],[0,0],[0,2.14],[0,0],[0,0],[0,0]],"v":[[6.5,11],[-6.5,11],[-10.5,7.2],[-10.5,7],[-10.5,-7],[-6.7,-11],[-6.5,-11],[-5.5,-11],[-1.5,-7.2],[-1.5,-7],[-1.5,2],[6.5,2],[10.5,5.8],[10.5,6],[10.5,7],[6.7,11],[6.5,11],[-6.5,11]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":24,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[329.999,106.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[329.999,106.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[329.999,106.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[329.999,106.552,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0]],"v":[[7.001,-16.552],[11.001,-12.552],[11.001,12.548],[7.001,16.548],[6.001,16.548],[2.001,12.548],[2.001,-7.552],[-6.999,-7.552],[-10.999,-11.552],[-10.999,-12.552],[-6.999,-16.552]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":25,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[308.68,170.232,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[224,151.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[224,151.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[308.68,170.232,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[308.68,170.232,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[224,151.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[224,151.552,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0]],"v":[[-6,-16.552],[-2,-12.552],[-2,-3.832],[7,-3.832],[11,0.168],[11,1.168],[7,5.168],[-2,5.168],[-2,12.548],[-6,16.548],[-7,16.548],[-11,12.548],[-11,-12.552],[-7,-16.552]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":26,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[369,150.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[369,150.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":154,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[369,150.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[369,150.552,0],"to":[0,0,0],"ti":[0,0,0]},{"t":320,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0]],"v":[[10,-16.552],[14,-12.552],[14,-11.552],[10,-7.552],[4,-7.552],[4,12.548],[0,16.548],[-1,16.548],[-1.12,16.548],[-10,16.548],[-14,12.548],[-14,2.548],[-10,-1.452],[-5,-1.452],[-5,-12.552],[-1,-16.552]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":27,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[309.68,171.682,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[398,151.828,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[398,151.828,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.682,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.682,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[398,151.828,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[398,151.828,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[2.12,-0.11],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21]],"o":[[0,2.15],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0]],"v":[[9,12.548],[5.2,16.548],[5,16.548],[-5,16.548],[-9,12.548],[-9,11.548],[-5,7.548],[0,7.548],[0,-12.552],[4,-16.552],[5,-16.552],[9,-12.552]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":28,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[309.68,171.682,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[223,195.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[223,195.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.682,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":151,"s":[309.68,171.682,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[223,195.552,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[223,195.552,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0]],"o":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"v":[[-6,16.548],[-10,12.548],[-10,-12.552],[-6,-16.552],[-5,-16.552],[-1,-12.552],[-1,-1.452],[6,-1.452],[10,2.548],[10,12.548],[6,16.548]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":29,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[256,190.052,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[256,190.052,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":154,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[256,190.052,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[256,190.052,0],"to":[0,0,0],"ti":[0,0,0]},{"t":319,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21]],"o":[[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0]],"v":[[-9,-18.052],[-5,-22.052],[5,-22.052],[9,-18.052],[9,-8.052],[5,-4.052],[0,-4.052],[0,13.048],[5,13.048],[9,17.048],[9,18.048],[5,22.048],[-5,22.048],[-9,18.048]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":30,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[372.5,183.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[372.5,183.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":154,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[372.5,183.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[372.5,183.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-10.5,-4.5],[10.5,-4.5],[14.5,-0.5],[14.5,0.5],[10.5,4.5],[-10.5,4.5],[-14.5,0.5],[-14.5,-0.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle Copy 40","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":31,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[280.999,225.752,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[280.999,225.752,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":154,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[280.999,225.752,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[280.999,225.752,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0]],"v":[[5.001,-9.752],[9.001,-5.752],[9.001,-4.752],[5.001,-0.752],[0.001,-0.752],[0.001,5.748],[-3.999,9.748],[-4.999,9.748],[-8.999,5.748],[-8.999,-5.752],[-4.999,-9.752]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":32,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[313.498,227.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[313.498,227.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":154,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[313.498,227.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[313.498,227.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.11,2.12],[0,0],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0]],"o":[[-2.14,0],[0,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"v":[[-9.498,9.5],[-13.498,5.7],[-13.498,5.5],[-13.498,4.5],[-9.498,0.5],[2.462,0.5],[2.462,-5.5],[6.462,-9.5],[9.502,-9.5],[13.502,-5.5],[13.502,5.5],[9.502,9.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":33,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[346.5,218,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[346.5,218,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":154,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[346.5,218,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[346.5,218,0],"to":[0,0,0],"ti":[0,0,0]},{"t":322,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[2.12,-0.11],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21]],"o":[[0,2.14],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0]],"v":[[10.5,7],[6.7,11],[6.5,11],[-6.5,11],[-10.5,7],[-10.5,6],[-6.5,2],[1.5,2],[1.5,-7],[5.5,-11],[6.5,-11],[10.5,-7]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":34,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[387.25,212.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[387.25,212.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":149,"s":[308.68,170.23,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[387.25,212.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[387.25,212.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":317,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0]],"v":[[15.75,-11],[19.75,-7],[19.75,7],[15.75,11],[14.75,11],[10.75,7],[10.75,-2],[-15.75,-2],[-19.75,-6],[-19.75,-7],[-15.75,-11]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":35,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":1,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[296.5,260,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[296.5,260,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":150,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[296.5,260,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[296.5,260,0],"to":[0,0,0],"ti":[0,0,0]},{"t":317,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":302,"s":[100,100,100]},{"t":322,"s":[90,90,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,0],[-2.21,0]],"o":[[2.21,0],[0,0],[0,2.21],[0,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0]],"v":[[22.5,-9],[26.5,-5],[26.5,-4],[22.5,0],[16.5,0],[16.5,5],[12.5,9],[-22.5,9],[-26.5,5],[-26.5,4],[-22.5,0],[-1.5,0],[-1.5,-5],[2.5,-9]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":36,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":1,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[372,247.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[372,247.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":150,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[372,247.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[372,247.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":318,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"o":[[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0],[0,-2.21]],"v":[[-11,-4.5],[11,-4.5],[15,-0.5],[15,0.5],[11,4.5],[-11,4.5],[-15,0.5],[-15,-0.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":37,"ty":4,"nm":"QR code","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.831],"y":[1]},"o":{"x":[0.183],"y":[0]},"t":0,"s":[120]},{"i":{"x":[0.762],"y":[1]},"o":{"x":[0.39],"y":[0]},"t":24,"s":[-45]},{"i":{"x":[0.708],"y":[1]},"o":{"x":[0.275],"y":[0]},"t":34,"s":[10]},{"t":42,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":-3,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[384.501,255.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[384.501,255.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":136,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":146,"s":[309.68,171.68,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[384.501,255.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":302,"s":[384.501,255.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":314,"s":[309.681,171.68,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,2.21],[-2.21,0],[0,0],[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0]],"o":[[-2.21,0],[0,-2.21],[0,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0]],"v":[[-18.499,13.5],[-22.499,9.5],[-18.499,5.5],[13.501,5.5],[13.501,-9.5],[17.501,-13.5],[18.501,-13.5],[22.501,-9.5],[22.501,9.5],[18.501,13.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.20000000298,0.20000000298,0.20000000298,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Done","refId":"comp_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":301,"s":[0]},{"t":302,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":297,"s":[551.625,282.005,0],"to":[-1.688,0.487,0],"ti":[1.688,-0.487,0]},{"t":309,"s":[541.5,284.925,0]}],"ix":2},"a":{"a":0,"k":[36.5,28,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.827,0.827,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":297,"s":[0,0,100]},{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":309,"s":[110,110,100]},{"i":{"x":[0.662,0.662,0.833],"y":[1,1,1]},"o":{"x":[0.283,0.283,0.167],"y":[0,0,0]},"t":329,"s":[95,95,100]},{"t":337,"s":[100,100,100]}],"ix":6}},"ao":0,"w":73,"h":56,"ip":297,"op":361,"st":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Logo full","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":302,"s":[100]},{"t":303,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":280,"s":[537.4,284,0],"to":[0,-0.024,0],"ti":[-0.436,0.443,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":295,"s":[537.4,283.857,0],"to":[0.508,-0.517,0],"ti":[-0.433,0.226,0]},{"t":303,"s":[540.001,282.5,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":286,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.283,0.283,0.167],"y":[0,0,0]},"t":295,"s":[110,110,100]},{"t":303,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-5.19,2.15],[-1.34,0.02],[-0.43,-0.34],[-0.05,-0.27],[0.04,-0.49],[1.18,-6.29],[0.95,-0.09],[2.01,1.3],[3.04,2],[-2.01,2.07],[-0.17,0.75],[0.21,0.18],[0.22,-0.05],[0,0],[1.73,0.04],[1.81,0.58],[-0.15,1.23]],"o":[[0,0],[9.89,-4.1],[0.3,0],[0.36,0.29],[0.04,0.28],[-0.54,5.62],[-0.5,2.66],[-2.07,0.19],[-3.13,-2.05],[-3.51,-2.31],[0.52,-0.55],[0.03,-0.1],[-0.21,-0.19],[-0.47,0.1],[0,0],[-1.27,-0.03],[-2.23,-0.73],[0.12,-0.96]],"v":[[-16.667,-2.222],[4.083,-11.142],[17.363,-15.982],[18.743,-15.572],[19.253,-14.612],[19.313,-13.212],[15.273,12.328],[12.843,15.968],[7.203,13.298],[-0.737,7.968],[0.033,2.318],[9.833,-7.242],[9.673,-7.872],[8.933,-7.942],[-6.077,1.958],[-9.937,3.378],[-15.467,2.078],[-19.317,-0.262]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 3","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":280,"op":361,"st":280,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Oval for logo full","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[539.18,282.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.674,0.674,0.667],"y":[1,1,1]},"o":{"x":[0.312,0.312,0.333],"y":[0,0,0]},"t":283,"s":[100,100,100]},{"i":{"x":[0.805,0.805,0.667],"y":[1,1,1]},"o":{"x":[0.325,0.325,0.333],"y":[0,0,0]},"t":293,"s":[110,110,100]},{"i":{"x":[0.746,0.746,0.667],"y":[0.999,0.999,1]},"o":{"x":[0.274,0.274,0.333],"y":[0,0,0]},"t":301,"s":[70,70,100]},{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":311,"s":[210,210,100]},{"i":{"x":[0.662,0.662,0.833],"y":[1,1,1]},"o":{"x":[0.283,0.283,0.167],"y":[0,0,0]},"t":321,"s":[195,195,100]},{"t":329,"s":[200,200,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[71.36,71.36],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.31372498157,0.654901960784,0.917646998985,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":280,"op":361,"st":280,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"QR in phone","refId":"comp_1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":154,"s":[0]},{"t":161,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":156,"s":[540,292,0],"to":[0,0,0],"ti":[0,1.598,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":158,"s":[540,295.5,0],"to":[0,-1.321,0],"ti":[0,-0.004,0]},{"t":161,"s":[540,289.637,0]}],"ix":2},"a":{"a":0,"k":[310,179,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.827,0.827,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":154,"s":[0,0,100]},{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":164,"s":[110,110,100]},{"i":{"x":[0.662,0.662,0.833],"y":[1,1,1]},"o":{"x":[0.283,0.283,0.167],"y":[0,0,0]},"t":174,"s":[95,95,100]},{"i":{"x":[0.827,0.827,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":182,"s":[100,100,100]},{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":290,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.283,0.283,0.167],"y":[0,0,0]},"t":301,"s":[110,110,100]},{"t":340,"s":[0,0,100]}],"ix":6}},"ao":0,"w":620,"h":358,"ip":154,"op":3599,"st":-1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Camera Icon","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":150,"s":[100]},{"t":159,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.1,"y":1},"o":{"x":0.25,"y":0.179},"t":90,"s":[1239.001,282,0],"to":[-116.5,0,0],"ti":[116.5,0,0]},{"t":150,"s":[540.001,282,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":143,"s":[110,110,100]},{"i":{"x":[0.662,0.662,0.833],"y":[1,1,1]},"o":{"x":[0.283,0.283,0.167],"y":[0,0,0]},"t":150,"s":[95,95,100]},{"t":160,"s":[516,516,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,-2.59],[0,0],[-2.59,0],[0,0],[0,2.59],[0,0],[2.59,0],[0,0],[0,0]],"o":[[0,0],[0,0],[-2.59,0],[0,0],[0,2.59],[0,0],[2.59,0],[0,0],[0,-2.59],[0,0],[0,0],[0,0]],"v":[[-7.07,-21.204],[-11.38,-16.494],[-18.85,-16.494],[-23.56,-11.784],[-23.56,16.496],[-18.85,21.206],[18.85,21.206],[23.56,16.496],[23.56,-11.784],[18.85,-16.494],[11.38,-16.494],[7.07,-21.204]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[6.5,0],[0,6.5],[-6.5,0],[0,-6.5]],"o":[[-6.5,0],[0,-6.5],[6.5,0],[0,6.5]],"v":[[0,14.136],[-11.78,2.356],[0,-9.424],[11.78,2.356]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"d":1,"ty":"el","s":{"a":0,"k":[15.078,15.078],"ix":2},"p":{"a":0,"k":[0,2.356],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.992156862745,0.992156862745,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":361,"st":-30,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Phone Display White","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":90,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":150,"s":[0]},{"t":159,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.1,"y":1},"o":{"x":0.25,"y":0.179},"t":90,"s":[1239.001,299.5,0],"to":[-116.5,0,0],"ti":[116.5,0,0]},{"i":{"x":0.1,"y":0.1},"o":{"x":0.1,"y":0.1},"t":150,"s":[540.001,299.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.1,"y":1},"o":{"x":0.167,"y":0},"t":301,"s":[540.001,299.5,0],"to":[0,-3,0],"ti":[0,3,0]},{"t":307,"s":[540.001,281.5,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":290,"s":[100,100,100]},{"i":{"x":[0.641,0.641,0.581],"y":[0.644,0.719,1]},"o":{"x":[0.291,0.291,0.18],"y":[0,0,0]},"t":301,"s":[110,110,100]},{"i":{"x":[0.679,0.679,0.702],"y":[1,1,1]},"o":{"x":[0.337,0.337,0.346],"y":[0.536,0.935,0]},"t":308,"s":[42.219,24.219,100]},{"t":315,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-13.71],[0,0],[-12.11,0],[0,0],[0,12.1],[0,0],[12.11,0],[0,0],[0,-2.01],[0,0],[7.68,0],[0,0],[0,7.31],[0,0],[2.01,0]],"o":[[-12.11,0],[0,0],[0,12.1],[0,0],[12.11,0],[0,0],[0,-13.71],[0,0],[-2.02,0],[0,0],[0,7.3],[0,0],[-7.67,0],[0,0],[0,-2.01],[0,0]],"v":[[-96.18,-231.5],[-121,-206.68],[-121,206.68],[-96.18,231.5],[96.18,231.5],[121,206.68],[121,-206.68],[96.18,-231.5],[65.44,-231.5],[61.78,-227.85],[61.78,-225.66],[47.89,-213.9],[-47.16,-213.9],[-61.05,-225.66],[-61.05,-227.85],[-64.7,-231.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":150,"op":361,"st":-30,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Phone Display","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.1,"y":1},"o":{"x":0.25,"y":0.179},"t":90,"s":[1239.001,299.5,0],"to":[-116.5,0,0],"ti":[116.5,0,0]},{"i":{"x":0.1,"y":0.1},"o":{"x":0.1,"y":0.1},"t":150,"s":[540.001,299.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.1,"y":1},"o":{"x":0.167,"y":0},"t":301,"s":[540.001,299.5,0],"to":[0,-3,0],"ti":[0,3,0]},{"t":307,"s":[540.001,281.5,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":290,"s":[100,100,100]},{"i":{"x":[0.641,0.641,0.581],"y":[0.644,0.719,1]},"o":{"x":[0.291,0.291,0.18],"y":[0,0,0]},"t":301,"s":[110,110,100]},{"i":{"x":[0.679,0.679,0.702],"y":[1,1,1]},"o":{"x":[0.337,0.337,0.346],"y":[0.536,0.935,0]},"t":308,"s":[42.219,24.219,100]},{"t":315,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-13.71],[0,0],[-12.11,0],[0,0],[0,12.1],[0,0],[12.11,0],[0,0],[0,-2.01],[0,0],[7.68,0],[0,0],[0,7.31],[0,0],[2.01,0]],"o":[[-12.11,0],[0,0],[0,12.1],[0,0],[12.11,0],[0,0],[0,-13.71],[0,0],[-2.02,0],[0,0],[0,7.3],[0,0],[-7.67,0],[0,0],[0,-2.01],[0,0]],"v":[[-96.18,-231.5],[-121,-206.68],[-121,206.68],[-96.18,231.5],[96.18,231.5],[121,206.68],[121,-206.68],[96.18,-231.5],[65.44,-231.5],[61.78,-227.85],[61.78,-225.66],[47.89,-213.9],[-47.16,-213.9],[-61.05,-225.66],[-61.05,-227.85],[-64.7,-231.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.130070465686,0.127464264514,0.127464264514,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":361,"st":-30,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Phone","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":300,"s":[100]},{"t":307,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.1,"y":1},"o":{"x":0.25,"y":0.179},"t":90,"s":[1239.001,299.5,0],"to":[-116.5,0,0],"ti":[116.5,0,0]},{"i":{"x":0.1,"y":0.1},"o":{"x":0.1,"y":0.1},"t":150,"s":[540.001,299.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.1,"y":1},"o":{"x":0.167,"y":0},"t":301,"s":[540.001,299.5,0],"to":[0,-3,0],"ti":[0,3,0]},{"t":307,"s":[540.001,281.5,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":290,"s":[100,100,100]},{"i":{"x":[0.641,0.641,0.581],"y":[0.644,0.719,1]},"o":{"x":[0.291,0.291,0.18],"y":[0,0,0]},"t":301,"s":[110,110,100]},{"i":{"x":[0.679,0.679,0.702],"y":[1,1,1]},"o":{"x":[0.337,0.337,0.346],"y":[0.536,0.935,0]},"t":308,"s":[42.219,24.219,100]},{"t":315,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-19.73],[0,0],[-0.12,-2.87],[0,0],[0,0],[2.84,-0.48],[0,0],[19.73,0],[0,0],[0,19.73],[0,0],[0,2.98],[0,0],[-2.84,0.5],[0,0],[0,2.97],[0,0],[-2.84,0.48],[0,0],[-19.73,0]],"o":[[19.73,0],[0,0],[2.76,0.46],[0,0],[0,0],[0,2.97],[0,0],[0,19.73],[0,0],[-19.73,0],[0,0],[-2.84,-0.5],[0,0],[0,-2.98],[0,0],[-2.84,-0.48],[0,0],[0,-2.97],[0,0],[0,-19.73],[0,0]],"v":[[94.78,-243.5],[130.5,-207.78],[130.5,-138.12],[134.5,-132.45],[134.5,-132.21],[134.5,-78.95],[130.5,-73.03],[130.5,207.78],[94.78,243.5],[-94.78,243.5],[-130.5,207.78],[-130.5,-65.44],[-134.5,-71.4],[-134.5,-91.83],[-130.5,-97.79],[-130.5,-109.65],[-134.5,-115.57],[-134.5,-136.09],[-130.5,-142.01],[-130.5,-207.78],[-94.78,-243.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":361,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Phone Background","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.1,"y":1},"o":{"x":0.25,"y":0.179},"t":90,"s":[1239.001,299.5,0],"to":[-116.5,0,0],"ti":[116.5,0,0]},{"i":{"x":0.1,"y":0.1},"o":{"x":0.1,"y":0.1},"t":150,"s":[540.001,299.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.1,"y":1},"o":{"x":0.167,"y":0},"t":301,"s":[540.001,299.5,0],"to":[0,-3,0],"ti":[0,3,0]},{"t":307,"s":[540.001,281.5,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":290,"s":[100,100,100]},{"i":{"x":[0.641,0.641,0.581],"y":[0.644,0.719,1]},"o":{"x":[0.291,0.291,0.18],"y":[0,0,0]},"t":301,"s":[110,110,100]},{"i":{"x":[0.679,0.679,0.702],"y":[1,1,1]},"o":{"x":[0.337,0.337,0.346],"y":[0.536,0.935,0]},"t":308,"s":[42.219,24.219,100]},{"t":315,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-19.73],[0,0],[-0.11,-2.87],[0,0],[0,0],[2.83,-0.47],[0,0],[19.73,0],[0,0],[0,19.73],[0,0],[0,2.99],[0,0],[-2.84,0.5],[0,0],[0,2.98],[0,0],[-2.84,0.48],[0,0],[-19.73,0]],"o":[[19.73,0],[0,0],[2.76,0.46],[0,0],[0,0],[0,2.98],[0,0],[0,19.73],[0,0],[-19.73,0],[0,0],[-2.84,-0.5],[0,0],[0,-2.98],[0,0],[-2.84,-0.48],[0,0],[0,-2.97],[0,0],[0,-19.73],[0,0]],"v":[[99.782,-248.5],[135.502,-212.78],[135.502,-143.12],[140.492,-137.45],[140.502,-137.21],[140.502,-73.99],[135.502,-68.07],[135.502,212.78],[99.782,248.5],[-99.778,248.5],[-135.498,212.78],[-135.498,-60.45],[-140.498,-66.42],[-140.498,-96.83],[-135.498,-102.79],[-135.498,-104.37],[-140.498,-110.29],[-140.498,-141.09],[-135.498,-147.01],[-135.498,-212.78],[-99.778,-248.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.988235294118,0.988235294118,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":361,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":0,"nm":"Laptop Open","refId":"comp_2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":101,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":124,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":299,"s":[25]},{"t":315,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,290.125,0],"ix":2},"a":{"a":0,"k":[310,179,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.827,0.827,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[95,95,100]},{"i":{"x":[0.747,0.747,0.667],"y":[1,1,1]},"o":{"x":[0.354,0.354,0.333],"y":[0,0,0]},"t":30,"s":[110,110,100]},{"i":{"x":[0.662,0.662,0.833],"y":[1,1,1]},"o":{"x":[0.283,0.283,0.167],"y":[0,0,0]},"t":40,"s":[95,95,100]},{"t":48,"s":[100,100,100]}],"ix":6}},"ao":0,"w":620,"h":358,"ip":0,"op":3600,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/TelegramUI/Resources/PresentationStrings.mapping b/submodules/TelegramUI/TelegramUI/Resources/PresentationStrings.mapping index b01c7bc5d0..2f6ecd7fdb 100644 Binary files a/submodules/TelegramUI/TelegramUI/Resources/PresentationStrings.mapping and b/submodules/TelegramUI/TelegramUI/Resources/PresentationStrings.mapping differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_10@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_10@2x.png new file mode 100644 index 0000000000..273fc1baa1 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_10@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_10@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_10@3x.png new file mode 100644 index 0000000000..9512f41d51 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_10@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_11@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_11@2x.png new file mode 100644 index 0000000000..23ea3e0116 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_11@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_11@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_11@3x.png new file mode 100644 index 0000000000..bafa6391bd Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_11@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_12@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_12@2x.png new file mode 100644 index 0000000000..d34297d32f Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_12@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_12@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_12@3x.png new file mode 100644 index 0000000000..9e9eddf176 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_12@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_13@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_13@2x.png new file mode 100644 index 0000000000..c4220a0f21 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_13@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_13@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_13@3x.png new file mode 100644 index 0000000000..c89abff58b Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_13@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_14@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_14@2x.png new file mode 100644 index 0000000000..49b95d209d Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_14@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_14@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_14@3x.png new file mode 100644 index 0000000000..afb24ede6d Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_14@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_15@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_15@2x.png new file mode 100644 index 0000000000..a5eab77246 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_15@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_15@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_15@3x.png new file mode 100644 index 0000000000..7c5caafcfb Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_15@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_16@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_16@2x.png new file mode 100644 index 0000000000..82d4399e55 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_16@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_16@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_16@3x.png new file mode 100644 index 0000000000..015aade5a4 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_16@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_17@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_17@2x.png new file mode 100644 index 0000000000..b3fa8a4ac0 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_17@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_17@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_17@3x.png new file mode 100644 index 0000000000..ec79f985dc Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_17@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_18@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_18@2x.png new file mode 100644 index 0000000000..7f63b34942 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_18@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_18@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_18@3x.png new file mode 100644 index 0000000000..c825b6c267 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_18@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_19@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_19@2x.png new file mode 100644 index 0000000000..c54d87dd6c Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_19@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_19@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_19@3x.png new file mode 100644 index 0000000000..641e005990 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_19@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_1@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_1@2x.png new file mode 100644 index 0000000000..5186865dc8 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_1@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_1@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_1@3x.png new file mode 100644 index 0000000000..110b478d0b Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_1@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_20@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_20@2x.png new file mode 100644 index 0000000000..7dacb98302 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_20@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_20@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_20@3x.png new file mode 100644 index 0000000000..b63f632cd6 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_20@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_2@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_2@2x.png new file mode 100644 index 0000000000..f8e1b6baef Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_2@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_2@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_2@3x.png new file mode 100644 index 0000000000..0b91a7da7b Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_2@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_3@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_3@2x.png new file mode 100644 index 0000000000..3ec81c2358 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_3@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_3@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_3@3x.png new file mode 100644 index 0000000000..0c3b06fe75 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_3@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_4@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_4@2x.png new file mode 100644 index 0000000000..01ab6d7561 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_4@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_4@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_4@3x.png new file mode 100644 index 0000000000..9d4e5e2484 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_4@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_5@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_5@2x.png new file mode 100644 index 0000000000..04ca9f9a40 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_5@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_5@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_5@3x.png new file mode 100644 index 0000000000..2c4a899721 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_5@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_6@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_6@2x.png new file mode 100644 index 0000000000..b26df7cc4b Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_6@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_6@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_6@3x.png new file mode 100644 index 0000000000..0a00f45b37 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_6@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_7@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_7@2x.png new file mode 100644 index 0000000000..d5dd29b4e0 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_7@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_7@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_7@3x.png new file mode 100644 index 0000000000..b4592ab6bc Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_7@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_8@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_8@2x.png new file mode 100644 index 0000000000..0225bc10bf Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_8@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_8@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_8@3x.png new file mode 100644 index 0000000000..651f1ab6ac Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_8@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_9@2x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_9@2x.png new file mode 100644 index 0000000000..d3bf39785e Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_9@2x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_9@3x.png b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_9@3x.png new file mode 100644 index 0000000000..f724365468 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/StillReactions/simplereaction_9@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/WebEmbed/Twitch.html b/submodules/TelegramUI/TelegramUI/Resources/WebEmbed/Twitch.html index b1b5faa9e3..53fb685257 100755 --- a/submodules/TelegramUI/TelegramUI/Resources/WebEmbed/Twitch.html +++ b/submodules/TelegramUI/TelegramUI/Resources/WebEmbed/Twitch.html @@ -25,12 +25,8 @@ }; })(); - function play() { - invoke("play"); - } - - function pause() { - invoke("play"); + function playPause() { + invoke("playPause"); } function receiveMessage(evt) { diff --git a/submodules/TelegramUI/TelegramUI/Resources/WebEmbed/TwitchUserScript.js b/submodules/TelegramUI/TelegramUI/Resources/WebEmbed/TwitchUserScript.js index c4865582a7..552a6e9aad 100644 --- a/submodules/TelegramUI/TelegramUI/Resources/WebEmbed/TwitchUserScript.js +++ b/submodules/TelegramUI/TelegramUI/Resources/WebEmbed/TwitchUserScript.js @@ -1,5 +1,5 @@ function initialize() { - var controls = document.getElementsByClassName("player-controls-bottom")[0]; + var controls = document.getElementsByClassName("pl-controls-bottom")[0]; if (controls == null) { controls = document.getElementsByClassName("player-overlay-container")[0]; } @@ -7,9 +7,14 @@ function initialize() { controls.style.display = "none"; } + var root = document.getElementsByClassName("player-root")[0]; + if (root != null) { + root.style.display = "none"; + } + var topBar = document.getElementById("top-bar"); if (topBar == null) { - topBar = document.getElementsByClassName("player-controls-top")[0]; + topBar = document.getElementsByClassName("pl-controls-top")[0]; } if (topBar != null) { topBar.style.display = "none"; @@ -17,7 +22,7 @@ function initialize() { var pauseOverlay = document.getElementsByClassName("player-play-overlay")[0]; if (pauseOverlay == null) { - pauseOverlay = document.getElementsByClassName("player-controls-bottom")[0]; + pauseOverlay = document.getElementsByClassName("pl-controls-bottom")[0]; } if (pauseOverlay != null) { pauseOverlay.style.display = "none"; @@ -85,13 +90,15 @@ function eventFire(el, etype){ } } -function play() { +function togglePlayPause() { var playButton = document.getElementsByClassName("js-control-playpause-button")[0]; if (playButton == null) { - playButton = document.getElementsByClassName("player-button--playpause")[0]; + playButton = document.getElementsByClassName("player-button")[0]; } - eventFire(playButton, "click"); + if (playButton != null) { + eventFire(playButton, "click"); + } } function receiveMessage(evt) { @@ -105,8 +112,8 @@ function receiveMessage(evt) { if (obj.command == "initialize") initialize(); - else if (obj.command == "play") - play(); + else if (obj.command == "playPause") + togglePlayPause(); } catch (ex) { } } diff --git a/submodules/TelegramUI/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift b/submodules/TelegramUI/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift index 4533f217fd..c554fb0f1a 100644 --- a/submodules/TelegramUI/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift @@ -44,7 +44,7 @@ final class SecretChatHandshakeStatusInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.unblockPeer() } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState diff --git a/submodules/TelegramUI/TelegramUI/ShareExtensionContext.swift b/submodules/TelegramUI/TelegramUI/ShareExtensionContext.swift index a75d65678b..831d15766c 100644 --- a/submodules/TelegramUI/TelegramUI/ShareExtensionContext.swift +++ b/submodules/TelegramUI/TelegramUI/ShareExtensionContext.swift @@ -48,14 +48,16 @@ private enum ShareAuthorizationError { public struct ShareRootControllerInitializationData { public let appGroupPath: String public let apiId: Int32 + public let apiHash: String public let languagesCategory: String public let encryptionParameters: (Data, Data) public let appVersion: String public let bundleData: Data? - public init(appGroupPath: String, apiId: Int32, languagesCategory: String, encryptionParameters: (Data, Data), appVersion: String, bundleData: Data?) { + public init(appGroupPath: String, apiId: Int32, apiHash: String, languagesCategory: String, encryptionParameters: (Data, Data), appVersion: String, bundleData: Data?) { self.appGroupPath = appGroupPath self.apiId = apiId + self.apiHash = apiHash self.languagesCategory = languagesCategory self.encryptionParameters = encryptionParameters self.appVersion = appVersion @@ -186,16 +188,26 @@ public class ShareRootControllerImpl { let presentationDataPromise = Promise() - let appLockContext = AppLockContextImpl(rootPath: rootPath, window: nil, applicationBindings: applicationBindings, accountManager: accountManager, presentationDataSignal: presentationDataPromise.get(), lockIconInitialFrame: { + let appLockContext = AppLockContextImpl(rootPath: rootPath, window: nil, rootController: nil, applicationBindings: applicationBindings, accountManager: accountManager, presentationDataSignal: presentationDataPromise.get(), lockIconInitialFrame: { return nil }) - let sharedContext = SharedAccountContextImpl(mainWindow: nil, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()), rootPath: rootPath, legacyBasePath: nil, legacyCache: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }) + let sharedContext = SharedAccountContextImpl(mainWindow: nil, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()), rootPath: rootPath, legacyBasePath: nil, legacyCache: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }) presentationDataPromise.set(sharedContext.presentationData) internalContext = InternalContext(sharedContext: sharedContext) globalInternalContext = internalContext } + var immediatePeerId: PeerId? + if #available(iOS 13.2, *), let sendMessageIntent = self.getExtensionContext()?.intent as? INSendMessageIntent { + if let contact = sendMessageIntent.recipients?.first, let handle = contact.customIdentifier, handle.hasPrefix("tg") { + let string = handle.suffix(from: handle.index(handle.startIndex, offsetBy: 2)) + if let peerId = Int64(string) { + immediatePeerId = PeerId(peerId) + } + } + } + let account: Signal<(SharedAccountContextImpl, Account, [AccountWithInfo]), ShareAuthorizationError> = internalContext.sharedContext.accountManager.transaction { transaction -> (SharedAccountContextImpl, LoggingSettings) in return (internalContext.sharedContext, transaction.getSharedData(SharedDataKeys.loggingSettings) as? LoggingSettings ?? LoggingSettings.defaultSettings) } @@ -206,24 +218,36 @@ public class ShareRootControllerImpl { Logger.shared.redactSensitiveData = loggingSettings.redactSensitiveData - return combineLatest(sharedContext.activeAccountsWithInfo, accountManager.transaction { transaction -> Set in - return Set(transaction.getRecords().map { record in + return combineLatest(sharedContext.activeAccountsWithInfo, accountManager.transaction { transaction -> (Set, PeerId?) in + let accountRecords = Set(transaction.getRecords().map { record in return record.id }) + let intentsSettings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.intentsSettings) as? IntentsSettings ?? IntentsSettings.defaultSettings + return (accountRecords, intentsSettings.account) }) |> castError(ShareAuthorizationError.self) |> take(1) - |> mapToSignal { primaryAndAccounts, validAccountIds -> Signal<(SharedAccountContextImpl, Account, [AccountWithInfo]), ShareAuthorizationError> in + |> mapToSignal { primaryAndAccounts, validAccountIdsAndIntentsAccountId -> Signal<(SharedAccountContextImpl, Account, [AccountWithInfo]), ShareAuthorizationError> in var (maybePrimary, accounts) = primaryAndAccounts + let (validAccountIds, intentsAccountId) = validAccountIdsAndIntentsAccountId for i in (0 ..< accounts.count).reversed() { if !validAccountIds.contains(accounts[i].account.id) { accounts.remove(at: i) } } + if let _ = immediatePeerId, let intentsAccountId = intentsAccountId { + for account in accounts { + if account.peer.id == intentsAccountId { + maybePrimary = account.account.id + } + } + } + guard let primary = maybePrimary, validAccountIds.contains(primary) else { return .fail(.unauthorized) } + guard let info = accounts.first(where: { $0.account.id == primary }) else { return .fail(.unauthorized) } @@ -235,16 +259,19 @@ public class ShareRootControllerImpl { let applicationInterface = account |> mapToSignal { sharedContext, account, otherAccounts -> Signal<(AccountContext, PostboxAccessChallengeData, [AccountWithInfo]), ShareAuthorizationError> in - let limitsConfiguration = account.postbox.transaction { transaction -> LimitsConfiguration in - return transaction.getPreferencesEntry(key: PreferencesKeys.limitsConfiguration) as? LimitsConfiguration ?? LimitsConfiguration.defaultValue + let limitsConfigurationAndContentSettings = account.postbox.transaction { transaction -> (LimitsConfiguration, ContentSettings) in + return ( + transaction.getPreferencesEntry(key: PreferencesKeys.limitsConfiguration) as? LimitsConfiguration ?? LimitsConfiguration.defaultValue, + getContentSettings(transaction: transaction) + ) } - return combineLatest(sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationPasscodeSettings]), limitsConfiguration, sharedContext.accountManager.accessChallengeData()) + return combineLatest(sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationPasscodeSettings]), limitsConfigurationAndContentSettings, sharedContext.accountManager.accessChallengeData()) |> take(1) |> deliverOnMainQueue |> castError(ShareAuthorizationError.self) - |> map { sharedData, limitsConfiguration, data -> (AccountContext, PostboxAccessChallengeData, [AccountWithInfo]) in + |> map { sharedData, limitsConfigurationAndContentSettings, data -> (AccountContext, PostboxAccessChallengeData, [AccountWithInfo]) in updateLegacyLocalization(strings: sharedContext.currentPresentationData.with({ $0 }).strings) - let context = AccountContextImpl(sharedContext: sharedContext, account: account, tonContext: nil, limitsConfiguration: limitsConfiguration) + let context = AccountContextImpl(sharedContext: sharedContext, account: account/*, tonContext: nil*/, limitsConfiguration: limitsConfigurationAndContentSettings.0, contentSettings: limitsConfigurationAndContentSettings.1) return (context, data.data, otherAccounts) } } @@ -292,17 +319,7 @@ public class ShareRootControllerImpl { } |> then(.single(.done)) } - - var immediatePeerId: PeerId? - if #available(iOS 13.0, *), let sendMessageIntent = self?.getExtensionContext()?.intent as? INSendMessageIntent { - if let contact = sendMessageIntent.recipients?.first, let handle = contact.customIdentifier, handle.hasPrefix("tg") { - let string = handle.suffix(from: handle.index(handle.startIndex, offsetBy: 2)) - if let userId = Int32(string) { - immediatePeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) - } - } - } - + let shareController = ShareController(context: context, subject: .fromExternal({ peerIds, additionalText, account in if let strongSelf = self, let inputItems = strongSelf.getExtensionContext()?.inputItems, !inputItems.isEmpty, !peerIds.isEmpty { let rawSignals = TGItemProviderSignals.itemSignals(forInputItems: inputItems)! @@ -388,7 +405,7 @@ public class ShareRootControllerImpl { return } let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 } - let controller = standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.Share_AuthTitle, text: presentationData.strings.Share_AuthDescription, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.Share_AuthTitle, text: presentationData.strings.Share_AuthDescription, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) })]) strongSelf.mainWindow?.present(controller, on: .root) diff --git a/submodules/TelegramUI/TelegramUI/SharedAccountContext.swift b/submodules/TelegramUI/TelegramUI/SharedAccountContext.swift index edb7b5645c..57e6012d9f 100644 --- a/submodules/TelegramUI/TelegramUI/SharedAccountContext.swift +++ b/submodules/TelegramUI/TelegramUI/SharedAccountContext.swift @@ -15,13 +15,15 @@ import PeersNearbyUI import PeerInfoUI import SettingsUI import UrlHandling +#if ENABLE_WALLET import WalletUI +import WalletCore +#endif import LegacyMediaPickerUI import LocalMediaResources import OverlayStatusController import AlertUI import PresentationDataUtils -import WalletCore private enum CallStatusText: Equatable { case none @@ -129,6 +131,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return self._automaticMediaDownloadSettings.get() } + public let currentAutodownloadSettings: Atomic + private let _autodownloadSettings = Promise() + private var currentAutodownloadSettingsDisposable = MetaDisposable() + public let currentMediaInputSettings: Atomic private var mediaInputSettingsDisposable: Disposable? @@ -145,6 +151,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { private let displayUpgradeProgress: (Float?) -> Void + private var spotlightDataContext: SpotlightDataContext? private var widgetDataContext: WidgetDataContext? public init(mainWindow: Window1?, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, rootPath: String, legacyBasePath: String?, legacyCache: LegacyCache?, apsNotificationToken: Signal, voipNotificationToken: Signal, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }) { @@ -167,9 +174,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.apsNotificationToken = apsNotificationToken self.voipNotificationToken = voipNotificationToken - - self.mediaManager = MediaManagerImpl(accountManager: accountManager, inForeground: applicationBindings.applicationInForeground) - + if applicationBindings.isMainApp { self.locationManager = DeviceLocationManager(queue: Queue.mainQueue()) self.contactDataManager = DeviceContactDataManagerImpl() @@ -180,13 +185,15 @@ public final class SharedAccountContextImpl: SharedAccountContext { self._currentPresentationData = Atomic(value: initialPresentationDataAndSettings.presentationData) self.currentAutomaticMediaDownloadSettings = Atomic(value: initialPresentationDataAndSettings.automaticMediaDownloadSettings) + self.currentAutodownloadSettings = Atomic(value: initialPresentationDataAndSettings.autodownloadSettings) self.currentMediaInputSettings = Atomic(value: initialPresentationDataAndSettings.mediaInputSettings) self.currentInAppNotificationSettings = Atomic(value: initialPresentationDataAndSettings.inAppNotificationSettings) - self._presentationData.set(.single(initialPresentationDataAndSettings.presentationData) + let presentationData: Signal = .single(initialPresentationDataAndSettings.presentationData) |> then( updatedPresentationData(accountManager: self.accountManager, applicationInForeground: self.applicationBindings.applicationInForeground, systemUserInterfaceStyle: mainWindow?.systemUserInterfaceStyle ?? .single(.light)) - )) + ) + self._presentationData.set(presentationData) self._automaticMediaDownloadSettings.set(.single(initialPresentationDataAndSettings.automaticMediaDownloadSettings) |> then(accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings, ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]) |> map { sharedData in @@ -196,6 +203,16 @@ public final class SharedAccountContextImpl: SharedAccountContext { } )) + self.mediaManager = MediaManagerImpl(accountManager: accountManager, inForeground: applicationBindings.applicationInForeground, presentationData: presentationData) + + self._autodownloadSettings.set(.single(initialPresentationDataAndSettings.autodownloadSettings) + |> then(accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings]) + |> map { sharedData in + let autodownloadSettings: AutodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings] as? AutodownloadSettings ?? .defaultSettings + return autodownloadSettings + } + )) + self.presentationDataDisposable.set((self.presentationData |> deliverOnMainQueue).start(next: { [weak self] next in if let strongSelf = self { @@ -267,6 +284,12 @@ public final class SharedAccountContextImpl: SharedAccountContext { } })) + self.currentAutodownloadSettingsDisposable.set(self._autodownloadSettings.get().start(next: { [weak self] next in + if let strongSelf = self { + let _ = strongSelf.currentAutodownloadSettings.swap(next) + } + })) + let startTime = CFAbsoluteTimeGetCurrent() let differenceDisposable = MetaDisposable() @@ -518,6 +541,24 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, { applicationBindings.openSettings() }) + }, isMediaPlaying: { [weak self] in + guard let strongSelf = self else { + return false + } + var result = false + let _ = (strongSelf.mediaManager.globalMediaPlayerState + |> take(1) + |> deliverOnMainQueue).start(next: { state in + if let (_, playbackState, _) = state, case let .state(value) = playbackState, case .playing = value.status.status { + result = true + } + }) + return result + }, resumeMediaPlayback: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.mediaManager.playlistControl(.playback(.play), type: nil) }, audioSession: self.mediaManager.audioSession, activeAccounts: self.activeAccounts |> map { _, accounts, _ in return Array(accounts.map({ $0.1 })) }) @@ -559,7 +600,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { resolvedText = .inProgress(nil) case .terminated: resolvedText = .none - case let .active(timestamp, _, _): + case .active(let timestamp, _, _), .reconnecting(let timestamp, _, _): resolvedText = .inProgress(timestamp) } } else { @@ -615,10 +656,31 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.updateNotificationTokensRegistration() - self.widgetDataContext = WidgetDataContext(basePath: self.basePath, activeAccount: self.activeAccounts - |> map { primary, _, _ in - return primary - }, presentationData: self.presentationData) + if applicationBindings.isMainApp { + self.widgetDataContext = WidgetDataContext(basePath: self.basePath, activeAccount: self.activeAccounts + |> map { primary, _, _ in + return primary + }, presentationData: self.presentationData) + + let enableSpotlight = accountManager.sharedData(keys: Set([ApplicationSpecificSharedDataKeys.intentsSettings])) + |> map { sharedData -> Bool in + let intentsSettings: IntentsSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.intentsSettings] as? IntentsSettings ?? .defaultSettings + return intentsSettings.contacts + } + |> distinctUntilChanged + self.spotlightDataContext = SpotlightDataContext(appBasePath: applicationBindings.containerPath, accountManager: accountManager, accounts: combineLatest(enableSpotlight, self.activeAccounts + |> map { _, accounts, _ in + return accounts.map { _, account, _ in + return account + } + }) |> map { enableSpotlight, accounts in + if enableSpotlight { + return accounts + } else { + return [] + } + }) + } } deinit { @@ -626,6 +688,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.registeredNotificationTokensDisposable.dispose() self.presentationDataDisposable.dispose() self.automaticMediaDownloadSettingsDisposable.dispose() + self.currentAutodownloadSettingsDisposable.dispose() self.inAppNotificationSettingsDisposable?.dispose() self.mediaInputSettingsDisposable?.dispose() self.callDisposable?.dispose() @@ -831,7 +894,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } public func makeTempAccountContext(account: Account) -> AccountContext { - return AccountContextImpl(sharedContext: self, account: account, tonContext: nil, limitsConfiguration: .defaultValue) + return AccountContextImpl(sharedContext: self, account: account/*, tonContext: nil*/, limitsConfiguration: .defaultValue, contentSettings: .default, temp: true) } public func openChatMessage(_ params: OpenChatMessageParams) -> Bool { @@ -943,8 +1006,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { handleTextLinkActionImpl(context: context, peerId: peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink) } - public func makePeerInfoController(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode) -> ViewController? { - let controller = peerInfoControllerImpl(context: context, peer: peer, mode: mode) + public func makePeerInfoController(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool) -> ViewController? { + let controller = peerInfoControllerImpl(context: context, peer: peer, mode: mode, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: fromChat) controller?.navigationPresentation = .modalInLargeLayout return controller } @@ -965,8 +1028,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return resolveUrlImpl(account: account, url: url) } - public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void) { - openResolvedUrlImpl(resolvedUrl, context: context, urlContext: urlContext, navigationController: navigationController, openPeer: openPeer, sendFile: sendFile, sendSticker: sendSticker, present: present, dismissInput: dismissInput) + public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) { + openResolvedUrlImpl(resolvedUrl, context: context, urlContext: urlContext, navigationController: navigationController, openPeer: openPeer, sendFile: sendFile, sendSticker: sendSticker, present: present, dismissInput: dismissInput, contentContext: contentContext) } public func makeDeviceContactInfoController(context: AccountContext, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController { @@ -1033,10 +1096,59 @@ public final class SharedAccountContextImpl: SharedAccountContext { return PeerSelectionControllerImpl(params) } - public func makeChatMessagePreviewItem(context: AccountContext, message: Message, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?) -> ListViewItem { - return ChatMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, animatedEmojiScale: 1.0, isPreview: true), context: context, chatLocation: .peer(message.id.peerId), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadNetworkType: .cellular, isRecentActions: false, isScheduledMessages: false, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus), controllerInteraction: defaultChatControllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes()), disableDate: true, additionalContent: nil) + public func makeChatMessagePreviewItem(context: AccountContext, message: Message, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)? = nil, clickThroughMessage: (() -> Void)? = nil) -> ListViewItem { + let controllerInteraction: ChatControllerInteraction + if tapMessage != nil || clickThroughMessage != nil { + controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, tapMessage: { message in + tapMessage?(message) + }, clickThroughMessage: { + clickThroughMessage?() + }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + }, presentController: { _, _ in }, navigationController: { + return nil + }, chatControllerNode: { + return nil + }, reactionContainerNode: { + return nil + }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in + }, canSetupReply: { _ in + return false + }, navigateToFirstDateMessage: { _ in + }, requestRedeliveryOfFailedMessages: { _ in + }, addContact: { _ in + }, rateCall: { _, _ in + }, requestSelectMessagePollOptions: { _, _ in + }, requestOpenMessagePollResults: { _, _ in + }, openAppStorePage: { + }, displayMessageTooltip: { _, _, _, _ in + }, seekToTimecode: { _, _, _ in + }, scheduleCurrentMessage: { + }, sendScheduledMessagesNow: { _ in + }, editScheduledMessagesTime: { _ in + }, performTextSelectionAction: { _, _, _ in + }, updateMessageReaction: { _, _ in + }, openMessageReactions: { _ in + }, displaySwipeToReplyHint: { + }, dismissReplyMarkupMessage: { _ in + }, openMessagePollResults: { _, _ in + }, openPollCreation: { _ in + }, requestMessageUpdate: { _ in + }, cancelInteractiveKeyboardGestures: { + }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, + pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false)) + } else { + controllerInteraction = defaultChatControllerInteraction + } + + return ChatMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), context: context, chatLocation: .peer(message.id.peerId), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadNetworkType: .cellular, isRecentActions: false, isScheduledMessages: false, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes()), disableDate: true, additionalContent: nil) } + public func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader { + return ChatMessageDateHeader(timestamp: timestamp, scheduled: false, presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), context: context) + } + + #if ENABLE_WALLET public func openWallet(context: AccountContext, walletContext: OpenWalletContext, present: @escaping (ViewController) -> Void) { guard let storedContext = context.tonContext else { return @@ -1096,6 +1208,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } }) } + #endif public func openImagePicker(context: AccountContext, completion: @escaping (UIImage) -> Void, present: @escaping (ViewController) -> Void) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -1128,6 +1241,33 @@ public final class SharedAccountContextImpl: SharedAccountContext { present(legacyController) }) } + + public func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController { + return recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: WebSessionsContext(account: context.account), websitesOnly: false) + } } private let defaultChatControllerInteraction = ChatControllerInteraction.default + +private func peerInfoControllerImpl(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool) -> ViewController? { + if let _ = peer as? TelegramGroup { + return PeerInfoScreen(context: context, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeer: false, callMessages: []) + } else if let channel = peer as? TelegramChannel { + return PeerInfoScreen(context: context, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeer: false, callMessages: []) + } else if peer is TelegramUser { + var nearbyPeer = false + var callMessages: [Message] = [] + switch mode { + case .nearbyPeer: + nearbyPeer = true + case let .calls(messages): + callMessages = messages + case .generic: + break + } + return PeerInfoScreen(context: context, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeer: nearbyPeer, callMessages: callMessages) + } else if peer is TelegramSecretChat { + return PeerInfoScreen(context: context, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeer: false, callMessages: []) + } + return nil +} diff --git a/submodules/TelegramUI/TelegramUI/SharedWakeupManager.swift b/submodules/TelegramUI/TelegramUI/SharedWakeupManager.swift index 962ac8a234..638751b63f 100644 --- a/submodules/TelegramUI/TelegramUI/SharedWakeupManager.swift +++ b/submodules/TelegramUI/TelegramUI/SharedWakeupManager.swift @@ -315,7 +315,7 @@ public final class SharedWakeupManager { if let taskId = self.beginBackgroundTask("background-wakeup", { handleExpiration() }) { - let timer = SwiftSignalKit.Timer(timeout: min(30.0, self.backgroundTimeRemaining()), repeat: false, completion: { + let timer = SwiftSignalKit.Timer(timeout: min(30.0, max(0.0, self.backgroundTimeRemaining() - 5.0)), repeat: false, completion: { handleExpiration() }, queue: Queue.mainQueue()) self.currentTask = (taskId, currentTime, timer) diff --git a/submodules/TelegramUI/TelegramUI/SpotlightContacts.swift b/submodules/TelegramUI/TelegramUI/SpotlightContacts.swift new file mode 100644 index 0000000000..0904964e64 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/SpotlightContacts.swift @@ -0,0 +1,275 @@ +import Foundation +import SwiftSignalKit +import Postbox +import SyncCore +import TelegramCore +import Display + +import CoreSpotlight +import MobileCoreServices + +private let roundCorners = { () -> UIImage in + let diameter: CGFloat = 60.0 + UIGraphicsBeginImageContextWithOptions(CGSize(width: diameter, height: diameter), false, 0.0) + let context = UIGraphicsGetCurrentContext()! + context.setBlendMode(.copy) + context.setFillColor(UIColor.black.cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: diameter, height: diameter))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: diameter, height: diameter))) + let image = UIGraphicsGetImageFromCurrentImageContext()!.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0)) + UIGraphicsEndImageContext() + return image +}() + +private struct SpotlightIndexStorageItem: Codable, Equatable { + var firstName: String + var lastName: String + var avatarSourcePath: String? +} + +private final class SpotlightIndexStorage { + private let appBasePath: String + private let basePath: String + private var items: [PeerId: SpotlightIndexStorageItem] = [:] + + init(appBasePath: String, basePath: String) { + self.appBasePath = appBasePath + self.basePath = basePath + + let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil) + + self.reload() + + if self.items.isEmpty { + CSSearchableIndex.default().deleteSearchableItems(withDomainIdentifiers: ["telegram-contacts"], completionHandler: { _ in }) + } + } + + private func path(peerId: PeerId) -> String { + return self.basePath + "/p:\(UInt64(bitPattern: peerId.toInt64()))" + } + + private func reload() { + self.items.removeAll() + + guard let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: self.basePath), includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsSubdirectoryDescendants], errorHandler: nil) else { + return + } + + while let item = enumerator.nextObject() { + guard let url = item as? NSURL else { + continue + } + guard let resourceValues = try? url.resourceValues(forKeys: [.isDirectoryKey]) else { + continue + } + if let value = resourceValues[.isDirectoryKey] as? Bool, !value { + continue + } + if let path = url.path, let directoryName = url.lastPathComponent, directoryName.hasPrefix("p:") { + let peerIdString = directoryName[directoryName.index(directoryName.startIndex, offsetBy: 2)...] + if let peerIdValue = UInt64(peerIdString) { + let peerId = PeerId(Int64(bitPattern: peerIdValue)) + + let item: SpotlightIndexStorageItem + if let itemData = try? Data(contentsOf: URL(fileURLWithPath: path + "/data.json")), let decodedItem = try? JSONDecoder().decode(SpotlightIndexStorageItem.self, from: itemData) { + item = decodedItem + } else { + let _ = try? FileManager.default.removeItem(atPath: path + "/data.json") + let _ = try? FileManager.default.removeItem(atPath: path + "/avatar.png") + item = SpotlightIndexStorageItem(firstName: "", lastName: "", avatarSourcePath: nil) + } + + self.items[peerId] = item + } + } + } + } + + func update(items: [PeerId: SpotlightIndexStorageItem]) { + let validPeerIds = Set(items.keys) + var removePeerIds: [PeerId] = [] + for (peerId, item) in self.items { + if !validPeerIds.contains(peerId) { + removePeerIds.append(peerId) + } + } + if !removePeerIds.isEmpty { + for peerId in removePeerIds { + let _ = try? FileManager.default.removeItem(atPath: self.path(peerId: peerId)) + self.items.removeValue(forKey: peerId) + } + + CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: removePeerIds.map { peerId in + return "contact-\(peerId.toInt64())" + }) + } + + var addToIndexItems: [CSSearchableItem] = [] + + for (peerId, item) in items { + let previousItem = self.items[peerId] + if previousItem != item { + var updatedAvatarSourcePath: String? + if let avatarSourcePath = item.avatarSourcePath, let _ = fileSize(self.appBasePath + "/" + avatarSourcePath) { + updatedAvatarSourcePath = avatarSourcePath + } + + var encodeItem = item + encodeItem.avatarSourcePath = updatedAvatarSourcePath + + if encodeItem == previousItem { + continue + } + + print("Spotlight: updating \(item.firstName) \(item.lastName)") + let path = self.path(peerId: peerId) + let _ = try? FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) + + var resolvedAvatarPath: String? + if previousItem?.avatarSourcePath != updatedAvatarSourcePath { + let avatarPath = path + "/avatar.png" + let _ = try? FileManager.default.removeItem(atPath: avatarPath) + + if let updatedAvatarSourcePathValue = updatedAvatarSourcePath, let avatarData = try? Data(contentsOf: URL(fileURLWithPath: self.appBasePath + "/" + updatedAvatarSourcePathValue)), let image = UIImage(data: avatarData) { + let size = CGSize(width: 120.0, height: 120.0) + let context = DrawingContext(size: size, scale: 1.0, clear: true) + context.withFlippedContext { c in + c.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) + c.setBlendMode(.destinationOut) + c.draw(roundCorners.cgImage!, in: CGRect(origin: CGPoint(), size: size)) + } + if let resultImage = context.generateImage(), let resultData = resultImage.pngData(), let _ = try? resultData.write(to: URL(fileURLWithPath: avatarPath)) { + resolvedAvatarPath = avatarPath + } else { + updatedAvatarSourcePath = nil + } + } + } + + let itemDataPath = path + "/data.json" + + let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeText as String) + attributeSet.version = "\(UInt64.random(in: 0 ..< UInt64.max))" + if !item.firstName.isEmpty && !item.lastName.isEmpty { + attributeSet.title = "\(item.firstName) \(item.lastName)" + } else if !item.firstName.isEmpty { + attributeSet.title = item.firstName + } else { + attributeSet.title = item.lastName + } + attributeSet.thumbnailURL = resolvedAvatarPath.flatMap(URL.init(fileURLWithPath:)) + let indexItem = CSSearchableItem(uniqueIdentifier: "contact-\(peerId.toInt64())", domainIdentifier: "telegram-contacts", attributeSet: attributeSet) + addToIndexItems.append(indexItem) + + encodeItem.avatarSourcePath = updatedAvatarSourcePath + if let data = try? JSONEncoder().encode(encodeItem) { + let _ = try? data.write(to: URL(fileURLWithPath: itemDataPath), options: [.atomic]) + } + + self.items[peerId] = item + } + } + + if !addToIndexItems.isEmpty { + CSSearchableIndex.default().indexSearchableItems(addToIndexItems, completionHandler: { error in + if let error = error { + Logger.shared.log("CSSearchableIndex", "indexSearchableItems error: \(error)") + } + }) + } + } +} + +private func manageableSpotlightContacts(appBasePath: String, accounts: Signal<[Account], NoError>) -> Signal<[PeerId: SpotlightIndexStorageItem], NoError> { + let queue = Queue() + return accounts + |> mapToSignal { accounts -> Signal<[[PeerId: SpotlightIndexStorageItem]], NoError> in + return combineLatest(queue: queue, accounts.map { account -> Signal<[PeerId: SpotlightIndexStorageItem], NoError> in + return account.postbox.contactPeersView(accountPeerId: account.peerId, includePresences: false) + |> map { view -> [PeerId: SpotlightIndexStorageItem] in + var result: [PeerId: SpotlightIndexStorageItem] = [:] + for peer in view.peers { + if let user = peer as? TelegramUser { + let avatarSourcePath = smallestImageRepresentation(user.photo).flatMap { representation -> String? in + let resourcePath = account.postbox.mediaBox.resourcePath(representation.resource) + if resourcePath.hasPrefix(appBasePath + "/") { + return String(resourcePath[resourcePath.index(resourcePath.startIndex, offsetBy: appBasePath.count + 1)...]) + } else { + return resourcePath + } + } + result[user.id] = SpotlightIndexStorageItem(firstName: user.firstName ?? "", lastName: user.lastName ?? "", avatarSourcePath: avatarSourcePath) + } + } + return result + } + |> distinctUntilChanged + }) + } + |> map { accountContacts -> [PeerId: SpotlightIndexStorageItem] in + var result: [PeerId: SpotlightIndexStorageItem] = [:] + for singleAccountContacts in accountContacts { + for (peerId, contact) in singleAccountContacts { + if result[peerId] == nil { + result[peerId] = contact + } + } + } + return result + } +} + +private final class SpotlightDataContextImpl { + private let queue: Queue + private let appBasePath: String + private let accountManager: AccountManager + private let indexStorage: SpotlightIndexStorage + + private var listDisposable: Disposable? + + init(queue: Queue, appBasePath: String, accountManager: AccountManager, accounts: Signal<[Account], NoError>) { + self.queue = queue + self.appBasePath = appBasePath + self.accountManager = accountManager + self.indexStorage = SpotlightIndexStorage(appBasePath: appBasePath, basePath: accountManager.basePath + "/spotlight") + + self.listDisposable = (manageableSpotlightContacts(appBasePath: appBasePath, accounts: accounts + |> map { accounts in + return accounts.sorted(by: { $0.id < $1.id }) + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs.count != rhs.count { + return false + } + for i in 0 ..< lhs.count { + if lhs[i] !== rhs[i] { + return false + } + } + return true + })) + |> deliverOn(self.queue)).start(next: { [weak self] items in + guard let strongSelf = self else { + return + } + strongSelf.updateContacts(items: items) + }) + } + + private func updateContacts(items: [PeerId: SpotlightIndexStorageItem]) { + self.indexStorage.update(items: items) + } +} + +public final class SpotlightDataContext { + private let impl: QueueLocalObject + + public init(appBasePath: String, accountManager: AccountManager, accounts: Signal<[Account], NoError>) { + let queue = Queue() + self.impl = QueueLocalObject(queue: queue, generate: { + return SpotlightDataContextImpl(queue: queue, appBasePath: appBasePath, accountManager: accountManager, accounts: accounts) + }) + } +} diff --git a/submodules/TelegramUI/TelegramUI/StickerPanePeerSpecificSetupGridItem.swift b/submodules/TelegramUI/TelegramUI/StickerPanePeerSpecificSetupGridItem.swift index 664cd70303..885253d798 100644 --- a/submodules/TelegramUI/TelegramUI/StickerPanePeerSpecificSetupGridItem.swift +++ b/submodules/TelegramUI/TelegramUI/StickerPanePeerSpecificSetupGridItem.swift @@ -148,7 +148,7 @@ class StickerPanePeerSpecificSetupGridItemNode: GridItemNode { let textSpacing: CGFloat = 3.0 let buttonSpacing: CGFloat = 6.0 - let (installLayout, installApply) = makeInstallLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Stickers_GroupChooseStickerPack, font: buttonFont, textColor: item.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (installLayout, installApply) = makeInstallLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Stickers_GroupChooseStickerPack, font: buttonFont, textColor: item.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Stickers_GroupStickers.uppercased(), font: titleFont, textColor: item.theme.chat.inputMediaPanel.stickersSectionTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset - 20.0 - installLayout.size.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) diff --git a/submodules/TelegramUI/TelegramUI/StickerPaneSearchContentNode.swift b/submodules/TelegramUI/TelegramUI/StickerPaneSearchContentNode.swift index 44de43fd83..5f38d14eef 100644 --- a/submodules/TelegramUI/TelegramUI/StickerPaneSearchContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/StickerPaneSearchContentNode.swift @@ -7,20 +7,23 @@ import Postbox import TelegramCore import SyncCore import TelegramPresentationData +import PresentationDataUtils import LegacyComponents import MergeLists import AccountContext import StickerPackPreviewUI import Emoji import AppBundle +import OverlayStatusController +import UndoUI final class StickerPaneSearchInteraction { let open: (StickerPackCollectionInfo) -> Void - let install: (StickerPackCollectionInfo) -> Void + let install: (StickerPackCollectionInfo, [ItemCollectionItem]) -> Void let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Void let getItemIsPreviewed: (StickerPackItem) -> Bool - init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo) -> Void, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { + init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo, [ItemCollectionItem]) -> Void, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { self.open = open self.install = install self.sendSticker = sendSticker @@ -35,13 +38,13 @@ private enum StickerSearchEntryId: Equatable, Hashable { private enum StickerSearchEntry: Identifiable, Comparable { case sticker(index: Int, code: String?, stickerItem: FoundStickerItem, theme: PresentationTheme) - case global(index: Int, info: StickerPackCollectionInfo, topItems: [StickerPackItem], installed: Bool) + case global(index: Int, info: StickerPackCollectionInfo, topItems: [StickerPackItem], installed: Bool, topSeparator: Bool) var stableId: StickerSearchEntryId { switch self { case let .sticker(_, code, stickerItem, _): return .sticker(code, stickerItem.file.fileId.id) - case let .global(_, info, _, _): + case let .global(_, info, _, _, _): return .global(info.id) } } @@ -66,8 +69,8 @@ private enum StickerSearchEntry: Identifiable, Comparable { } else { return false } - case let .global(index, info, topItems, installed): - if case .global(index, info, topItems, installed) = rhs { + case let .global(index, info, topItems, installed, topSeparator): + if case .global(index, info, topItems, installed, topSeparator) = rhs { return true } else { return false @@ -84,11 +87,11 @@ private enum StickerSearchEntry: Identifiable, Comparable { default: return true } - case let .global(lhsIndex, _, _, _): + case let .global(lhsIndex, _, _, _, _): switch rhs { case .sticker: return false - case let .global(rhsIndex, _, _, _): + case let .global(rhsIndex, _, _, _, _): return lhsIndex < rhsIndex } } @@ -100,11 +103,11 @@ private enum StickerSearchEntry: Identifiable, Comparable { return StickerPaneSearchStickerItem(account: account, code: code, stickerItem: stickerItem, inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { node, rect in interaction.sendSticker(.standalone(media: stickerItem.file), node, rect) }) - case let .global(_, info, topItems, installed): - return StickerPaneSearchGlobalItem(account: account, theme: theme, strings: strings, info: info, topItems: topItems, grid: false, installed: installed, unread: false, open: { + case let .global(_, info, topItems, installed, topSeparator): + return StickerPaneSearchGlobalItem(account: account, theme: theme, strings: strings, info: info, topItems: topItems, grid: false, topSeparator: topSeparator, installed: installed, unread: false, open: { interaction.open(info) }, install: { - interaction.install(info) + interaction.install(info, topItems) }, getItemIsPreviewed: { item in return interaction.getItemIsPreviewed(item) }) @@ -171,6 +174,8 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { var deactivateSearchBar: (() -> Void)? var updateActivity: ((Bool) -> Void)? + private let installDisposable = MetaDisposable() + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) { self.context = context self.controllerInteraction = controllerInteraction @@ -181,7 +186,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { self.trendingPane = ChatMediaInputTrendingPane(context: context, controllerInteraction: controllerInteraction, getItemIsPreviewed: { [weak inputNodeInteraction] item in return inputNodeInteraction?.previewedStickerPackItem == .pack(item) - }) + }, isPane: false) self.gridNode = GridNode() @@ -217,36 +222,97 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { self.interaction = StickerPaneSearchInteraction(open: { [weak self] info in if let strongSelf = self { strongSelf.view.window?.endEditing(true) - - let controller = StickerPackPreviewController(context: strongSelf.context, stickerPack: .id(id: info.id.id, accessHash: info.accessHash), parentNavigationController: strongSelf.controllerInteraction.navigationController()) - controller.sendSticker = { [weak self] fileReference, sourceNode, sourceRect in + let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) + let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { [weak self] fileReference, sourceNode, sourceRect in if let strongSelf = self { return strongSelf.controllerInteraction.sendSticker(fileReference, false, sourceNode, sourceRect) } else { return false } - } - strongSelf.controllerInteraction.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }) + strongSelf.controllerInteraction.presentController(controller, nil) } - }, install: { [weak self] info in - if let strongSelf = self { - let _ = (loadedStickerPack(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false) - |> mapToSignal { result -> Signal in - switch result { - case let .result(info, items, installed): - if installed { + }, install: { [weak self] info, items in + guard let strongSelf = self else { + return + } + let account = strongSelf.context.account + var installSignal = loadedStickerPack(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false) + |> mapToSignal { result -> Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError> in + switch result { + case let .result(info, items, installed): + if installed { + return .complete() + } else { + return preloadedStickerPackThumbnail(account: account, info: info, items: items) + |> filter { $0 } + |> ignoreValues + |> then( + addStickerPackInteractively(postbox: strongSelf.context.account.postbox, info: info, items: items) + |> ignoreValues + ) + |> mapToSignal { _ -> Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError> in return .complete() - } else { - return addStickerPackInteractively(postbox: strongSelf.context.account.postbox, info: info, items: items) } - case .fetching: - break - case .none: - break + |> then(.single((info, items))) } - return .complete() - }).start() + case .fetching: + break + case .none: + break + } + return .complete() } + |> deliverOnMainQueue + + let context = strongSelf.context + var cancelImpl: (() -> Void)? + let progressSignal = Signal { subscriber in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + self?.controllerInteraction.presentController(controller, nil) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.12, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + installSignal = installSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { + self?.installDisposable.set(nil) + } + + strongSelf.installDisposable.set(installSignal.start(next: { info, items in + guard let strongSelf = self else { + return + } + + var animateInAsReplacement = false + if let navigationController = strongSelf.controllerInteraction.navigationController() { + for controller in navigationController.overlayControllers { + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitActionAndReplacementAnimation() + animateInAsReplacement = true + } + } + } + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + strongSelf.controllerInteraction.navigationController()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).0, undo: false, info: info, topItem: items.first, account: strongSelf.context.account), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in + return true + })) + })) }, sendSticker: { [weak self] file, sourceNode, sourceRect in if let strongSelf = self { let _ = strongSelf.controllerInteraction.sendSticker(file, false, sourceNode, sourceRect) @@ -263,6 +329,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { deinit { self.searchDisposable.dispose() + self.installDisposable.dispose() } func updateText(_ text: String, languageCode: String?) { @@ -323,17 +390,57 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { let local = searchStickerSets(postbox: context.account.postbox, query: text) let remote = searchStickerSetsRemotely(network: context.account.network, query: text) |> delay(0.2, queue: Queue.mainQueue()) - let packs = local + let rawPacks = local |> mapToSignal { result -> Signal<(FoundStickerSets, Bool, FoundStickerSets?), NoError> in var localResult = result if let currentRemote = self.currentRemotePacks.with ({ $0 }) { localResult = localResult.merge(with: currentRemote) } return .single((localResult, false, nil)) - |> then(remote |> map { remote -> (FoundStickerSets, Bool, FoundStickerSets?) in - return (result.merge(with: remote), true, remote) - }) + |> then( + remote + |> map { remote -> (FoundStickerSets, Bool, FoundStickerSets?) in + return (result.merge(with: remote), true, remote) + } + ) } + + let installedPackIds = context.account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])]) + |> map { view -> Set in + var installedPacks = Set() + if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionInfosView { + if let packsEntries = stickerPacksView.entriesByNamespace[Namespaces.ItemCollection.CloudStickerPacks] { + for entry in packsEntries { + installedPacks.insert(entry.id) + } + } + } + return installedPacks + } + |> distinctUntilChanged + let packs = combineLatest(rawPacks, installedPackIds) + |> map { packs, installedPackIds -> (FoundStickerSets, Bool, FoundStickerSets?) in + var (localPacks, completed, remotePacks) = packs + + for i in 0 ..< localPacks.infos.count { + let installed = installedPackIds.contains(localPacks.infos[i].0) + if installed != localPacks.infos[i].3 { + localPacks.infos[i].3 = installed + } + } + + if remotePacks != nil { + for i in 0 ..< remotePacks!.infos.count { + let installed = installedPackIds.contains(remotePacks!.infos[i].0) + if installed != remotePacks!.infos[i].3 { + remotePacks!.infos[i].3 = installed + } + } + } + + return (localPacks, completed, remotePacks) + } + signal = combineLatest(stickers, packs) |> map { stickers, packs -> ([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)? in return (stickers, packs.0, packs.1, packs.2) @@ -375,6 +482,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { existingStickerIds.insert(id) } } + var isFirstGlobal = true for (collectionId, info, _, installed) in packs.infos { if let info = info as? StickerPackCollectionInfo { var topItems: [StickerPackItem] = [] @@ -385,7 +493,8 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { } } } - entries.append(.global(index: index, info: info, topItems: topItems, installed: installed)) + entries.append(.global(index: index, info: info, topItems: topItems, installed: installed, topSeparator: !isFirstGlobal)) + isFirstGlobal = false index += 1 } } diff --git a/submodules/TelegramUI/TelegramUI/StickerPaneSearchGlobaltem.swift b/submodules/TelegramUI/TelegramUI/StickerPaneSearchGlobaltem.swift index a8754bcf8f..b6041c4349 100644 --- a/submodules/TelegramUI/TelegramUI/StickerPaneSearchGlobaltem.swift +++ b/submodules/TelegramUI/TelegramUI/StickerPaneSearchGlobaltem.swift @@ -7,6 +7,7 @@ import SyncCore import SwiftSignalKit import Postbox import TelegramPresentationData +import StickerPackPreviewUI final class StickerPaneSearchGlobalSection: GridSection { let height: CGFloat = 0.0 @@ -39,7 +40,9 @@ final class StickerPaneSearchGlobalItem: GridItem { let info: StickerPackCollectionInfo let topItems: [StickerPackItem] let grid: Bool + let topSeparator: Bool let installed: Bool + let installing: Bool let unread: Bool let open: () -> Void let install: () -> Void @@ -47,17 +50,19 @@ final class StickerPaneSearchGlobalItem: GridItem { let section: GridSection? = StickerPaneSearchGlobalSection() var fillsRowWithHeight: CGFloat? { - return self.grid ? nil : 128.0 + return self.grid ? nil : (128.0 + (self.topSeparator ? 12.0 : 0.0)) } - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, info: StickerPackCollectionInfo, topItems: [StickerPackItem], grid: Bool, installed: Bool, unread: Bool, open: @escaping () -> Void, install: @escaping () -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, info: StickerPackCollectionInfo, topItems: [StickerPackItem], grid: Bool, topSeparator: Bool, installed: Bool, installing: Bool = false, unread: Bool, open: @escaping () -> Void, install: @escaping () -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { self.account = account self.theme = theme self.strings = strings self.info = info self.topItems = topItems self.grid = grid + self.topSeparator = topSeparator self.installed = installed + self.installing = installing self.unread = unread self.open = open self.install = install @@ -81,7 +86,7 @@ final class StickerPaneSearchGlobalItem: GridItem { private let titleFont = Font.bold(16.0) private let statusFont = Font.regular(15.0) -private let buttonFont = Font.medium(13.0) +private let buttonFont = Font.semibold(13.0) class StickerPaneSearchGlobalItemNode: GridItemNode { private let titleNode: TextNode @@ -91,10 +96,14 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { private let installBackgroundNode: ASImageNode private let installButtonNode: HighlightTrackingButtonNode private var itemNodes: [TrendingTopItemNode] + private let topSeparatorNode: ASDisplayNode private var item: StickerPaneSearchGlobalItem? private var appliedItem: StickerPaneSearchGlobalItem? private let preloadDisposable = MetaDisposable() + private let preloadedStickerPackThumbnailDisposable = MetaDisposable() + + private var preloadedThumbnail = false override var isVisibleInGrid: Bool { didSet { @@ -102,6 +111,12 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { for node in self.itemNodes { node.visibility = self.isVisibleInGrid } + + if let item = self.item, self.isVisibleInGrid, !self.preloadedThumbnail { + self.preloadedThumbnail = true + + self.preloadedStickerPackThumbnailDisposable.set(preloadedStickerPackThumbnail(account: item.account, info: item.info, items: item.topItems).start()) + } } } } @@ -134,6 +149,9 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { self.installButtonNode = HighlightTrackingButtonNode() + self.topSeparatorNode = ASDisplayNode() + self.topSeparatorNode.isLayerBacked = true + self.itemNodes = [] super.init() @@ -144,6 +162,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { self.addSubnode(self.installBackgroundNode) self.addSubnode(self.installTextNode) self.addSubnode(self.installButtonNode) + self.addSubnode(self.topSeparatorNode) self.installButtonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { @@ -166,6 +185,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { deinit { self.preloadDisposable.dispose() + self.preloadedStickerPackThumbnailDisposable.dispose() } override func didLoad() { @@ -193,6 +213,15 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { let params = ListViewItemLayoutParams(width: self.bounds.size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: self.bounds.height) + var topOffset: CGFloat = 12.0 + if item.topSeparator { + topOffset += 12.0 + } + + self.topSeparatorNode.isHidden = !item.topSeparator + self.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: CGSize(width: params.width - 16.0 * 2.0, height: UIScreenPixel)) + self.topSeparatorNode.backgroundColor = item.theme.chat.inputMediaPanel.stickersSectionTextColor.withAlphaComponent(0.3) + let makeInstallLayout = TextNode.asyncLayout(self.installTextNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode) @@ -208,9 +237,8 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { let leftInset: CGFloat = 14.0 let rightInset: CGFloat = 16.0 - let topOffset: CGFloat = 12.0 - let (installLayout, installApply) = makeInstallLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Stickers_Install, font: buttonFont, textColor: item.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (installLayout, installApply) = makeInstallLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Stickers_Install, font: buttonFont, textColor: item.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.info.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset - 20.0 - installLayout.size.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -226,8 +254,8 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { strongSelf.installBackgroundNode.image = updateButtonBackgroundImage } - let installWidth: CGFloat = installLayout.size.width + 20.0 - let buttonFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - rightInset - installWidth, y: 4.0 + topOffset), size: CGSize(width: installWidth, height: 26.0)) + let installWidth: CGFloat = installLayout.size.width + 32.0 + let buttonFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - rightInset - installWidth, y: 4.0 + topOffset), size: CGSize(width: installWidth, height: 28.0)) strongSelf.installBackgroundNode.frame = buttonFrame strongSelf.installTextNode.frame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - installLayout.size.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - installLayout.size.height) / 2.0) + 1.0), size: installLayout.size) strongSelf.installButtonNode.frame = buttonFrame diff --git a/submodules/TelegramUI/TelegramUI/StickersChatInputContextPanelNode.swift b/submodules/TelegramUI/TelegramUI/StickersChatInputContextPanelNode.swift index 9d1bef1ad8..0075a48ca7 100644 --- a/submodules/TelegramUI/TelegramUI/StickersChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/StickersChatInputContextPanelNode.swift @@ -7,6 +7,7 @@ import SyncCore import Display import SwiftSignalKit import TelegramPresentationData +import TelegramUIPreferences import MergeLists import AccountContext import StickerPackPreviewUI @@ -80,7 +81,7 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { private var stickerPreviewController: StickerPreviewController? - override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { + override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { self.strings = strings self.listView = ListView() @@ -92,7 +93,7 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { self.stickersInteraction = StickersChatInputContextPanelInteraction() - super.init(context: context, theme: theme, strings: strings) + super.init(context: context, theme: theme, strings: strings, fontSize: fontSize) self.isOpaque = false self.clipsToBounds = true @@ -147,14 +148,14 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { switch attribute { case let .Sticker(_, packReference, _): if let packReference = packReference { - let controller = StickerPackPreviewController(context: strongSelf.context, stickerPack: packReference, parentNavigationController: controllerInteraction.navigationController()) - controller.sendSticker = { file, sourceNode, sourceRect in + let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { return controllerInteraction.sendSticker(file, true, sourceNode, sourceRect) } else { return false } - } + + }) controllerInteraction.navigationController()?.view.window?.endEditing(true) controllerInteraction.presentController(controller, nil) @@ -167,7 +168,7 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { } return true }), - PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: { _, _ in return true }) + PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true }) ] return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) } else { @@ -319,29 +320,8 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: listViewCurve) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve) self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) diff --git a/submodules/TelegramUI/TelegramUI/TelegramAccountAuxiliaryMethods.swift b/submodules/TelegramUI/TelegramUI/TelegramAccountAuxiliaryMethods.swift index 827767d176..c0859cb01a 100644 --- a/submodules/TelegramUI/TelegramUI/TelegramAccountAuxiliaryMethods.swift +++ b/submodules/TelegramUI/TelegramUI/TelegramAccountAuxiliaryMethods.swift @@ -7,6 +7,7 @@ import PassportUI import OpenInExternalAppUI import MusicAlbumArtResources import LocalMediaResources +import LocationResources public let telegramAccountAuxiliaryMethods = AccountAuxiliaryMethods(updatePeerChatInputState: { interfaceState, inputState -> PeerChatInterfaceState? in if interfaceState == nil { @@ -25,8 +26,6 @@ public let telegramAccountAuxiliaryMethods = AccountAuxiliaryMethods(updatePeerC return fetchLocalFileGifMediaResource(resource: resource) } else if let photoLibraryResource = resource as? PhotoLibraryMediaResource { return fetchPhotoLibraryResource(localIdentifier: photoLibraryResource.localIdentifier) - } else if let mapSnapshotResource = resource as? MapSnapshotMediaResource { - return .never() } else if let resource = resource as? ExternalMusicAlbumArtResource { return fetchExternalMusicAlbumArtResource(account: account, resource: resource) } else if let resource = resource as? ICloudFileResource { @@ -37,6 +36,8 @@ public let telegramAccountAuxiliaryMethods = AccountAuxiliaryMethods(updatePeerC return fetchOpenInAppIconResource(resource: resource) } else if let resource = resource as? EmojiSpriteResource { return fetchEmojiSpriteResource(postbox: account.postbox, network: account.network, resource: resource) + } else if let resource = resource as? VenueIconResource { + return fetchVenueIconResource(account: account, resource: resource) } return nil }, fetchResourceMediaReferenceHash: { resource in diff --git a/submodules/TelegramUI/TelegramUI/TelegramRootController.swift b/submodules/TelegramUI/TelegramUI/TelegramRootController.swift index 1e93467e36..9e44967397 100644 --- a/submodules/TelegramUI/TelegramUI/TelegramRootController.swift +++ b/submodules/TelegramUI/TelegramUI/TelegramRootController.swift @@ -120,6 +120,19 @@ public final class TelegramRootController: NavigationController { self.accountSettingsController = accountSettingsController self.rootTabController = tabBarController self.pushViewController(tabBarController, animated: false) + +// let _ = (archivedStickerPacks(account: self.context.account, namespace: .stickers) +// |> deliverOnMainQueue).start(next: { [weak self] stickerPacks in +// var packs: [(StickerPackCollectionInfo, StickerPackItem?)] = [] +// for pack in stickerPacks { +// packs.append((pack.info, pack.topItems.first)) +// } +// +// if let strongSelf = self { +// let controller = archivedStickersNoticeController(context: strongSelf.context, archivedStickerPacks: packs) +// strongSelf.chatListController?.present(controller, in: .window(.root)) +// } +// }) } public func updateRootControllers(showCallsTab: Bool) { diff --git a/submodules/TelegramUI/TelegramUI/TextLinkHandling.swift b/submodules/TelegramUI/TelegramUI/TextLinkHandling.swift index 1697a207aa..14401f2ffa 100644 --- a/submodules/TelegramUI/TelegramUI/TextLinkHandling.swift +++ b/submodules/TelegramUI/TelegramUI/TextLinkHandling.swift @@ -32,7 +32,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate peerSignal = context.account.postbox.loadedPeerWithId(peerId) |> map(Optional.init) navigateDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { peer in if let controller = controller, let peer = peer { - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { (controller.navigationController as? NavigationController)?.pushViewController(infoController) } } @@ -42,7 +42,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate } }, sendFile: nil, sendSticker: nil, - present: presentImpl, dismissInput: {}) + present: presentImpl, dismissInput: {}, contentContext: nil) } let openLinkImpl: (String) -> Void = { [weak controller] url in @@ -58,17 +58,20 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), subject: .message(messageId))) } case let .stickerPack(name): - controller.present(StickerPackPreviewController(context: context, stickerPack: .name(name), parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root)) + let packReference: StickerPackReference = .name(name) + controller.present(StickerPackScreen(context: context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root)) case let .instantView(webpage, anchor): (controller.navigationController as? NavigationController)?.pushViewController(InstantPageController(context: context, webPage: webpage, sourcePeerType: .group, anchor: anchor)) case let .join(link): controller.present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peerId in openResolvedPeerImpl(peerId, .chat(textInputState: nil, subject: nil)) - }), in: .window(.root)) + }, parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root)) + #if ENABLE_WALLET case let .wallet(address, amount, comment): context.sharedContext.openWallet(context: context, walletContext: .send(address: address, amount: amount, comment: comment)) { c in (controller.navigationController as? NavigationController)?.pushViewController(c) } + #endif default: break } @@ -105,7 +108,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate case let .url(url): let canOpenIn = availableOpenInOptions(context: context, item: .url(url: url)).count > 1 let openText = canOpenIn ? presentationData.strings.Conversation_FileOpenIn : presentationData.strings.Conversation_LinkDialogOpen - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: url), ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in @@ -123,13 +126,13 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate } }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) controller.present(actionSheet, in: .window(.root)) case let .mention(mention): - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: mention), ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in @@ -141,13 +144,13 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate UIPasteboard.general.string = mention }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) controller.present(actionSheet, in: .window(.root)) case let .hashtag(_, hashtag): - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: hashtag), ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in @@ -160,7 +163,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate UIPasteboard.general.string = hashtag }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) diff --git a/submodules/TelegramUI/TelegramUI/ThemeUpdateManager.swift b/submodules/TelegramUI/TelegramUI/ThemeUpdateManager.swift index a6c3d8f9e8..2371c501a3 100644 --- a/submodules/TelegramUI/TelegramUI/ThemeUpdateManager.swift +++ b/submodules/TelegramUI/TelegramUI/ThemeUpdateManager.swift @@ -84,7 +84,7 @@ final class ThemeUpdateManagerImpl: ThemeUpdateManager { guard let file = theme.file else { return .complete() } - return telegramThemeData(account: account, accountManager: accountManager, resource: file.resource) + return telegramThemeData(account: account, accountManager: accountManager, reference: .standalone(resource: file.resource)) |> mapToSignal { data -> Signal<(PresentationThemeReference, PresentationTheme?), NoError> in guard let data = data, let presentationTheme = makePresentationTheme(data: data) else { return .complete() @@ -104,17 +104,17 @@ final class ThemeUpdateManagerImpl: ThemeUpdateManager { |> mapToSignal { wallpaper -> Signal<(PresentationThemeReference, PresentationTheme?), NoError> in if let wallpaper = wallpaper, case let .file(file) = wallpaper { var convertedRepresentations: [ImageRepresentationWithReference] = [] - convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource), reference: .media(media: .standalone(media: file.file), resource: file.file.resource))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) return wallpaperDatas(account: account, accountManager: accountManager, fileReference: .standalone(media: file.file), representations: convertedRepresentations, alwaysShowThumbnailFirst: false, thumbnail: false, onlyFullSize: true, autoFetchFullSize: true, synchronousLoad: false) |> mapToSignal { _, fullSizeData, complete -> Signal<(PresentationThemeReference, PresentationTheme?), NoError> in guard complete, let fullSizeData = fullSizeData else { return .complete() } - accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData) - return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: wallpaper)), presentationTheme)) + accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData, synchronous: true) + return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: wallpaper, creatorAccountId: theme.isCreator ? account.id : nil)), presentationTheme)) } } else { - return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil)), presentationTheme)) + return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: theme.isCreator ? account.id : nil)), presentationTheme)) } } } @@ -131,25 +131,15 @@ final class ThemeUpdateManagerImpl: ThemeUpdateManager { current = PresentationThemeSettings.defaultSettings } - var chatWallpaper = current.chatWallpaper + var theme = current.theme var automaticThemeSwitchSetting = current.automaticThemeSwitchSetting if isAutoNight { automaticThemeSwitchSetting.theme = updatedTheme } else { - if let themeSpecificWallpaper = current.themeSpecificChatWallpapers[updatedTheme.index] { - chatWallpaper = themeSpecificWallpaper - } else if let presentationTheme = presentationTheme { - if case let .cloud(info) = updatedTheme, let resolvedWallpaper = info.resolvedWallpaper { - chatWallpaper = resolvedWallpaper - } else { - chatWallpaper = presentationTheme.chat.defaultWallpaper - } - } else { - chatWallpaper = current.chatWallpaper - } + theme = updatedTheme } - return PresentationThemeSettings(chatWallpaper: chatWallpaper, theme: updatedTheme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: current.themeSpecificChatWallpapers, fontSize: current.fontSize, automaticThemeSwitchSetting: automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) + return PresentationThemeSettings(theme: theme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: current.themeSpecificChatWallpapers, useSystemFont: current.useSystemFont, fontSize: current.fontSize, listsFontSize: current.listsFontSize, chatBubbleSettings: current.chatBubbleSettings, automaticThemeSwitchSetting: automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) }) }).start() } diff --git a/submodules/TelegramUI/TelegramUI/TransformOutgoingMessageMedia.swift b/submodules/TelegramUI/TelegramUI/TransformOutgoingMessageMedia.swift index 36e6875435..37aa45c842 100644 --- a/submodules/TelegramUI/TelegramUI/TransformOutgoingMessageMedia.swift +++ b/submodules/TelegramUI/TelegramUI/TransformOutgoingMessageMedia.swift @@ -161,7 +161,7 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me let thumbnailResource = LocalFileMediaResource(fileId: arc4random64()) postbox.mediaBox.storeResourceData(thumbnailResource.id, data: smallestData) representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(smallestSize), resource: thumbnailResource)) - let updatedImage = TelegramMediaImage(imageId: image.imageId, representations: representations, immediateThumbnailData: image.immediateThumbnailData, reference: image.reference, partialReference: image.partialReference) + let updatedImage = TelegramMediaImage(imageId: image.imageId, representations: representations, immediateThumbnailData: image.immediateThumbnailData, reference: image.reference, partialReference: image.partialReference, flags: []) return .single(.standalone(media: updatedImage)) } } diff --git a/submodules/TelegramUI/TelegramUI/UpgradedAccounts.swift b/submodules/TelegramUI/TelegramUI/UpgradedAccounts.swift index 72f5efb54f..6e8797da91 100644 --- a/submodules/TelegramUI/TelegramUI/UpgradedAccounts.swift +++ b/submodules/TelegramUI/TelegramUI/UpgradedAccounts.swift @@ -158,16 +158,16 @@ public func upgradedAccounts(accountManager: AccountManager, rootPath: String, e if let value = values[LegacyApplicationSpecificPreferencesKeyValues.presentationThemeSettings.key] as? PresentationThemeSettings { let mediaBox = MediaBox(basePath: path + "/postbox/media") - let wallpapers = [value.chatWallpaper] + Array(value.themeSpecificChatWallpapers.values) + let wallpapers = Array(value.themeSpecificChatWallpapers.values) for wallpaper in wallpapers { switch wallpaper { case let .file(file): if let path = mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data) let _ = accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start() - if file.isPattern { + if wallpaper.isPattern { if let color = file.settings.color, let intensity = file.settings.intensity { - let _ = accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPatternWallpaperRepresentation(color: color, intensity: intensity), complete: true, fetch: true).start() + let _ = accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPatternWallpaperRepresentation(color: color, bottomColor: file.settings.bottomColor, intensity: intensity, rotation: file.settings.rotation), complete: true, fetch: true).start() } } else { if file.settings.blur { diff --git a/submodules/TelegramUI/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift index 8b9ae73ea9..eae5612e39 100644 --- a/submodules/TelegramUI/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/VerticalListContextResultsChatInputContextPanelNode.swift @@ -6,8 +6,10 @@ import TelegramCore import SyncCore import Display import TelegramPresentationData +import TelegramUIPreferences import MergeLists import AccountContext +import SwiftSignalKit private enum VerticalChatContextResultsEntryStableId: Hashable { case action @@ -121,13 +123,17 @@ private func preparedTransition(from fromEntries: [VerticalListContextResultsCha final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContextPanelNode { private let listView: ListView - private var currentResults: ChatContextResultCollection? + private var currentExternalResults: ChatContextResultCollection? + private var currentProcessedResults: ChatContextResultCollection? private var currentEntries: [VerticalListContextResultsChatInputContextPanelEntry]? private var enqueuedTransitions: [(VerticalListContextResultsChatInputContextPanelTransition, Bool)] = [] private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)? - override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { + private let loadMoreDisposable = MetaDisposable() + private var isLoadingMore: Bool = false + + override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { self.listView = ListView() self.listView.isOpaque = false self.listView.stackFromBottom = true @@ -136,16 +142,39 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex self.listView.isHidden = true self.listView.view.disablesInteractiveTransitionGestureRecognizer = true - super.init(context: context, theme: theme, strings: strings) + super.init(context: context, theme: theme, strings: strings, fontSize: fontSize) self.isOpaque = false self.clipsToBounds = true self.addSubnode(self.listView) + + self.listView.visibleBottomContentOffsetChanged = { [weak self] offset in + guard let strongSelf = self, !strongSelf.isLoadingMore, case let .known(value) = offset, value < 40.0 else { + return + } + strongSelf.loadMore() + } + } + + deinit { + self.loadMoreDisposable.dispose() } func updateResults(_ results: ChatContextResultCollection) { - self.currentResults = results + if self.currentExternalResults == results { + return + } + self.currentExternalResults = results + self.currentProcessedResults = results + + self.isLoadingMore = false + self.loadMoreDisposable.set(nil) + + self.updateInternalResults(results) + } + + private func updateInternalResults(_ results: ChatContextResultCollection) { var entries: [VerticalListContextResultsChatInputContextPanelEntry] = [] var index = 0 var resultIds = Set() @@ -202,15 +231,13 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex var options = ListViewDeleteAndInsertOptions() if firstTime { - //options.insert(.Synchronous) - //options.insert(.LowLatency) } else { options.insert(.AnimateTopItemPosition) options.insert(.AnimateCrossfade) } var insets = UIEdgeInsets() - insets.top = topInsetForLayout(size: validLayout.0, hasSwitchPeer: self.currentResults?.switchPeer != nil) + insets.top = topInsetForLayout(size: validLayout.0, hasSwitchPeer: self.currentExternalResults?.switchPeer != nil) insets.left = validLayout.1 insets.right = validLayout.2 @@ -250,35 +277,14 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex self.validLayout = (size, leftInset, rightInset, bottomInset) var insets = UIEdgeInsets() - insets.top = self.topInsetForLayout(size: size, hasSwitchPeer: self.currentResults?.switchPeer != nil) + insets.top = self.topInsetForLayout(size: size, hasSwitchPeer: self.currentExternalResults?.switchPeer != nil) insets.left = leftInset insets.right = rightInset transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: listViewCurve) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve) self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) @@ -288,12 +294,12 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex } } - if self.theme !== interfaceState.theme, let currentResults = currentResults { + if self.theme !== interfaceState.theme, let currentProcessedResults = self.currentProcessedResults { self.theme = interfaceState.theme self.listView.keepBottomItemOverscrollBackground = self.theme.list.plainBackgroundColor let new = self.currentEntries?.map({$0.withUpdatedTheme(interfaceState.theme)}) ?? [] - prepareTransition(from: self.currentEntries, to: new, results: currentResults) + prepareTransition(from: self.currentEntries, to: new, results: currentProcessedResults) } } @@ -319,4 +325,33 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex let listViewFrame = self.listView.frame return self.listView.hitTest(CGPoint(x: point.x - listViewFrame.minX, y: point.y - listViewFrame.minY), with: event) } + + private func loadMore() { + guard !self.isLoadingMore, let currentProcessedResults = self.currentProcessedResults, let nextOffset = currentProcessedResults.nextOffset else { + return + } + self.isLoadingMore = true + self.loadMoreDisposable.set((requestChatContextResults(account: self.context.account, botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, location: .single(currentProcessedResults.geoPoint), offset: nextOffset) + |> deliverOnMainQueue).start(next: { [weak self] nextResults in + guard let strongSelf = self, let nextResults = nextResults else { + return + } + strongSelf.isLoadingMore = false + var results: [ChatContextResult] = [] + var existingIds = Set() + for result in currentProcessedResults.results { + results.append(result) + existingIds.insert(result.id) + } + for result in nextResults.results { + if !existingIds.contains(result.id) { + results.append(result) + existingIds.insert(result.id) + } + } + let mergedResults = ChatContextResultCollection(botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, geoPoint: currentProcessedResults.geoPoint, queryId: nextResults.queryId, nextOffset: nextResults.nextOffset, presentation: currentProcessedResults.presentation, switchPeer: currentProcessedResults.switchPeer, results: results, cacheTimeout: currentProcessedResults.cacheTimeout) + strongSelf.currentProcessedResults = mergedResults + strongSelf.updateInternalResults(mergedResults) + })) + } } diff --git a/submodules/TelegramUI/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift index 87a96b55f0..4306bfefe0 100644 --- a/submodules/TelegramUI/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift @@ -252,7 +252,7 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { updateIconImageSignal = chatMessageSticker(account: item.account, file: stickerFile, small: false, fetched: true) } else { let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 55, height: 55), resource: imageResource) - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) updateIconImageSignal = chatWebpageSnippetPhoto(account: item.account, photoReference: .standalone(media: tmpImage)) } } else { diff --git a/submodules/TelegramUI/TelegramUI/WalletContextImpl.swift b/submodules/TelegramUI/TelegramUI/WalletContextImpl.swift index bc76fed87d..f4e6630aa5 100644 --- a/submodules/TelegramUI/TelegramUI/WalletContextImpl.swift +++ b/submodules/TelegramUI/TelegramUI/WalletContextImpl.swift @@ -1,3 +1,5 @@ +#if ENABLE_WALLET + import Foundation import UIKit import Display @@ -199,8 +201,8 @@ final class WalletContextImpl: WalletContext { statusBarStyle: theme.rootController.statusBarStyle.style, navigationBar: navigationBarData.theme, keyboardAppearance: theme.rootController.keyboardColor.keyboardAppearance, - alert: AlertControllerTheme(presentationTheme: theme), - actionSheet: ActionSheetControllerTheme(presentationTheme: theme) + alert: AlertControllerTheme(presentationData: presentationData), + actionSheet: ActionSheetControllerTheme(presentationData: presentationData) ), strings: WalletStrings( primaryComponent: WalletStringsComponent( languageCode: strings.primaryComponent.languageCode, @@ -278,3 +280,5 @@ final class WalletContextImpl: WalletContext { }) } } + +#endif diff --git a/submodules/TelegramUI/TelegramUI/WallpaperPreviewMedia.swift b/submodules/TelegramUI/TelegramUI/WallpaperPreviewMedia.swift index 15f80886af..6d6433875d 100644 --- a/submodules/TelegramUI/TelegramUI/WallpaperPreviewMedia.swift +++ b/submodules/TelegramUI/TelegramUI/WallpaperPreviewMedia.swift @@ -5,8 +5,10 @@ import TelegramCore import SyncCore enum WallpaperPreviewMediaContent: Equatable { - case file(TelegramMediaFile, UIColor?, Bool, Bool) + case file(TelegramMediaFile, UIColor?, UIColor?, Int32?, Bool, Bool) case color(UIColor) + case gradient(UIColor, UIColor, Int32?) + case themeSettings(TelegramThemeSettings) } final class WallpaperPreviewMedia: Media { diff --git a/submodules/TelegramUI/TelegramUI/WallpaperUploadManager.swift b/submodules/TelegramUI/TelegramUI/WallpaperUploadManager.swift index 8bbdb76129..0527746dd8 100644 --- a/submodules/TelegramUI/TelegramUI/WallpaperUploadManager.swift +++ b/submodules/TelegramUI/TelegramUI/WallpaperUploadManager.swift @@ -105,6 +105,7 @@ final class WallpaperUploadManagerImpl: WallpaperUploadManager { return result } + let autoNightModeTriggered = presentationData.autoNightModeTriggered ?? false disposable.set(uploadSignal.start(next: { [weak self] status in guard let strongSelf = self else { return @@ -116,20 +117,24 @@ final class WallpaperUploadManagerImpl: WallpaperUploadManager { let _ = sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start(completed: {}) } - if strongSelf.currentPresentationData?.theme.name == presentationData.theme.name { - let _ = (updatePresentationThemeSettingsInteractively(accountManager: sharedContext.accountManager, { current in - let updatedWallpaper: TelegramWallpaper - if let currentSettings = current.chatWallpaper.settings { - updatedWallpaper = wallpaper.withUpdatedSettings(currentSettings) - } else { - updatedWallpaper = wallpaper - } - - var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers - themeSpecificChatWallpapers[current.theme.index] = updatedWallpaper - return PresentationThemeSettings(chatWallpaper: updatedWallpaper, theme: current.theme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) - })).start() - } + let _ = (updatePresentationThemeSettingsInteractively(accountManager: sharedContext.accountManager, { current in + let updatedWallpaper: TelegramWallpaper + if let currentSettings = currentWallpaper.settings { + updatedWallpaper = wallpaper.withUpdatedSettings(currentSettings) + } else { + updatedWallpaper = wallpaper + } + let themeReference: PresentationThemeReference + if autoNightModeTriggered { + themeReference = current.automaticThemeSwitchSetting.theme + } else { + themeReference = current.theme + } + var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers + themeSpecificChatWallpapers[themeReference.index] = updatedWallpaper + themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: current.themeSpecificAccentColors[themeReference.index])] = updatedWallpaper + return PresentationThemeSettings(theme: current.theme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, useSystemFont: current.useSystemFont, fontSize: current.fontSize, listsFontSize: current.listsFontSize, chatBubbleSettings: current.chatBubbleSettings, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) + })).start() } if case let .file(_, _, _, _, _, _, _, file, settings) = wallpaper, settings.blur { diff --git a/submodules/TelegramUI/TelegramUI/WebpagePreviewAccessoryPanelNode.swift b/submodules/TelegramUI/TelegramUI/WebpagePreviewAccessoryPanelNode.swift index 8518d7a1e4..9344843603 100644 --- a/submodules/TelegramUI/TelegramUI/WebpagePreviewAccessoryPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/WebpagePreviewAccessoryPanelNode.swift @@ -117,7 +117,7 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { } else { text = stringForMediaKind(mediaKind, strings: self.strings).0 } - } else if let files = content.files, content.type == "telegram_theme" { + } else if content.type == "telegram_theme" { text = strings.Message_Theme } else if let _ = content.image { text = stringForMediaKind(.image, strings: self.strings).0 diff --git a/submodules/TelegramUI/TelegramUI/WidgetDataContext.swift b/submodules/TelegramUI/TelegramUI/WidgetDataContext.swift index 1484f199c1..7bc692f053 100644 --- a/submodules/TelegramUI/TelegramUI/WidgetDataContext.swift +++ b/submodules/TelegramUI/TelegramUI/WidgetDataContext.swift @@ -32,7 +32,20 @@ final class WidgetDataContext { guard let user = peer as? TelegramUser else { return nil } - return WidgetDataPeer(id: user.id.toInt64(), name: user.shortNameOrPhone ?? "", letters: user.displayLetters, avatarPath: smallestImageRepresentation(user.photo).flatMap { representation in + + var name: String = "" + var lastName: String? + + if let firstName = user.firstName { + name = firstName + lastName = user.lastName + } else if let lastName = user.lastName { + name = lastName + } else if let phone = user.phone, !phone.isEmpty { + name = phone + } + + return WidgetDataPeer(id: user.id.toInt64(), name: name, lastName: lastName, letters: user.displayLetters, avatarPath: smallestImageRepresentation(user.photo).flatMap { representation in return account.postbox.mediaBox.resourcePath(representation.resource) }) })) diff --git a/submodules/TelegramUIPreferences/Sources/CallListSettings.swift b/submodules/TelegramUIPreferences/Sources/CallListSettings.swift index ff81d69b1a..1a7640f660 100644 --- a/submodules/TelegramUIPreferences/Sources/CallListSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/CallListSettings.swift @@ -3,22 +3,58 @@ import Postbox import SwiftSignalKit public struct CallListSettings: PreferencesEntry, Equatable { - public var showTab: Bool + public var _showTab: Bool? + public var defaultShowTab: Bool? public static var defaultSettings: CallListSettings { - return CallListSettings(showTab: false) + return CallListSettings(showTab: true) + } + + public var showTab: Bool { + get { + if let value = self._showTab { + return value + } else if let defaultValue = self.defaultShowTab { + return defaultValue + } else { + return CallListSettings.defaultSettings.showTab + } + } set { + self._showTab = newValue + } } public init(showTab: Bool) { - self.showTab = showTab + self._showTab = showTab + } + + public init(showTab: Bool?, defaultShowTab: Bool?) { + self._showTab = showTab + self.defaultShowTab = defaultShowTab } public init(decoder: PostboxDecoder) { - self.showTab = decoder.decodeInt32ForKey("showTab", orElse: 0) != 0 + var defaultValue = CallListSettings.defaultSettings.showTab + if let alternativeDefaultValue = decoder.decodeOptionalInt32ForKey("defaultShowTab") { + defaultValue = alternativeDefaultValue != 0 + self.defaultShowTab = alternativeDefaultValue != 0 + } + if let value = decoder.decodeOptionalInt32ForKey("showTab") { + self._showTab = value != 0 + } } public func encode(_ encoder: PostboxEncoder) { - encoder.encodeInt32(self.showTab ? 1 : 0, forKey: "showTab") + if let defaultShowTab = self.defaultShowTab { + encoder.encodeInt32(defaultShowTab ? 1 : 0, forKey: "defaultShowTab") + } else { + encoder.encodeNil(forKey: "defaultShowTab") + } + if let showTab = self._showTab { + encoder.encodeInt32(showTab ? 1 : 0, forKey: "showTab") + } else { + encoder.encodeNil(forKey: "showTab") + } } public func isEqual(to: PreferencesEntry) -> Bool { @@ -30,11 +66,11 @@ public struct CallListSettings: PreferencesEntry, Equatable { } public static func ==(lhs: CallListSettings, rhs: CallListSettings) -> Bool { - return lhs.showTab == rhs.showTab + return lhs._showTab == rhs._showTab && lhs.defaultShowTab == rhs.defaultShowTab } public func withUpdatedShowTab(_ showTab: Bool) -> CallListSettings { - return CallListSettings(showTab: showTab) + return CallListSettings(showTab: showTab, defaultShowTab: self.defaultShowTab) } } @@ -51,3 +87,17 @@ public func updateCallListSettingsInteractively(accountManager: AccountManager, }) } } + +public func storeCurrentCallListTabDefaultValue(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Void in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.callListSettings, { entry in + let currentSettings: CallListSettings + if let entry = entry as? CallListSettings { + currentSettings = entry + } else { + currentSettings = CallListSettings(showTab: nil, defaultShowTab: CallListSettings.defaultSettings.showTab) + } + return currentSettings + }) + } +} diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 0f0ded202d..575d4c6ba5 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -8,20 +8,18 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry { public var crashOnLongQueries: Bool public var chatListPhotos: Bool public var knockoutWallpaper: Bool - public var gradientBubbles: Bool public var wallets: Bool public static var defaultSettings: ExperimentalUISettings { - return ExperimentalUISettings(keepChatNavigationStack: false, skipReadHistory: false, crashOnLongQueries: false, chatListPhotos: false, knockoutWallpaper: false, gradientBubbles: false, wallets: false) + return ExperimentalUISettings(keepChatNavigationStack: false, skipReadHistory: false, crashOnLongQueries: false, chatListPhotos: false, knockoutWallpaper: false, wallets: false) } - public init(keepChatNavigationStack: Bool, skipReadHistory: Bool, crashOnLongQueries: Bool, chatListPhotos: Bool, knockoutWallpaper: Bool, gradientBubbles: Bool, wallets: Bool) { + public init(keepChatNavigationStack: Bool, skipReadHistory: Bool, crashOnLongQueries: Bool, chatListPhotos: Bool, knockoutWallpaper: Bool, wallets: Bool) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory self.crashOnLongQueries = crashOnLongQueries self.chatListPhotos = chatListPhotos self.knockoutWallpaper = knockoutWallpaper - self.gradientBubbles = gradientBubbles self.wallets = wallets } @@ -31,7 +29,6 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry { self.crashOnLongQueries = decoder.decodeInt32ForKey("crashOnLongQueries", orElse: 0) != 0 self.chatListPhotos = decoder.decodeInt32ForKey("chatListPhotos", orElse: 0) != 0 self.knockoutWallpaper = decoder.decodeInt32ForKey("knockoutWallpaper", orElse: 0) != 0 - self.gradientBubbles = decoder.decodeInt32ForKey("gradientBubbles", orElse: 0) != 0 self.wallets = decoder.decodeInt32ForKey("wallets", orElse: 0) != 0 } @@ -41,7 +38,6 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry { encoder.encodeInt32(self.crashOnLongQueries ? 1 : 0, forKey: "crashOnLongQueries") encoder.encodeInt32(self.chatListPhotos ? 1 : 0, forKey: "chatListPhotos") encoder.encodeInt32(self.knockoutWallpaper ? 1 : 0, forKey: "knockoutWallpaper") - encoder.encodeInt32(self.gradientBubbles ? 1 : 0, forKey: "gradientBubbles") encoder.encodeInt32(self.wallets ? 1 : 0, forKey: "wallets") } diff --git a/submodules/TelegramUIPreferences/Sources/InAppNotificationSettings.swift b/submodules/TelegramUIPreferences/Sources/InAppNotificationSettings.swift index e1c47d10f7..3da4f51362 100644 --- a/submodules/TelegramUIPreferences/Sources/InAppNotificationSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/InAppNotificationSettings.swift @@ -1,6 +1,7 @@ import Foundation import Postbox import SwiftSignalKit +import SyncCore public enum TotalUnreadCountDisplayStyle: Int32 { case filtered = 0 @@ -38,7 +39,7 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable { public var displayNotificationsFromAllAccounts: Bool public static var defaultSettings: InAppNotificationSettings { - return InAppNotificationSettings(playSounds: true, vibrate: false, displayPreviews: true, totalUnreadCountDisplayStyle: .filtered, totalUnreadCountDisplayCategory: .messages, totalUnreadCountIncludeTags: [.regularChatsAndPrivateGroups], displayNameOnLockscreen: true, displayNotificationsFromAllAccounts: true) + return InAppNotificationSettings(playSounds: true, vibrate: false, displayPreviews: true, totalUnreadCountDisplayStyle: .filtered, totalUnreadCountDisplayCategory: .messages, totalUnreadCountIncludeTags: [.privateChat, .secretChat, .bot, .privateGroup], displayNameOnLockscreen: true, displayNotificationsFromAllAccounts: true) } public init(playSounds: Bool, vibrate: Bool, displayPreviews: Bool, totalUnreadCountDisplayStyle: TotalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: TotalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: PeerSummaryCounterTags, displayNameOnLockscreen: Bool, displayNotificationsFromAllAccounts: Bool) { @@ -58,10 +59,25 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable { self.displayPreviews = decoder.decodeInt32ForKey("p", orElse: 0) != 0 self.totalUnreadCountDisplayStyle = TotalUnreadCountDisplayStyle(rawValue: decoder.decodeInt32ForKey("cds", orElse: 0)) ?? .filtered self.totalUnreadCountDisplayCategory = TotalUnreadCountDisplayCategory(rawValue: decoder.decodeInt32ForKey("totalUnreadCountDisplayCategory", orElse: 1)) ?? .messages - if let value = decoder.decodeOptionalInt32ForKey("totalUnreadCountIncludeTags") { + if let value = decoder.decodeOptionalInt32ForKey("totalUnreadCountIncludeTags_2") { self.totalUnreadCountIncludeTags = PeerSummaryCounterTags(rawValue: value) + } else if let value = decoder.decodeOptionalInt32ForKey("totalUnreadCountIncludeTags") { + var resultTags: PeerSummaryCounterTags = [] + for legacyTag in LegacyPeerSummaryCounterTags(rawValue: value) { + if legacyTag == .regularChatsAndPrivateGroups { + resultTags.insert(.privateChat) + resultTags.insert(.secretChat) + resultTags.insert(.bot) + resultTags.insert(.privateGroup) + } else if legacyTag == .publicGroups { + resultTags.insert(.publicGroup) + } else if legacyTag == .channels { + resultTags.insert(.channel) + } + } + self.totalUnreadCountIncludeTags = resultTags } else { - self.totalUnreadCountIncludeTags = [.regularChatsAndPrivateGroups] + self.totalUnreadCountIncludeTags = [.privateChat, .secretChat, .bot, .privateGroup] } self.displayNameOnLockscreen = decoder.decodeInt32ForKey("displayNameOnLockscreen", orElse: 1) != 0 self.displayNotificationsFromAllAccounts = decoder.decodeInt32ForKey("displayNotificationsFromAllAccounts", orElse: 1) != 0 @@ -73,7 +89,7 @@ public struct InAppNotificationSettings: PreferencesEntry, Equatable { encoder.encodeInt32(self.displayPreviews ? 1 : 0, forKey: "p") encoder.encodeInt32(self.totalUnreadCountDisplayStyle.rawValue, forKey: "cds") encoder.encodeInt32(self.totalUnreadCountDisplayCategory.rawValue, forKey: "totalUnreadCountDisplayCategory") - encoder.encodeInt32(self.totalUnreadCountIncludeTags.rawValue, forKey: "totalUnreadCountIncludeTags") + encoder.encodeInt32(self.totalUnreadCountIncludeTags.rawValue, forKey: "totalUnreadCountIncludeTags_2") encoder.encodeInt32(self.displayNameOnLockscreen ? 1 : 0, forKey: "displayNameOnLockscreen") encoder.encodeInt32(self.displayNotificationsFromAllAccounts ? 1 : 0, forKey: "displayNotificationsFromAllAccounts") } diff --git a/submodules/TelegramUIPreferences/Sources/IntentsSettings.swift b/submodules/TelegramUIPreferences/Sources/IntentsSettings.swift index 9cb936c6aa..77ba26eea7 100644 --- a/submodules/TelegramUIPreferences/Sources/IntentsSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/IntentsSettings.swift @@ -5,20 +5,49 @@ import SwiftSignalKit public struct IntentsSettings: PreferencesEntry, Equatable { public let initiallyReset: Bool + public let account: PeerId? + public let contacts: Bool + public let privateChats: Bool + public let savedMessages: Bool + public let groups: Bool + public let onlyShared: Bool + public static var defaultSettings: IntentsSettings { - return IntentsSettings(initiallyReset: false) + return IntentsSettings(initiallyReset: false, account: nil, contacts: true, privateChats: false, savedMessages: true, groups: false, onlyShared: false) } - public init(initiallyReset: Bool) { + public init(initiallyReset: Bool, account: PeerId?, contacts: Bool, privateChats: Bool, savedMessages: Bool, groups: Bool, onlyShared: Bool) { self.initiallyReset = initiallyReset + self.account = account + self.contacts = contacts + self.privateChats = privateChats + self.savedMessages = savedMessages + self.groups = groups + self.onlyShared = onlyShared } public init(decoder: PostboxDecoder) { - self.initiallyReset = decoder.decodeBoolForKey("initiallyReset", orElse: false) + self.initiallyReset = decoder.decodeBoolForKey("initiallyReset_v2", orElse: false) + self.account = decoder.decodeOptionalInt64ForKey("account").flatMap { PeerId($0) } + self.contacts = decoder.decodeBoolForKey("contacts", orElse: true) + self.privateChats = decoder.decodeBoolForKey("privateChats", orElse: false) + self.savedMessages = decoder.decodeBoolForKey("savedMessages", orElse: true) + self.groups = decoder.decodeBoolForKey("groups", orElse: false) + self.onlyShared = decoder.decodeBoolForKey("onlyShared", orElse: false) } public func encode(_ encoder: PostboxEncoder) { - encoder.encodeBool(self.initiallyReset, forKey: "initiallyReset") + encoder.encodeBool(self.initiallyReset, forKey: "initiallyReset_v2") + if let account = self.account { + encoder.encodeInt64(account.toInt64(), forKey: "account") + } else { + encoder.encodeNil(forKey: "account") + } + encoder.encodeBool(self.contacts, forKey: "contacts") + encoder.encodeBool(self.privateChats, forKey: "privateChats") + encoder.encodeBool(self.savedMessages, forKey: "savedMessages") + encoder.encodeBool(self.groups, forKey: "groups") + encoder.encodeBool(self.onlyShared, forKey: "onlyShared") } public func isEqual(to: PreferencesEntry) -> Bool { @@ -30,6 +59,50 @@ public struct IntentsSettings: PreferencesEntry, Equatable { } public static func ==(lhs: IntentsSettings, rhs: IntentsSettings) -> Bool { - return lhs.initiallyReset == rhs.initiallyReset + return lhs.initiallyReset == rhs.initiallyReset && lhs.account == rhs.account && lhs.contacts == rhs.contacts && lhs.privateChats == rhs.privateChats && lhs.savedMessages == rhs.savedMessages && lhs.groups == rhs.groups && lhs.onlyShared == rhs.onlyShared + } + + public func withUpdatedAccount(_ account: PeerId?) -> IntentsSettings { + return IntentsSettings(initiallyReset: self.initiallyReset, account: account, contacts: self.contacts, privateChats: self.privateChats, savedMessages: self.savedMessages, groups: self.groups, onlyShared: self.onlyShared) + } + + public func withUpdatedContacts(_ contacts: Bool) -> IntentsSettings { + return IntentsSettings(initiallyReset: self.initiallyReset, account: self.account, contacts: contacts, privateChats: self.privateChats, savedMessages: self.savedMessages, groups: self.groups, onlyShared: self.onlyShared) + } + + public func withUpdatedPrivateChats(_ privateChats: Bool) -> IntentsSettings { + return IntentsSettings(initiallyReset: self.initiallyReset, account: self.account, contacts: self.contacts, privateChats: privateChats, savedMessages: self.savedMessages, groups: self.groups, onlyShared: self.onlyShared) + } + + public func withUpdatedSavedMessages(_ savedMessages: Bool) -> IntentsSettings { + return IntentsSettings(initiallyReset: self.initiallyReset, account: self.account, contacts: self.contacts, privateChats: self.privateChats, savedMessages: savedMessages, groups: self.groups, onlyShared: self.onlyShared) + } + + public func withUpdatedGroups(_ groups: Bool) -> IntentsSettings { + return IntentsSettings(initiallyReset: self.initiallyReset, account: self.account, contacts: self.contacts, privateChats: self.privateChats, savedMessages: self.savedMessages, groups: groups, onlyShared: self.onlyShared) + } + + public func withUpdatedOnlyShared(_ onlyShared: Bool) -> IntentsSettings { + return IntentsSettings(initiallyReset: self.initiallyReset, account: self.account, contacts: self.contacts, privateChats: self.privateChats, savedMessages: self.savedMessages, groups: self.groups, onlyShared: onlyShared) + } +} + + +public func updateIntentsSettingsInteractively(accountManager: AccountManager, _ f: @escaping (IntentsSettings) -> IntentsSettings) -> Signal<(IntentsSettings?, IntentsSettings?), NoError> { + return accountManager.transaction { transaction -> (IntentsSettings?, IntentsSettings?) in + var previousSettings: IntentsSettings? = nil + var updatedSettings: IntentsSettings? = nil + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.intentsSettings, { entry in + let currentSettings: IntentsSettings + if let entry = entry as? IntentsSettings { + currentSettings = entry + } else { + currentSettings = IntentsSettings.defaultSettings + } + previousSettings = currentSettings + updatedSettings = f(currentSettings) + return updatedSettings + }) + return (previousSettings, updatedSettings) } } diff --git a/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift b/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift index 543a981aa8..905662b02b 100644 --- a/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/MediaAutoDownloadSettings.swift @@ -10,6 +10,17 @@ public enum MediaAutoDownloadNetworkType { case cellular } +public extension MediaAutoDownloadNetworkType { + init(_ networkType: NetworkType) { + switch networkType { + case .none, .cellular: + self = .cellular + case .wifi: + self = .wifi + } + } +} + public enum MediaAutoDownloadPreset: Int32 { case low case medium diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index 21684e9c6d..17f712a5e0 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -6,11 +6,13 @@ import Postbox private enum ApplicationSpecificPreferencesKeyValues: Int32 { case voipDerivedState = 16 case chatArchiveSettings = 17 + case chatListFilterSettings = 18 } public struct ApplicationSpecificPreferencesKeys { public static let voipDerivedState = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.voipDerivedState.rawValue) public static let chatArchiveSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.chatArchiveSettings.rawValue) + public static let chatListFilterSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.chatListFilterSettings.rawValue) } private enum ApplicationSpecificSharedDataKeyValues: Int32 { @@ -60,6 +62,7 @@ private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 { case cachedInstantPages = 1 case cachedWallpapers = 2 case mediaPlaybackStoredState = 3 + case cachedGeocodes = 4 } public struct ApplicationSpecificItemCacheCollectionId { @@ -67,6 +70,7 @@ public struct ApplicationSpecificItemCacheCollectionId { public static let cachedInstantPages = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.cachedInstantPages.rawValue) public static let cachedWallpapers = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.cachedWallpapers.rawValue) public static let mediaPlaybackStoredState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.mediaPlaybackStoredState.rawValue) + public static let cachedGeocodes = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.cachedGeocodes.rawValue) } private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 { diff --git a/submodules/TelegramUIPreferences/Sources/PresentationThemeSettings.swift b/submodules/TelegramUIPreferences/Sources/PresentationThemeSettings.swift index d7d1de1c33..ab9d7e7291 100644 --- a/submodules/TelegramUIPreferences/Sources/PresentationThemeSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/PresentationThemeSettings.swift @@ -10,6 +10,32 @@ public enum PresentationBuiltinThemeReference: Int32 { case night = 1 case day = 2 case nightAccent = 3 + + public init(baseTheme: TelegramBaseTheme) { + switch baseTheme { + case .classic: + self = .dayClassic + case .day: + self = .day + case .night: + self = .night + case .tinted: + self = .nightAccent + } + } + + public var baseTheme: TelegramBaseTheme { + switch self { + case .dayClassic: + return .classic + case .day: + return .day + case .night: + return .night + case .nightAccent: + return .tinted + } + } } public struct WallpaperPresentationOptions: OptionSet { @@ -71,15 +97,18 @@ public struct PresentationLocalTheme: PostboxCoding, Equatable { public struct PresentationCloudTheme: PostboxCoding, Equatable { public let theme: TelegramTheme public let resolvedWallpaper: TelegramWallpaper? + public let creatorAccountId: AccountRecordId? - public init(theme: TelegramTheme, resolvedWallpaper: TelegramWallpaper?) { + public init(theme: TelegramTheme, resolvedWallpaper: TelegramWallpaper?, creatorAccountId: AccountRecordId?) { self.theme = theme self.resolvedWallpaper = resolvedWallpaper + self.creatorAccountId = creatorAccountId } public init(decoder: PostboxDecoder) { self.theme = decoder.decodeObjectForKey("theme", decoder: { TelegramTheme(decoder: $0) }) as! TelegramTheme self.resolvedWallpaper = decoder.decodeObjectForKey("wallpaper", decoder: { TelegramWallpaper(decoder: $0) }) as? TelegramWallpaper + self.creatorAccountId = decoder.decodeOptionalInt64ForKey("account").flatMap { AccountRecordId(rawValue: $0) } } public func encode(_ encoder: PostboxEncoder) { @@ -89,6 +118,11 @@ public struct PresentationCloudTheme: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "wallpaper") } + if let accountId = self.creatorAccountId { + encoder.encodeInt64(accountId.int64, forKey: "account") + } else { + encoder.encodeNil(forKey: "account") + } } public static func ==(lhs: PresentationCloudTheme, rhs: PresentationCloudTheme) -> Bool { @@ -96,7 +130,7 @@ public struct PresentationCloudTheme: PostboxCoding, Equatable { return false } if lhs.resolvedWallpaper != rhs.resolvedWallpaper { - return false + return false } return true } @@ -177,7 +211,7 @@ public enum PresentationThemeReference: PostboxCoding, Equatable { acc = (acc &* 20261) &+ high acc = (acc &* 20261) &+ low - return Int32(bitPattern: acc & UInt32(0x7FFFFFFF)) + return Int32(bitPattern: acc & UInt32(0x7fffffff)) } switch self { @@ -194,9 +228,31 @@ public enum PresentationThemeReference: PostboxCoding, Equatable { return (Int64(namespace) << 32) | Int64(bitPattern: UInt64(UInt32(bitPattern: id))) } + + public var generalThemeReference: PresentationThemeReference { + let generalThemeReference: PresentationThemeReference + if case let .cloud(theme) = self, let settings = theme.theme.settings { + generalThemeReference = .builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)) + } else { + generalThemeReference = self + } + return generalThemeReference + } } -public enum PresentationFontSize: Int32 { +public func coloredThemeIndex(reference: PresentationThemeReference, accentColor: PresentationThemeAccentColor?) -> Int64 { + if let accentColor = accentColor { + if case let .builtin(theme) = reference { + return reference.index * 1000 &+ Int64(accentColor.index) + } else { + return reference.index &+ Int64(accentColor.index) + } + } else { + return reference.index + } +} + +public enum PresentationFontSize: Int32, CaseIterable { case extraSmall = 0 case small = 1 case regular = 2 @@ -314,6 +370,8 @@ public enum PresentationThemeBaseColor: Int32, CaseIterable { case black case white case custom + case preset + case theme public var color: UIColor { let value: UInt32 @@ -340,86 +398,179 @@ public enum PresentationThemeBaseColor: Int32, CaseIterable { value = 0x000000 case .white: value = 0xffffff - case .custom: + case .custom, .preset, .theme: return .clear } return UIColor(rgb: value) } - - public var outgoingGradientColors: (UIColor, UIColor) { - switch self { - case .blue: - return (UIColor(rgb: 0x63BFFB), UIColor(rgb: 0x007AFF)) - case .cyan: - return (UIColor(rgb: 0x5CE0E9), UIColor(rgb: 0x00C2ED)) - case .green: - return (UIColor(rgb: 0x93D374), UIColor(rgb: 0x29B327)) - case .pink: - return (UIColor(rgb: 0xE296C1), UIColor(rgb: 0xEB6CA4)) - case .orange: - return (UIColor(rgb: 0xF2A451), UIColor(rgb: 0xF08200)) - case .purple: - return (UIColor(rgb: 0xAC98E6), UIColor(rgb: 0x9472EE)) - case .red: - return (UIColor(rgb: 0xE06D54), UIColor(rgb: 0xD33213)) - case .yellow: - return (UIColor(rgb: 0xF7DA6B), UIColor(rgb: 0xEDB400)) - case .gray: - return (UIColor(rgb: 0x7D8E9A), UIColor(rgb: 0x6D839E)) - case .black: - return (UIColor(rgb: 0x000000), UIColor(rgb: 0x000000)) - case .white: - return (UIColor(rgb: 0xffffff), UIColor(rgb: 0xffffff)) - case .custom: - return (UIColor(rgb: 0x000000), UIColor(rgb: 0x000000)) - } - } } public struct PresentationThemeAccentColor: PostboxCoding, Equatable { - public var baseColor: PresentationThemeBaseColor - public var value: Int32? + public static func == (lhs: PresentationThemeAccentColor, rhs: PresentationThemeAccentColor) -> Bool { + return lhs.index == rhs.index && lhs.baseColor == rhs.baseColor && lhs.accentColor == rhs.accentColor && lhs.bubbleColors?.0 == rhs.bubbleColors?.0 && lhs.bubbleColors?.1 == rhs.bubbleColors?.1 + } - public init(baseColor: PresentationThemeBaseColor, value: Int32? = nil) { + public var index: Int32 + public var baseColor: PresentationThemeBaseColor + public var accentColor: UInt32? + public var bubbleColors: (UInt32, UInt32?)? + public var wallpaper: TelegramWallpaper? + public var themeIndex: Int64? + + public init(baseColor: PresentationThemeBaseColor) { + if baseColor != .preset && baseColor != .custom { + self.index = baseColor.rawValue + 10 + } else { + self.index = -1 + } self.baseColor = baseColor - self.value = value + self.accentColor = nil + self.bubbleColors = nil + self.wallpaper = nil + } + + public init(index: Int32, baseColor: PresentationThemeBaseColor, accentColor: UInt32? = nil, bubbleColors: (UInt32, UInt32?)? = nil, wallpaper: TelegramWallpaper? = nil) { + self.index = index + self.baseColor = baseColor + self.accentColor = accentColor + self.bubbleColors = bubbleColors + self.wallpaper = wallpaper + } + + public init(themeIndex: Int64) { + self.index = -1 + self.baseColor = .theme + self.accentColor = nil + self.bubbleColors = nil + self.wallpaper = nil + self.themeIndex = themeIndex } public init(decoder: PostboxDecoder) { + self.index = decoder.decodeInt32ForKey("i", orElse: -1) self.baseColor = PresentationThemeBaseColor(rawValue: decoder.decodeInt32ForKey("b", orElse: 0)) ?? .blue - self.value = decoder.decodeOptionalInt32ForKey("c") + self.accentColor = decoder.decodeOptionalInt32ForKey("c").flatMap { UInt32(bitPattern: $0) } + if let bubbleTopColor = decoder.decodeOptionalInt32ForKey("bt") { + if let bubbleBottomColor = decoder.decodeOptionalInt32ForKey("bb") { + self.bubbleColors = (UInt32(bitPattern: bubbleTopColor), UInt32(bitPattern: bubbleBottomColor)) + } else { + self.bubbleColors = (UInt32(bitPattern: bubbleTopColor), nil) + } + } else { + self.bubbleColors = nil + } + self.wallpaper = decoder.decodeObjectForKey("w", decoder: { TelegramWallpaper(decoder: $0) }) as? TelegramWallpaper + self.themeIndex = decoder.decodeOptionalInt64ForKey("t") } public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.index, forKey: "i") encoder.encodeInt32(self.baseColor.rawValue, forKey: "b") - if let value = self.value { - encoder.encodeInt32(value, forKey: "c") + if let value = self.accentColor { + encoder.encodeInt32(Int32(bitPattern: value), forKey: "c") } else { encoder.encodeNil(forKey: "c") } + if let bubbleColors = self.bubbleColors { + encoder.encodeInt32(Int32(bitPattern: bubbleColors.0), forKey: "bt") + if let bubbleBottomColor = bubbleColors.1 { + encoder.encodeInt32(Int32(bitPattern: bubbleBottomColor), forKey: "bb") + } else { + encoder.encodeNil(forKey: "bb") + } + } else { + encoder.encodeNil(forKey: "bt") + encoder.encodeNil(forKey: "bb") + } + if let wallpaper = self.wallpaper { + encoder.encodeObject(wallpaper, forKey: "w") + } else { + encoder.encodeNil(forKey: "w") + } + if let themeIndex = self.themeIndex { + encoder.encodeInt64(themeIndex, forKey: "t") + } else { + encoder.encodeNil(forKey: "t") + } } public var color: UIColor { - if let value = self.value { + if let value = self.accentColor { return UIColor(rgb: UInt32(bitPattern: value)) } else { return self.baseColor.color } } + + public var customBubbleColors: (UIColor, UIColor?)? { + if let bubbleColors = self.bubbleColors { + if let bottomColor = bubbleColors.1 { + return (UIColor(rgb: UInt32(bitPattern: bubbleColors.0)), UIColor(rgb: UInt32(bitPattern: bottomColor))) + } else { + return (UIColor(rgb: UInt32(bitPattern: bubbleColors.0)), nil) + } + } else { + return nil + } + } + + public var plainBubbleColors: (UIColor, UIColor)? { + if let bubbleColors = self.bubbleColors { + if let bottomColor = bubbleColors.1 { + return (UIColor(rgb: UInt32(bitPattern: bubbleColors.0)), UIColor(rgb: UInt32(bitPattern: bottomColor))) + } else { + return (UIColor(rgb: UInt32(bitPattern: bubbleColors.0)), UIColor(rgb: UInt32(bitPattern: bubbleColors.0))) + } + } else { + return nil + } + } + + public func withUpdatedWallpaper(_ wallpaper: TelegramWallpaper?) -> PresentationThemeAccentColor { + return PresentationThemeAccentColor(index: self.index, baseColor: self.baseColor, accentColor: self.accentColor, bubbleColors: self.bubbleColors, wallpaper: wallpaper) + } +} + +public struct PresentationChatBubbleSettings: PostboxCoding, Equatable { + public var mainRadius: Int32 + public var auxiliaryRadius: Int32 + public var mergeBubbleCorners: Bool + + public static var `default`: PresentationChatBubbleSettings = PresentationChatBubbleSettings(mainRadius: 16, auxiliaryRadius: 8, mergeBubbleCorners: true) + + public init(mainRadius: Int32, auxiliaryRadius: Int32, mergeBubbleCorners: Bool) { + self.mainRadius = mainRadius + self.auxiliaryRadius = auxiliaryRadius + self.mergeBubbleCorners = mergeBubbleCorners + } + + public init(decoder: PostboxDecoder) { + self.mainRadius = decoder.decodeInt32ForKey("mainRadius", orElse: 16) + self.auxiliaryRadius = decoder.decodeInt32ForKey("auxiliaryRadius", orElse: 8) + self.mergeBubbleCorners = decoder.decodeInt32ForKey("mergeBubbleCorners", orElse: 1) != 0 + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.mainRadius, forKey: "mainRadius") + encoder.encodeInt32(self.auxiliaryRadius, forKey: "auxiliaryRadius") + encoder.encodeInt32(self.mergeBubbleCorners ? 1 : 0, forKey: "mergeBubbleCorners") + } } public struct PresentationThemeSettings: PreferencesEntry { - public var chatWallpaper: TelegramWallpaper public var theme: PresentationThemeReference public var themeSpecificAccentColors: [Int64: PresentationThemeAccentColor] public var themeSpecificChatWallpapers: [Int64: TelegramWallpaper] + public var useSystemFont: Bool public var fontSize: PresentationFontSize + public var listsFontSize: PresentationFontSize + public var chatBubbleSettings: PresentationChatBubbleSettings public var automaticThemeSwitchSetting: AutomaticThemeSwitchSetting public var largeEmoji: Bool public var disableAnimations: Bool private func wallpaperResources(_ wallpaper: TelegramWallpaper) -> [MediaResourceId] { - switch self.chatWallpaper { + switch wallpaper { case let .image(representations, _): return representations.map { $0.resource.id } case let .file(_, _, _, _, _, _, _, file, _): @@ -434,7 +585,6 @@ public struct PresentationThemeSettings: PreferencesEntry { public var relatedResources: [MediaResourceId] { var resources: [MediaResourceId] = [] - resources.append(contentsOf: wallpaperResources(self.chatWallpaper)) for (_, chatWallpaper) in self.themeSpecificChatWallpapers { resources.append(contentsOf: wallpaperResources(chatWallpaper)) } @@ -455,22 +605,23 @@ public struct PresentationThemeSettings: PreferencesEntry { } public static var defaultSettings: PresentationThemeSettings { - return PresentationThemeSettings(chatWallpaper: .builtin(WallpaperSettings()), theme: .builtin(.dayClassic), themeSpecificAccentColors: [:], themeSpecificChatWallpapers: [:], fontSize: .regular, automaticThemeSwitchSetting: AutomaticThemeSwitchSetting(trigger: .system, theme: .builtin(.night)), largeEmoji: true, disableAnimations: true) + return PresentationThemeSettings(theme: .builtin(.dayClassic), themeSpecificAccentColors: [:], themeSpecificChatWallpapers: [:], useSystemFont: true, fontSize: .regular, listsFontSize: .regular, chatBubbleSettings: .default, automaticThemeSwitchSetting: AutomaticThemeSwitchSetting(trigger: .system, theme: .builtin(.night)), largeEmoji: true, disableAnimations: true) } - public init(chatWallpaper: TelegramWallpaper, theme: PresentationThemeReference, themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], fontSize: PresentationFontSize, automaticThemeSwitchSetting: AutomaticThemeSwitchSetting, largeEmoji: Bool, disableAnimations: Bool) { - self.chatWallpaper = chatWallpaper + public init(theme: PresentationThemeReference, themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], useSystemFont: Bool, fontSize: PresentationFontSize, listsFontSize: PresentationFontSize, chatBubbleSettings: PresentationChatBubbleSettings, automaticThemeSwitchSetting: AutomaticThemeSwitchSetting, largeEmoji: Bool, disableAnimations: Bool) { self.theme = theme self.themeSpecificAccentColors = themeSpecificAccentColors self.themeSpecificChatWallpapers = themeSpecificChatWallpapers + self.useSystemFont = useSystemFont self.fontSize = fontSize + self.listsFontSize = listsFontSize + self.chatBubbleSettings = chatBubbleSettings self.automaticThemeSwitchSetting = automaticThemeSwitchSetting self.largeEmoji = largeEmoji self.disableAnimations = disableAnimations } public init(decoder: PostboxDecoder) { - self.chatWallpaper = (decoder.decodeObjectForKey("w", decoder: { TelegramWallpaper(decoder: $0) }) as? TelegramWallpaper) ?? .builtin(WallpaperSettings()) self.theme = decoder.decodeObjectForKey("t", decoder: { PresentationThemeReference(decoder: $0) }) as? PresentationThemeReference ?? .builtin(.dayClassic) self.themeSpecificChatWallpapers = decoder.decodeObjectDictionaryForKey("themeSpecificChatWallpapers", keyDecoder: { decoder in @@ -485,39 +636,17 @@ public struct PresentationThemeSettings: PreferencesEntry { return PresentationThemeAccentColor(decoder: decoder) }) - if self.themeSpecificAccentColors[PresentationThemeReference.builtin(.day).index] == nil, let themeAccentColor = decoder.decodeOptionalInt32ForKey("themeAccentColor") { - let baseColor: PresentationThemeBaseColor - switch themeAccentColor { - case 0xf83b4c: - baseColor = .red - case 0xff7519: - baseColor = .orange - case 0xeba239: - baseColor = .yellow - case 0x29b327: - baseColor = .green - case 0x00c2ed: - baseColor = .cyan - case 0x007ee5: - baseColor = .blue - case 0x7748ff: - baseColor = .purple - case 0xff5da2: - baseColor = .pink - default: - baseColor = .blue - } - self.themeSpecificAccentColors[PresentationThemeReference.builtin(.day).index] = PresentationThemeAccentColor(baseColor: baseColor) - } - - self.fontSize = PresentationFontSize(rawValue: decoder.decodeInt32ForKey("f", orElse: PresentationFontSize.regular.rawValue)) ?? .regular + self.useSystemFont = decoder.decodeInt32ForKey("useSystemFont", orElse: 1) != 0 + let fontSize = PresentationFontSize(rawValue: decoder.decodeInt32ForKey("f", orElse: PresentationFontSize.regular.rawValue)) ?? .regular + self.fontSize = fontSize + self.listsFontSize = PresentationFontSize(rawValue: decoder.decodeInt32ForKey("lf", orElse: PresentationFontSize.regular.rawValue)) ?? fontSize + self.chatBubbleSettings = decoder.decodeObjectForKey("chatBubbleSettings", decoder: { PresentationChatBubbleSettings(decoder: $0) }) as? PresentationChatBubbleSettings ?? PresentationChatBubbleSettings.default self.automaticThemeSwitchSetting = (decoder.decodeObjectForKey("automaticThemeSwitchSetting", decoder: { AutomaticThemeSwitchSetting(decoder: $0) }) as? AutomaticThemeSwitchSetting) ?? AutomaticThemeSwitchSetting(trigger: .system, theme: .builtin(.night)) self.largeEmoji = decoder.decodeBoolForKey("largeEmoji", orElse: true) self.disableAnimations = decoder.decodeBoolForKey("disableAnimations", orElse: true) } public func encode(_ encoder: PostboxEncoder) { - encoder.encodeObject(self.chatWallpaper, forKey: "w") encoder.encodeObject(self.theme, forKey: "t") encoder.encodeObjectDictionary(self.themeSpecificAccentColors, forKey: "themeSpecificAccentColors", keyEncoder: { key, encoder in encoder.encodeInt64(key, forKey: "k") @@ -525,7 +654,10 @@ public struct PresentationThemeSettings: PreferencesEntry { encoder.encodeObjectDictionary(self.themeSpecificChatWallpapers, forKey: "themeSpecificChatWallpapers", keyEncoder: { key, encoder in encoder.encodeInt64(key, forKey: "k") }) + encoder.encodeInt32(self.useSystemFont ? 1 : 0, forKey: "useSystemFont") encoder.encodeInt32(self.fontSize.rawValue, forKey: "f") + encoder.encodeInt32(self.listsFontSize.rawValue, forKey: "lf") + encoder.encodeObject(self.chatBubbleSettings, forKey: "chatBubbleSettings") encoder.encodeObject(self.automaticThemeSwitchSetting, forKey: "automaticThemeSwitchSetting") encoder.encodeBool(self.largeEmoji, forKey: "largeEmoji") encoder.encodeBool(self.disableAnimations, forKey: "disableAnimations") @@ -540,7 +672,43 @@ public struct PresentationThemeSettings: PreferencesEntry { } public static func ==(lhs: PresentationThemeSettings, rhs: PresentationThemeSettings) -> Bool { - return lhs.chatWallpaper == rhs.chatWallpaper && lhs.theme == rhs.theme && lhs.themeSpecificAccentColors == rhs.themeSpecificAccentColors && lhs.themeSpecificChatWallpapers == rhs.themeSpecificChatWallpapers && lhs.fontSize == rhs.fontSize && lhs.automaticThemeSwitchSetting == rhs.automaticThemeSwitchSetting && lhs.largeEmoji == rhs.largeEmoji && lhs.disableAnimations == rhs.disableAnimations + return lhs.theme == rhs.theme && lhs.themeSpecificAccentColors == rhs.themeSpecificAccentColors && lhs.themeSpecificChatWallpapers == rhs.themeSpecificChatWallpapers && lhs.useSystemFont == rhs.useSystemFont && lhs.fontSize == rhs.fontSize && lhs.listsFontSize == rhs.listsFontSize && lhs.chatBubbleSettings == rhs.chatBubbleSettings && lhs.automaticThemeSwitchSetting == rhs.automaticThemeSwitchSetting && lhs.largeEmoji == rhs.largeEmoji && lhs.disableAnimations == rhs.disableAnimations + } + + public func withUpdatedTheme(_ theme: PresentationThemeReference) -> PresentationThemeSettings { + return PresentationThemeSettings(theme: theme, themeSpecificAccentColors: self.themeSpecificAccentColors, themeSpecificChatWallpapers: self.themeSpecificChatWallpapers, useSystemFont: self.useSystemFont, fontSize: self.fontSize, listsFontSize: self.listsFontSize, chatBubbleSettings: self.chatBubbleSettings, automaticThemeSwitchSetting: self.automaticThemeSwitchSetting, largeEmoji: self.largeEmoji, disableAnimations: self.disableAnimations) + } + + public func withUpdatedThemeSpecificAccentColors(_ themeSpecificAccentColors: [Int64: PresentationThemeAccentColor]) -> PresentationThemeSettings { + return PresentationThemeSettings(theme: self.theme, themeSpecificAccentColors: themeSpecificAccentColors, themeSpecificChatWallpapers: self.themeSpecificChatWallpapers, useSystemFont: self.useSystemFont, fontSize: self.fontSize, listsFontSize: self.listsFontSize, chatBubbleSettings: self.chatBubbleSettings, automaticThemeSwitchSetting: self.automaticThemeSwitchSetting, largeEmoji: self.largeEmoji, disableAnimations: self.disableAnimations) + } + + public func withUpdatedThemeSpecificChatWallpapers(_ themeSpecificChatWallpapers: [Int64: TelegramWallpaper]) -> PresentationThemeSettings { + return PresentationThemeSettings(theme: self.theme, themeSpecificAccentColors: self.themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, useSystemFont: self.useSystemFont, fontSize: self.fontSize, listsFontSize: self.listsFontSize, chatBubbleSettings: self.chatBubbleSettings, automaticThemeSwitchSetting: self.automaticThemeSwitchSetting, largeEmoji: self.largeEmoji, disableAnimations: self.disableAnimations) + } + + public func withUpdatedUseSystemFont(_ useSystemFont: Bool) -> PresentationThemeSettings { + return PresentationThemeSettings(theme: self.theme, themeSpecificAccentColors: self.themeSpecificAccentColors, themeSpecificChatWallpapers: self.themeSpecificChatWallpapers, useSystemFont: useSystemFont, fontSize: self.fontSize, listsFontSize: self.listsFontSize, chatBubbleSettings: self.chatBubbleSettings, automaticThemeSwitchSetting: self.automaticThemeSwitchSetting, largeEmoji: self.largeEmoji, disableAnimations: self.disableAnimations) + } + + public func withUpdatedFontSizes(fontSize: PresentationFontSize, listsFontSize: PresentationFontSize) -> PresentationThemeSettings { + return PresentationThemeSettings(theme: self.theme, themeSpecificAccentColors: self.themeSpecificAccentColors, themeSpecificChatWallpapers: self.themeSpecificChatWallpapers, useSystemFont: self.useSystemFont, fontSize: fontSize, listsFontSize: listsFontSize, chatBubbleSettings: self.chatBubbleSettings, automaticThemeSwitchSetting: self.automaticThemeSwitchSetting, largeEmoji: self.largeEmoji, disableAnimations: self.disableAnimations) + } + + public func withUpdatedChatBubbleSettings(_ chatBubbleSettings: PresentationChatBubbleSettings) -> PresentationThemeSettings { + return PresentationThemeSettings(theme: self.theme, themeSpecificAccentColors: self.themeSpecificAccentColors, themeSpecificChatWallpapers: self.themeSpecificChatWallpapers, useSystemFont: self.useSystemFont, fontSize: self.fontSize, listsFontSize: self.listsFontSize, chatBubbleSettings: chatBubbleSettings, automaticThemeSwitchSetting: self.automaticThemeSwitchSetting, largeEmoji: self.largeEmoji, disableAnimations: self.disableAnimations) + } + + public func withUpdatedAutomaticThemeSwitchSetting(_ automaticThemeSwitchSetting: AutomaticThemeSwitchSetting) -> PresentationThemeSettings { + return PresentationThemeSettings(theme: self.theme, themeSpecificAccentColors: self.themeSpecificAccentColors, themeSpecificChatWallpapers: self.themeSpecificChatWallpapers, useSystemFont: self.useSystemFont, fontSize: self.fontSize, listsFontSize: self.listsFontSize, chatBubbleSettings: self.chatBubbleSettings, automaticThemeSwitchSetting: automaticThemeSwitchSetting, largeEmoji: self.largeEmoji, disableAnimations: self.disableAnimations) + } + + public func withUpdatedLargeEmoji(_ largeEmoji: Bool) -> PresentationThemeSettings { + return PresentationThemeSettings(theme: self.theme, themeSpecificAccentColors: self.themeSpecificAccentColors, themeSpecificChatWallpapers: self.themeSpecificChatWallpapers, useSystemFont: self.useSystemFont, fontSize: self.fontSize, listsFontSize: self.listsFontSize, chatBubbleSettings: self.chatBubbleSettings, automaticThemeSwitchSetting: self.automaticThemeSwitchSetting, largeEmoji: largeEmoji, disableAnimations: self.disableAnimations) + } + + public func withUpdatedDisableAnimations(_ disableAnimations: Bool) -> PresentationThemeSettings { + return PresentationThemeSettings(theme: self.theme, themeSpecificAccentColors: self.themeSpecificAccentColors, themeSpecificChatWallpapers: self.themeSpecificChatWallpapers, useSystemFont: self.useSystemFont, fontSize: self.fontSize, listsFontSize: self.listsFontSize, chatBubbleSettings: self.chatBubbleSettings, automaticThemeSwitchSetting: self.automaticThemeSwitchSetting, largeEmoji: self.largeEmoji, disableAnimations: disableAnimations) } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/TwitchEmbedImplementation.swift b/submodules/TelegramUniversalVideoContent/Sources/TwitchEmbedImplementation.swift new file mode 100644 index 0000000000..ea36275ed9 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/Sources/TwitchEmbedImplementation.swift @@ -0,0 +1,117 @@ +import Foundation +import WebKit +import SwiftSignalKit +import UniversalMediaPlayer +import AppBundle + +func isTwitchVideoUrl(_ url: String) -> Bool { + return url.contains("//player.twitch.tv/") || url.contains("//clips.twitch.tv/") +} + +final class TwitchEmbedImplementation: WebEmbedImplementation { + private var evalImpl: ((String) -> Void)? + private var updateStatus: ((MediaPlayerStatus) -> Void)? + private var onPlaybackStarted: (() -> Void)? + + private let url: String + private var status : MediaPlayerStatus + + private var started = false + + init(url: String) { + self.url = url + self.status = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true), soundEnabled: true) + } + + func setup(_ webView: WKWebView, userContentController: WKUserContentController, evaluateJavaScript: @escaping (String) -> Void, updateStatus: @escaping (MediaPlayerStatus) -> Void, onPlaybackStarted: @escaping () -> Void) { + let bundle = getAppBundle() + guard let userScriptPath = bundle.path(forResource: "TwitchUserScript", ofType: "js") else { + return + } + guard let userScriptData = try? Data(contentsOf: URL(fileURLWithPath: userScriptPath)) else { + return + } + guard let userScript = String(data: userScriptData, encoding: .utf8) else { + return + } + guard let htmlTemplatePath = bundle.path(forResource: "Twitch", ofType: "html") else { + return + } + guard let htmlTemplateData = try? Data(contentsOf: URL(fileURLWithPath: htmlTemplatePath)) else { + return + } + guard let htmlTemplate = String(data: htmlTemplateData, encoding: .utf8) else { + return + } + + self.evalImpl = evaluateJavaScript + self.updateStatus = updateStatus + self.onPlaybackStarted = onPlaybackStarted + updateStatus(self.status) + + let html = String(format: htmlTemplate, self.url) + webView.loadHTMLString(html, baseURL: URL(string: "about:blank")) + + userContentController.addUserScript(WKUserScript(source: userScript, injectionTime: .atDocumentEnd, forMainFrameOnly: false)) + } + + func play() { + if let eval = self.evalImpl { + eval("playPause()") + } + + self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: self.status.duration, dimensions: self.status.dimensions, timestamp: self.status.timestamp, baseRate: 1.0, seekId: self.status.seekId, status: .playing, soundEnabled: self.status.soundEnabled) + if let updateStatus = self.updateStatus { + updateStatus(self.status) + } + } + + func pause() { + if let eval = self.evalImpl { + eval("playPause()") + } + + self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: self.status.duration, dimensions: self.status.dimensions, timestamp: self.status.timestamp, baseRate: 1.0, seekId: self.status.seekId, status: .paused, soundEnabled: self.status.soundEnabled) + if let updateStatus = self.updateStatus { + updateStatus(self.status) + } + } + + func togglePlayPause() { + if self.status.status == .playing { + self.pause() + } else { + self.play() + } + } + + func seek(timestamp: Double) { + } + + func pageReady() { +// Queue.mainQueue().after(delay: 0.5) { +// if let onPlaybackStarted = self.onPlaybackStarted { +// onPlaybackStarted() +// } +// } + } + + func callback(url: URL) { + switch url.host { + case "onPlayback": + if !self.started { + self.started = true + self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: self.status.duration, dimensions: self.status.dimensions, timestamp: self.status.timestamp, baseRate: 1.0, seekId: self.status.seekId, status: .playing, soundEnabled: self.status.soundEnabled) + if let updateStatus = self.updateStatus { + updateStatus(self.status) + } + + if let onPlaybackStarted = self.onPlaybackStarted { + onPlaybackStarted() + } + } + default: + break + } + } +} diff --git a/submodules/TelegramUniversalVideoContent/Sources/VimeoEmbedImplementation.swift b/submodules/TelegramUniversalVideoContent/Sources/VimeoEmbedImplementation.swift index 7b9621ddb0..d0b94965aa 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/VimeoEmbedImplementation.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/VimeoEmbedImplementation.swift @@ -134,7 +134,7 @@ final class VimeoEmbedImplementation: WebEmbedImplementation { } func play() { - if let eval = evalImpl { + if let eval = self.evalImpl { eval("play();") } @@ -142,7 +142,7 @@ final class VimeoEmbedImplementation: WebEmbedImplementation { } func pause() { - if let eval = evalImpl { + if let eval = self.evalImpl { eval("pause();") } } @@ -156,7 +156,7 @@ final class VimeoEmbedImplementation: WebEmbedImplementation { } func seek(timestamp: Double) { - if let eval = evalImpl { + if let eval = self.evalImpl { eval("seek(\(timestamp));") } @@ -165,7 +165,7 @@ final class VimeoEmbedImplementation: WebEmbedImplementation { updateStatus(self.status) } - ignorePosition = 2 + self.ignorePosition = 2 } func pageReady() { diff --git a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift index 5ebd43ed5d..12db0f5a2f 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift @@ -22,6 +22,7 @@ protocol WebEmbedImplementation { public enum WebEmbedType { case youtube(videoId: String, timestamp: Int) case vimeo(videoId: String, timestamp: Int) + case twitch(url: String) case iframe(url: String) public var supportsSeeking: Bool { @@ -37,6 +38,10 @@ public enum WebEmbedType { public func webEmbedType(content: TelegramMediaWebpageLoadedContent, forcedTimestamp: Int? = nil) -> WebEmbedType { if let (videoId, timestamp) = extractYoutubeVideoIdAndTimestamp(url: content.url) { return .youtube(videoId: videoId, timestamp: forcedTimestamp ?? timestamp) + } else if let (videoId, timestamp) = extractVimeoVideoIdAndTimestamp(url: content.url) { + return .vimeo(videoId: videoId, timestamp: forcedTimestamp ?? timestamp) + } else if let embedUrl = content.embedUrl, isTwitchVideoUrl(embedUrl) { + return .twitch(url: embedUrl) } else { return .iframe(url: content.embedUrl ?? content.url) } @@ -48,6 +53,8 @@ func webEmbedImplementation(for type: WebEmbedType) -> WebEmbedImplementation { return YoutubeEmbedImplementation(videoId: videoId, timestamp: timestamp) case let .vimeo(videoId, timestamp): return VimeoEmbedImplementation(videoId: videoId, timestamp: timestamp) + case let .twitch(url): + return TwitchEmbedImplementation(url: url) case let .iframe(url): return GenericEmbedImplementation(url: url) } diff --git a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift index bd59af1e90..25d71fd5f1 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift @@ -151,6 +151,8 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { if case let .timecode(time) = seek { self.playerNode.seek(timestamp: time) + } else { + self.playerNode.play() } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/YoutubeEmbedImplementation.swift b/submodules/TelegramUniversalVideoContent/Sources/YoutubeEmbedImplementation.swift index 421bac9852..7bc1d1bf88 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/YoutubeEmbedImplementation.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/YoutubeEmbedImplementation.swift @@ -93,8 +93,8 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation { private var ignoreEarlierTimestamps = false private var status : MediaPlayerStatus - private var ready: Bool = false - private var started: Bool = false + private var ready = false + private var started = false private var ignorePosition: Int? private enum PlaybackDelay { @@ -176,7 +176,7 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation { return } - if let eval = evalImpl { + if let eval = self.evalImpl { eval("play();") } @@ -184,7 +184,7 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation { } func pause() { - if let eval = evalImpl { + if let eval = self.evalImpl { eval("pause();") } } diff --git a/submodules/TelegramUpdateUI/Sources/UpdateInfoController.swift b/submodules/TelegramUpdateUI/Sources/UpdateInfoController.swift index 4ffa10b036..ebf7e3e051 100644 --- a/submodules/TelegramUpdateUI/Sources/UpdateInfoController.swift +++ b/submodules/TelegramUpdateUI/Sources/UpdateInfoController.swift @@ -68,7 +68,7 @@ private enum UpdateInfoControllerEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: Any) -> ListViewItem { + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! UpdateInfoControllerArguments switch self { case let .info(theme, icon, title, text, entities): @@ -76,7 +76,7 @@ private enum UpdateInfoControllerEntry: ItemListNodeEntry { arguments.linkAction(action, itemLink) }) case let .update(theme, title): - return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.openAppStorePage() }) } @@ -121,8 +121,8 @@ public func updateInfoController(context: AccountContext, appUpdateInfo: AppUpda let leftNavigationButton = appUpdateInfo.blocking ? nil : ItemListNavigationButton(content: .text(presentationData.strings.Update_Skip), style: .regular, enabled: true, action: { dismissImpl?() }) - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Update_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: updateInfoControllerEntries(theme: presentationData.theme, strings: presentationData.strings, appIcon: appIcon, appUpdateInfo: appUpdateInfo), style: .blocks, animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Update_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: updateInfoControllerEntries(theme: presentationData.theme, strings: presentationData.strings, appIcon: appIcon, appUpdateInfo: appUpdateInfo), style: .blocks, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/TelegramVoip/Sources/OngoingCallContext.swift b/submodules/TelegramVoip/Sources/OngoingCallContext.swift index 0ed0ed551e..210144168a 100644 --- a/submodules/TelegramVoip/Sources/OngoingCallContext.swift +++ b/submodules/TelegramVoip/Sources/OngoingCallContext.swift @@ -74,6 +74,7 @@ private let setupLogs: Bool = { public enum OngoingCallContextState { case initializing case connected + case reconnecting case failed } @@ -151,6 +152,8 @@ public final class OngoingCallContext { return .connected case .failed: return .failed + case .reconnecting: + return .reconnecting default: return .failed } @@ -241,9 +244,10 @@ public final class OngoingCallContext { })) } - public func stop(callId: CallId? = nil, sendDebugLogs: Bool = false) { + public func stop(callId: CallId? = nil, sendDebugLogs: Bool = false, debugLogValue: Promise) { self.withContext { context in context.stop { debugLog, bytesSentWifi, bytesReceivedWifi, bytesSentMobile, bytesReceivedMobile in + debugLogValue.set(.single(debugLog)) let delta = NetworkUsageStatsConnectionsEntry( cellular: NetworkUsageStatsDirectionsEntry( incoming: bytesReceivedMobile, @@ -254,7 +258,7 @@ public final class OngoingCallContext { updateAccountNetworkUsageStats(account: self.account, category: .call, delta: delta) if let callId = callId, let debugLog = debugLog, sendDebugLogs { - let _ = saveCallDebugLog(account: self.account, callId: callId, log: debugLog).start() + let _ = saveCallDebugLog(network: self.account.network, callId: callId, log: debugLog).start() } } let derivedState = context.getDerivedState() diff --git a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift index a1e70f6726..b5e386a3a8 100644 --- a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift +++ b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift @@ -27,9 +27,34 @@ private final class PeerChannelMembersOnlineContext { } } +private final class ProfileDataPreloadContext { + let subscribers = Bag<() -> Void>() + + let disposable: Disposable + var emptyTimer: SwiftSignalKit.Timer? + + init(disposable: Disposable) { + self.disposable = disposable + } +} + +private final class ProfileDataPhotoPreloadContext { + let subscribers = Bag<(Any?) -> Void>() + + let disposable: Disposable + var value: Any? + var emptyTimer: SwiftSignalKit.Timer? + + init(disposable: Disposable) { + self.disposable = disposable + } +} + private final class PeerChannelMemberCategoriesContextsManagerImpl { fileprivate var contexts: [PeerId: PeerChannelMemberCategoriesContext] = [:] fileprivate var onlineContexts: [PeerId: PeerChannelMembersOnlineContext] = [:] + fileprivate var profileDataPreloadContexts: [PeerId: ProfileDataPreloadContext] = [:] + fileprivate var profileDataPhotoPreloadContexts: [PeerId: ProfileDataPhotoPreloadContext] = [:] func getContext(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, key: PeerChannelMemberContextKey, requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl) { if let current = self.contexts[peerId] { @@ -121,6 +146,121 @@ private final class PeerChannelMemberCategoriesContextsManagerImpl { context.loadMore(control) } } + + func profileData(postbox: Postbox, network: Network, peerId: PeerId, customData: Signal?) -> Disposable { + let context: ProfileDataPreloadContext + if let current = self.profileDataPreloadContexts[peerId] { + context = current + } else { + let disposable = DisposableSet() + context = ProfileDataPreloadContext(disposable: disposable) + self.profileDataPreloadContexts[peerId] = context + + if let customData = customData { + disposable.add(customData.start()) + } + + /*disposable.set(signal.start(next: { [weak context] value in + guard let context = context else { + return + } + context.value = value + for f in context.subscribers.copyItems() { + f(value) + } + }))*/ + } + + if let emptyTimer = context.emptyTimer { + emptyTimer.invalidate() + context.emptyTimer = nil + } + + let index = context.subscribers.add({ + }) + //updated(context.value ?? 0) + + return ActionDisposable { [weak self, weak context] in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + if let current = strongSelf.profileDataPreloadContexts[peerId], let context = context, current === context { + current.subscribers.remove(index) + if current.subscribers.isEmpty { + if current.emptyTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: 60.0, repeat: false, completion: { [weak context] in + if let current = strongSelf.profileDataPreloadContexts[peerId], let context = context, current === context { + if current.subscribers.isEmpty { + strongSelf.profileDataPreloadContexts.removeValue(forKey: peerId) + current.disposable.dispose() + } + } + }, queue: Queue.mainQueue()) + current.emptyTimer = timer + timer.start() + } + } + } + } + } + } + + func profilePhotos(postbox: Postbox, network: Network, peerId: PeerId, fetch: Signal, updated: @escaping (Any?) -> Void) -> Disposable { + let context: ProfileDataPhotoPreloadContext + if let current = self.profileDataPhotoPreloadContexts[peerId] { + context = current + } else { + let disposable = MetaDisposable() + context = ProfileDataPhotoPreloadContext(disposable: disposable) + self.profileDataPhotoPreloadContexts[peerId] = context + + disposable.set(fetch.start(next: { [weak context] value in + guard let context = context else { + return + } + context.value = value + for f in context.subscribers.copyItems() { + f(value) + } + })) + } + + if let emptyTimer = context.emptyTimer { + emptyTimer.invalidate() + context.emptyTimer = nil + } + + let index = context.subscribers.add({ next in + updated(next) + }) + updated(context.value) + + return ActionDisposable { [weak self, weak context] in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + if let current = strongSelf.profileDataPhotoPreloadContexts[peerId], let context = context, current === context { + current.subscribers.remove(index) + if current.subscribers.isEmpty { + if current.emptyTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: 60.0, repeat: false, completion: { [weak context] in + if let current = strongSelf.profileDataPhotoPreloadContexts[peerId], let context = context, current === context { + if current.subscribers.isEmpty { + strongSelf.profileDataPhotoPreloadContexts.removeValue(forKey: peerId) + current.disposable.dispose() + } + } + }, queue: Queue.mainQueue()) + current.emptyTimer = timer + timer.start() + } + } + } + } + } + } } public final class PeerChannelMemberCategoriesContextsManager { @@ -394,4 +534,35 @@ public final class PeerChannelMemberCategoriesContextsManager { } |> runOn(Queue.mainQueue()) } + + public func profileData(postbox: Postbox, network: Network, peerId: PeerId, customData: Signal?) -> Signal { + return Signal { [weak self] subscriber in + guard let strongSelf = self else { + subscriber.putCompletion() + return EmptyDisposable + } + let disposable = strongSelf.impl.syncWith({ impl -> Disposable in + return impl.profileData(postbox: postbox, network: network, peerId: peerId, customData: customData) + }) + return disposable ?? EmptyDisposable + } + |> runOn(Queue.mainQueue()) + } + + public func profilePhotos(postbox: Postbox, network: Network, peerId: PeerId, fetch: Signal) -> Signal { + return Signal { [weak self] subscriber in + guard let strongSelf = self else { + subscriber.putNext(0) + subscriber.putCompletion() + return EmptyDisposable + } + let disposable = strongSelf.impl.syncWith({ impl -> Disposable in + return impl.profilePhotos(postbox: postbox, network: network, peerId: peerId, fetch: fetch, updated: { value in + subscriber.putNext(value) + }) + }) + return disposable ?? EmptyDisposable + } + |> runOn(Queue.mainQueue()) + } } diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 82cc311a45..33aa43790d 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -560,14 +560,14 @@ public func trimChatInputText(_ text: NSAttributedString) -> NSAttributedString } public func breakChatInputText(_ text: NSAttributedString) -> [NSAttributedString] { - if text.length <= 4000 { + if text.length <= 4096 { return [text] } else { let rawText: NSString = text.string as NSString var result: [NSAttributedString] = [] var offset = 0 while offset < text.length { - var range = NSRange(location: offset, length: min(text.length - offset, 4000)) + var range = NSRange(location: offset, length: min(text.length - offset, 4096)) if range.upperBound < text.length { inner: for i in (range.lowerBound ..< range.upperBound).reversed() { let c = rawText.character(at: i) diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index 476efa7ddf..e3cad1726e 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -51,7 +51,7 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti let string = NSMutableAttributedString(string: text, attributes: [NSAttributedString.Key.font: baseFont, NSAttributedString.Key.foregroundColor: baseColor]) var skipEntity = false var underlineAllLinks = false - if linkColor.isEqual(baseColor) { + if linkColor.argb == baseColor.argb { underlineAllLinks = true } var fontAttributes: [NSRange: ChatTextFontAttributes] = [:] @@ -209,6 +209,15 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti string.insert(NSAttributedString(string: paragraphBreak), at: paragraphRange.upperBound) rangeOffset += paragraphBreak.count + case .BankCard: + string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) + if underlineLinks && underlineAllLinks { + string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) + } + if nsString == nil { + nsString = text as NSString + } + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard), value: nsString!.substring(with: range), range: range) case let .Custom(type): if type == ApplicationSpecificEntityType.Timecode { string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) diff --git a/submodules/TextFormat/Sources/TelegramAttributes.swift b/submodules/TextFormat/Sources/TelegramAttributes.swift index b05f982d82..ab276a41e3 100644 --- a/submodules/TextFormat/Sources/TelegramAttributes.swift +++ b/submodules/TextFormat/Sources/TelegramAttributes.swift @@ -37,6 +37,7 @@ public struct TelegramTextAttributes { public static let PeerTextMention = "TelegramPeerTextMention" public static let BotCommand = "TelegramBotCommand" public static let Hashtag = "TelegramHashtag" + public static let BankCard = "TelegramBankCard" public static let Timecode = "TelegramTimecode" public static let BlockQuote = "TelegramBlockQuote" } diff --git a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift index a196e51f00..81fc232615 100644 --- a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift +++ b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift @@ -34,12 +34,12 @@ private func cancelScrollViewGestures(view: UIView?) { } } -private func generateKnobImage(color: UIColor, inverted: Bool = false) -> UIImage? { +private func generateKnobImage(color: UIColor, diameter: CGFloat, inverted: Bool = false) -> UIImage? { let f: (CGSize, CGContext) -> Void = { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(color.cgColor) context.fill(CGRect(origin: CGPoint(x: (size.width - 2.0) / 2.0, y: size.width / 2.0), size: CGSize(width: 2.0, height: size.height - size.width / 2.0 - 1.0))) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: floor((size.width - diameter) / 2.0), y: floor((size.width - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter))) context.fillEllipse(in: CGRect(origin: CGPoint(x: (size.width - 2.0) / 2.0, y: size.width + 2.0), size: CGSize(width: 2.0, height: 2.0))) } let size = CGSize(width: 12.0, height: 12.0 + 2.0 + 2.0) @@ -53,10 +53,12 @@ private func generateKnobImage(color: UIColor, inverted: Bool = false) -> UIImag public final class TextSelectionTheme { public let selection: UIColor public let knob: UIColor + public let knobDiameter: CGFloat - public init(selection: UIColor, knob: UIColor) { + public init(selection: UIColor, knob: UIColor, knobDiameter: CGFloat = 12.0) { self.selection = selection self.knob = knob + self.knobDiameter = knobDiameter } } @@ -204,6 +206,9 @@ public final class TextSelectionNode: ASDisplayNode { public let highlightAreaNode: ASDisplayNode + private var recognizer: TextSelectionGetureRecognizer? + private var displayLinkAnimator: DisplayLinkAnimator? + public init(theme: TextSelectionTheme, strings: PresentationStrings, textNode: TextNode, updateIsActive: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, rootNode: ASDisplayNode, performAction: @escaping (String, TextSelectionAction) -> Void) { self.theme = theme self.strings = strings @@ -214,13 +219,13 @@ public final class TextSelectionNode: ASDisplayNode { self.performAction = performAction self.leftKnob = ASImageNode() self.leftKnob.isUserInteractionEnabled = false - self.leftKnob.image = generateKnobImage(color: theme.knob) + self.leftKnob.image = generateKnobImage(color: theme.knob, diameter: theme.knobDiameter) self.leftKnob.displaysAsynchronously = false self.leftKnob.displayWithoutProcessing = true self.leftKnob.alpha = 0.0 self.rightKnob = ASImageNode() self.rightKnob.isUserInteractionEnabled = false - self.rightKnob.image = generateKnobImage(color: theme.knob, inverted: true) + self.rightKnob.image = generateKnobImage(color: theme.knob, diameter: theme.knobDiameter, inverted: true) self.rightKnob.displaysAsynchronously = false self.rightKnob.displayWithoutProcessing = true self.rightKnob.alpha = 0.0 @@ -255,18 +260,17 @@ public final class TextSelectionNode: ASDisplayNode { let mappedPoint = strongSelf.view.convert(point, to: strongSelf.textNode.view) if let stringIndex = strongSelf.textNode.attributesAtPoint(mappedPoint, orNearest: true)?.0 { - //let string = attributedString.string as NSString - var updatedMin = currentRange.0 - var updatedMax = currentRange.1 + var updatedLeft = currentRange.0 + var updatedRight = currentRange.1 switch knob { case .left: - updatedMin = stringIndex + updatedLeft = stringIndex case .right: - updatedMax = stringIndex + updatedRight = stringIndex } - let updatedRange = NSRange(location: min(updatedMin, updatedMax), length: max(updatedMin, updatedMax) - min(updatedMin, updatedMax)) - if strongSelf.currentRange?.0 != updatedMin || strongSelf.currentRange?.1 != updatedMax { - strongSelf.currentRange = (updatedMin, updatedMax) + if strongSelf.currentRange?.0 != updatedLeft || strongSelf.currentRange?.1 != updatedRight { + strongSelf.currentRange = (updatedLeft, updatedRight) + let updatedRange = NSRange(location: min(updatedLeft, updatedRight), length: max(updatedLeft, updatedRight) - min(updatedLeft, updatedRight)) strongSelf.updateSelection(range: updatedRange, animateIn: false) } @@ -297,12 +301,12 @@ public final class TextSelectionNode: ASDisplayNode { let inputRange = CFRangeMake(0, string.length) let flag = UInt(kCFStringTokenizerUnitWord) let locale = CFLocaleCopyCurrent() - let tokenizer = CFStringTokenizerCreate( kCFAllocatorDefault, string as CFString, inputRange, flag, locale) + let tokenizer = CFStringTokenizerCreate(kCFAllocatorDefault, string as CFString, inputRange, flag, locale) var tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer) while !tokenType.isEmpty { let currentTokenRange = CFStringTokenizerGetCurrentTokenRange(tokenizer) - if currentTokenRange.location <= stringIndex && currentTokenRange.location + currentTokenRange.length > stringIndex { + if currentTokenRange.location <= stringIndex && currentTokenRange.location + currentTokenRange.length > stringIndex { resultRange = NSRange(location: currentTokenRange.location, length: currentTokenRange.length) break } @@ -324,6 +328,7 @@ public final class TextSelectionNode: ASDisplayNode { self?.dismissSelection() self?.updateIsActive(false) } + self.recognizer = recognizer self.view.addGestureRecognizer(recognizer) } @@ -337,16 +342,70 @@ public final class TextSelectionNode: ASDisplayNode { } } + public func pretendInitiateSelection() { + guard let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString else { + return + } + + var resultRange: NSRange? + let stringIndex = 0 + let string = attributedString.string as NSString + + let inputRange = CFRangeMake(0, string.length) + let flag = UInt(kCFStringTokenizerUnitWord) + let locale = CFLocaleCopyCurrent() + let tokenizer = CFStringTokenizerCreate(kCFAllocatorDefault, string as CFString, inputRange, flag, locale) + var tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer) + + while !tokenType.isEmpty { + let currentTokenRange = CFStringTokenizerGetCurrentTokenRange(tokenizer) + if currentTokenRange.location <= stringIndex && currentTokenRange.location + currentTokenRange.length > stringIndex { + resultRange = NSRange(location: currentTokenRange.location, length: currentTokenRange.length) + break + } + tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer) + } + if resultRange == nil { + resultRange = NSRange(location: stringIndex, length: 1) + } + + self.currentRange = resultRange.flatMap { + ($0.lowerBound, $0.upperBound) + } + self.updateSelection(range: resultRange, animateIn: true) + self.updateIsActive(true) + } + + public func pretendExtendSelection(to index: Int) { + guard let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString, let endRangeRect = cachedLayout.rangeRects(in: NSRange(location: index, length: 1))?.rects.first else { + return + } + let startPoint = self.rightKnob.frame.center + let endPoint = endRangeRect.center + let displayLinkAnimator = DisplayLinkAnimator(duration: 0.3, from: 0.0, to: 1.0, update: { [weak self] progress in + guard let strongSelf = self else { + return + } + let point = CGPoint(x: (1.0 - progress) * startPoint.x + progress * endPoint.x, y: (1.0 - progress) * startPoint.y + progress * endPoint.y) + strongSelf.recognizer?.moveKnob?(.right, point) + }, completion: { [weak self] in + guard let strongSelf = self else { + return + } + }) + self.displayLinkAnimator = displayLinkAnimator + } + private func updateSelection(range: NSRange?, animateIn: Bool) { - var rects: [CGRect]? + var rects: (rects: [CGRect], start: TextRangeRectEdge, end: TextRangeRectEdge)? if let range = range { rects = self.textNode.rangeRects(in: range) } - self.currentRects = rects + self.currentRects = rects?.rects - if let rects = rects, !rects.isEmpty { + if let (rects, startEdge, endEdge) = rects, !rects.isEmpty { let highlightOverlay: LinkHighlightingNode if let current = self.highlightOverlay { highlightOverlay = current @@ -362,8 +421,8 @@ public final class TextSelectionNode: ASDisplayNode { highlightOverlay.frame = self.bounds highlightOverlay.updateRects(rects) if let image = self.leftKnob.image { - self.leftKnob.frame = CGRect(origin: CGPoint(x: floor(rects[0].minX - 1.0 - image.size.width / 2.0), y: rects[0].minY - 1.0 - image.size.width), size: CGSize(width: image.size.width, height: image.size.width + rects[0].height + 2.0)) - self.rightKnob.frame = CGRect(origin: CGPoint(x: floor(rects[rects.count - 1].maxX + 1.0 - image.size.width / 2.0), y: rects[rects.count - 1].maxY + 1.0 - (rects[0].height + 2.0)), size: CGSize(width: image.size.width, height: image.size.width + rects[0].height + 2.0)) + self.leftKnob.frame = CGRect(origin: CGPoint(x: floor(startEdge.x - image.size.width / 2.0), y: startEdge.y + 1.0 - 12.0), size: CGSize(width: image.size.width, height: self.theme.knobDiameter + startEdge.height + 2.0)) + self.rightKnob.frame = CGRect(origin: CGPoint(x: floor(endEdge.x + 1.0 - image.size.width / 2.0), y: endEdge.y + endEdge.height + 3.0 - (endEdge.height + 2.0)), size: CGSize(width: image.size.width, height: self.theme.knobDiameter + endEdge.height + 2.0)) } if self.leftKnob.alpha.isZero { highlightOverlay.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) diff --git a/submodules/UndoUI/BUCK b/submodules/UndoUI/BUCK index 5170abfb5a..64b36cb8d5 100644 --- a/submodules/UndoUI/BUCK +++ b/submodules/UndoUI/BUCK @@ -19,6 +19,8 @@ static_library( "//submodules/AnimationUI:AnimationUI", "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/AppBundle:AppBundle", + "//submodules/StickerResources:StickerResources", + "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 49e1b0e111..8dd4662294 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -2,6 +2,9 @@ import Foundation import UIKit import Display import TelegramPresentationData +import SyncCore +import Postbox +import TelegramCore public enum UndoOverlayContent { case removedChat(text: String) @@ -11,6 +14,14 @@ public enum UndoOverlayContent { case succeed(text: String) case emoji(path: String, text: String) case swipeToReply(title: String, text: String) + case actionSucceeded(title: String, text: String, cancel: String) + case stickersModified(title: String, text: String, undo: Bool, info: StickerPackCollectionInfo, topItem: ItemCollectionItem?, account: Account) +} + +public enum UndoOverlayAction { + case info + case undo + case commit } public final class UndoOverlayController: ViewController { @@ -18,11 +29,12 @@ public final class UndoOverlayController: ViewController { public let content: UndoOverlayContent private let elevatedLayout: Bool private let animateInAsReplacement: Bool - private var action: (Bool) -> Void + private var action: (UndoOverlayAction) -> Bool private var didPlayPresentationAnimation = false + private var dismissed = false - public init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, animateInAsReplacement: Bool = false, action: @escaping (Bool) -> Void) { + public init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, animateInAsReplacement: Bool = false, action: @escaping (UndoOverlayAction) -> Bool) { self.presentationData = presentationData self.content = content self.elevatedLayout = elevatedLayout @@ -40,7 +52,7 @@ public final class UndoOverlayController: ViewController { override public func loadDisplayNode() { self.displayNode = UndoOverlayControllerNode(presentationData: self.presentationData, content: self.content, elevatedLayout: self.elevatedLayout, action: { [weak self] value in - self?.action(value) + return self?.action(value) ?? false }, dismiss: { [weak self] in self?.dismiss() }) @@ -48,12 +60,12 @@ public final class UndoOverlayController: ViewController { } public func dismissWithCommitAction() { - self.action(true) + self.action(.commit) self.dismiss() } public func dismissWithCommitActionAndReplacementAnimation() { - self.action(true) + self.action(.commit) (self.displayNode as! UndoOverlayControllerNode).animateOutWithReplacement(completion: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) }) @@ -75,6 +87,10 @@ public final class UndoOverlayController: ViewController { } override public func dismiss(completion: (() -> Void)? = nil) { + guard !self.dismissed else { + return + } + self.dismissed = true (self.displayNode as! UndoOverlayControllerNode).animateOut(completion: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) completion?() diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index f89c417530..e5308845aa 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -9,23 +9,31 @@ import Markdown import RadialStatusNode import AppBundle import AnimatedStickerNode +import TelegramAnimatedStickerNode import AnimationUI +import SyncCore +import Postbox +import TelegramCore +import StickerResources final class UndoOverlayControllerNode: ViewControllerTracingNode { private let elevatedLayout: Bool - private let statusNode: RadialStatusNode + private var statusNode: RadialStatusNode? private let timerTextNode: ImmediateTextNode private let iconNode: ASImageNode? private let iconCheckNode: RadialStatusNode? private let animationNode: AnimationNode? - private let animatedStickerNode: AnimatedStickerNode? + private var animatedStickerNode: AnimatedStickerNode? + private var stillStickerNode: TransformImageNode? + private var stickerImageSize: CGSize? private let titleNode: ImmediateTextNode private let textNode: ImmediateTextNode - private let buttonTextNode: ImmediateTextNode private let buttonNode: HighlightTrackingButtonNode + private let undoButtonTextNode: ImmediateTextNode + private let undoButtonNode: HighlightTrackingButtonNode private let panelNode: ASDisplayNode private let panelWrapperNode: ASDisplayNode - private let action: (Bool) -> Void + private let action: (UndoOverlayAction) -> Bool private let dismiss: () -> Void private let effectView: UIView @@ -38,7 +46,9 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { private var validLayout: ContainerViewLayout? - init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, action: @escaping (Bool) -> Void, dismiss: @escaping () -> Void) { + private var fetchResourceDisposable: Disposable? + + init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, action: @escaping (UndoOverlayAction) -> Bool, dismiss: @escaping () -> Void) { self.elevatedLayout = elevatedLayout self.action = action @@ -55,7 +65,11 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.displaysAsynchronously = false self.textNode.maximumNumberOfLines = 0 + self.buttonNode = HighlightTrackingButtonNode() + var displayUndo = true + var undoText = presentationData.strings.Undo_Undo + var undoTextColor = UIColor(rgb: 0x5ac8fa) if presentationData.theme.overallDarkAppearance { self.animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor @@ -72,6 +86,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white) displayUndo = true self.originalRemainingSeconds = 5 + self.statusNode = RadialStatusNode(backgroundNodeColor: .clear) case let .archivedChat(_, title, text, undo): if undo { self.iconNode = ASImageNode() @@ -122,6 +137,23 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.maximumNumberOfLines = 2 displayUndo = false self.originalRemainingSeconds = 5 + case let .actionSucceeded(title, text, cancel): + self.iconNode = nil + self.iconCheckNode = nil + self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0) + self.animatedStickerNode = nil + + undoTextColor = UIColor(rgb: 0xff7b74) + + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural) + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + self.textNode.attributedText = attributedText + displayUndo = true + undoText = cancel + self.originalRemainingSeconds = 5 case let .emoji(path, text): self.iconNode = nil self.iconCheckNode = nil @@ -147,17 +179,102 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.maximumNumberOfLines = 2 displayUndo = false self.originalRemainingSeconds = 5 + case let .stickersModified(title, text, undo, info, topItem, account): + self.iconNode = nil + self.iconCheckNode = nil + self.animationNode = nil + + let stillStickerNode = TransformImageNode() + + self.stillStickerNode = stillStickerNode + + enum StickerPackThumbnailItem { + case still(TelegramMediaImageRepresentation) + case animated(MediaResource) + } + + var thumbnailItem: StickerPackThumbnailItem? + var resourceReference: MediaResourceReference? + + if let thumbnail = info.thumbnail { + if info.flags.contains(.isAnimated) { + thumbnailItem = .animated(thumbnail.resource) + resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource) + } else { + thumbnailItem = .still(thumbnail) + resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource) + } + } else if let item = topItem as? StickerPackItem { + if item.file.isAnimatedSticker { + thumbnailItem = .animated(item.file.resource) + resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: item.file.resource) + } else if let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource { + thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource)) + resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource) + } + } + + var updatedImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + var updatedFetchSignal: Signal? + + let imageBoundingSize = CGSize(width: 34.0, height: 34.0) + + if let thumbnailItem = thumbnailItem { + switch thumbnailItem { + case let .still(representation): + let stillImageSize = representation.dimensions.cgSize.aspectFitted(imageBoundingSize) + self.stickerImageSize = stillImageSize + + updatedImageSignal = chatMessageStickerPackThumbnail(postbox: account.postbox, resource: representation.resource) + case let .animated(resource): + self.stickerImageSize = imageBoundingSize + + updatedImageSignal = chatMessageStickerPackThumbnail(postbox: account.postbox, resource: resource, animated: true) + } + if let resourceReference = resourceReference { + updatedFetchSignal = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: resourceReference) + } + } else { + updatedImageSignal = .single({ _ in return nil }) + updatedFetchSignal = .complete() + } + + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural) + self.textNode.attributedText = attributedText + self.textNode.maximumNumberOfLines = 2 + displayUndo = undo + self.originalRemainingSeconds = 2 + + if let updatedFetchSignal = updatedFetchSignal { + self.fetchResourceDisposable = updatedFetchSignal.start() + } + + if let updatedImageSignal = updatedImageSignal { + stillStickerNode.setSignal(updatedImageSignal) + } + + if let thumbnailItem = thumbnailItem { + switch thumbnailItem { + case .still: + break + case let .animated(resource): + let animatedStickerNode = AnimatedStickerNode() + self.animatedStickerNode = animatedStickerNode + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: 80, height: 80, mode: .cached) + } + } } self.remainingSeconds = self.originalRemainingSeconds - self.statusNode = RadialStatusNode(backgroundNodeColor: .clear) + self.undoButtonTextNode = ImmediateTextNode() + self.undoButtonTextNode.displaysAsynchronously = false + self.undoButtonTextNode.attributedText = NSAttributedString(string: undoText, font: Font.regular(17.0), textColor: undoTextColor) - self.buttonTextNode = ImmediateTextNode() - self.buttonTextNode.displaysAsynchronously = false - self.buttonTextNode.attributedText = NSAttributedString(string: presentationData.strings.Undo_Undo, font: Font.regular(17.0), textColor: UIColor(rgb: 0x5ac8fa)) - - self.buttonNode = HighlightTrackingButtonNode() + self.undoButtonNode = HighlightTrackingButtonNode() self.panelNode = ASDisplayNode() if presentationData.theme.overallDarkAppearance { @@ -175,37 +292,48 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { super.init() switch content { - case .removedChat: - self.panelWrapperNode.addSubnode(self.timerTextNode) - self.panelWrapperNode.addSubnode(self.statusNode) - case .archivedChat, .hidArchive, .revealedArchive, .succeed, .emoji, .swipeToReply: - break + case .removedChat: + self.panelWrapperNode.addSubnode(self.timerTextNode) + case .archivedChat, .hidArchive, .revealedArchive, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified: + break } + self.statusNode.flatMap(self.panelWrapperNode.addSubnode) self.iconNode.flatMap(self.panelWrapperNode.addSubnode) self.iconCheckNode.flatMap(self.panelWrapperNode.addSubnode) self.animationNode.flatMap(self.panelWrapperNode.addSubnode) + self.stillStickerNode.flatMap(self.panelWrapperNode.addSubnode) self.animatedStickerNode.flatMap(self.panelWrapperNode.addSubnode) self.panelWrapperNode.addSubnode(self.titleNode) self.panelWrapperNode.addSubnode(self.textNode) + self.panelWrapperNode.addSubnode(self.buttonNode) if displayUndo { - self.panelWrapperNode.addSubnode(self.buttonTextNode) - self.panelWrapperNode.addSubnode(self.buttonNode) + self.panelWrapperNode.addSubnode(self.undoButtonTextNode) + self.panelWrapperNode.addSubnode(self.undoButtonNode) } self.addSubnode(self.panelNode) self.addSubnode(self.panelWrapperNode) - self.buttonNode.highligthedChanged = { [weak self] highlighted in + self.undoButtonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { - strongSelf.buttonTextNode.layer.removeAnimation(forKey: "opacity") - strongSelf.buttonTextNode.alpha = 0.4 + strongSelf.undoButtonTextNode.layer.removeAnimation(forKey: "opacity") + strongSelf.undoButtonTextNode.alpha = 0.4 } else { - strongSelf.buttonTextNode.alpha = 1.0 - strongSelf.buttonTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.undoButtonTextNode.alpha = 1.0 + strongSelf.undoButtonTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.undoButtonNode.addTarget(self, action: #selector(self.undoButtonPressed), forControlEvents: .touchUpInside) + + self.animatedStickerNode?.started = { [weak self] in + self?.stillStickerNode?.isHidden = true + } + } + + deinit { + self.fetchResourceDisposable?.dispose() } override func didLoad() { @@ -216,7 +344,13 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } @objc private func buttonPressed() { - self.action(false) + if self.action(.info) { + self.dismiss() + } + } + + @objc private func undoButtonPressed() { + self.action(.undo) self.dismiss() } @@ -225,7 +359,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.remainingSeconds -= 1 } if self.remainingSeconds == 0 { - self.action(true) + self.action(.commit) self.dismiss() } else { if !self.timerTextNode.bounds.size.width.isZero, let snapshot = self.timerTextNode.view.snapshotContentTree() { @@ -272,9 +406,9 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let margin: CGFloat = 16.0 - let buttonTextSize = self.buttonTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + let buttonTextSize = self.undoButtonTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) let buttonMinX: CGFloat - if self.buttonNode.supernode != nil { + if self.undoButtonNode.supernode != nil { buttonMinX = layout.size.width - layout.safeInsets.left - rightInset - buttonTextSize.width - margin * 2.0 } else { buttonMinX = layout.size.width - layout.safeInsets.left - rightInset @@ -302,8 +436,12 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.effectView.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width - margin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight) let buttonTextFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - rightInset - buttonTextSize.width - margin * 2.0, y: floor((contentHeight - buttonTextSize.height) / 2.0)), size: buttonTextSize) - transition.updateFrame(node: self.buttonTextNode, frame: buttonTextFrame) - self.buttonNode.frame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - rightInset - buttonTextSize.width - 8.0 - margin * 2.0, y: 0.0), size: CGSize(width: layout.safeInsets.right + rightInset + buttonTextSize.width + 8.0 + margin, height: contentHeight)) + transition.updateFrame(node: self.undoButtonTextNode, frame: buttonTextFrame) + + let undoButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - rightInset - buttonTextSize.width - 8.0 - margin * 2.0, y: 0.0), size: CGSize(width: layout.safeInsets.right + rightInset + buttonTextSize.width + 8.0 + margin, height: contentHeight)) + self.undoButtonNode.frame = undoButtonFrame + + self.buttonNode.frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: 0.0), size: CGSize(width: undoButtonFrame.minX - layout.safeInsets.left, height: contentHeight)) var textContentHeight = textSize.height var textOffset: CGFloat = 0.0 @@ -337,7 +475,22 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { transition.updateFrame(node: animationNode, frame: iconFrame) } - if let animatedStickerNode = self.animatedStickerNode { + if let stickerImageSize = self.stickerImageSize { + let iconSize = stickerImageSize + let iconFrame = CGRect(origin: CGPoint(x: floor((leftInset - iconSize.width) / 2.0), y: floor((contentHeight - iconSize.height) / 2.0)), size: iconSize) + + if let stillStickerNode = self.stillStickerNode { + let makeImageLayout = stillStickerNode.asyncLayout() + let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: stickerImageSize, boundingSize: stickerImageSize, intrinsicInsets: UIEdgeInsets())) + let _ = imageApply() + transition.updateFrame(node: stillStickerNode, frame: iconFrame) + } + + if let animatedStickerNode = self.animatedStickerNode { + animatedStickerNode.updateLayout(size: iconFrame.size) + transition.updateFrame(node: animatedStickerNode, frame: iconFrame) + } + } else if let animatedStickerNode = self.animatedStickerNode { let iconSize = CGSize(width: 32.0, height: 32.0) let iconFrame = CGRect(origin: CGPoint(x: floor((leftInset - iconSize.width) / 2.0), y: floor((contentHeight - iconSize.height) / 2.0)), size: iconSize) animatedStickerNode.updateLayout(size: iconFrame.size) @@ -347,9 +500,11 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let timerTextSize = self.timerTextNode.updateLayout(CGSize(width: 100.0, height: 100.0)) transition.updateFrame(node: self.timerTextNode, frame: CGRect(origin: CGPoint(x: floor((leftInset - timerTextSize.width) / 2.0), y: floor((contentHeight - timerTextSize.height) / 2.0)), size: timerTextSize)) let statusSize: CGFloat = 30.0 - transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: floor((leftInset - statusSize) / 2.0), y: floor((contentHeight - statusSize) / 2.0)), size: CGSize(width: statusSize, height: statusSize))) - if firstLayout { - self.statusNode.transitionToState(.secretTimeout(color: .white, icon: nil, beginTime: CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, timeout: Double(self.remainingSeconds), sparks: false), completion: {}) + if let statusNode = self.statusNode { + transition.updateFrame(node: statusNode, frame: CGRect(origin: CGPoint(x: floor((leftInset - statusSize) / 2.0), y: floor((contentHeight - statusSize) / 2.0)), size: CGSize(width: statusSize, height: statusSize))) + if firstLayout { + statusNode.transitionToState(.secretTimeout(color: .white, icon: nil, beginTime: CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, timeout: Double(self.remainingSeconds), sparks: false), completion: {}) + } } } @@ -375,10 +530,17 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { }) } + self.animatedStickerNode?.visibility = true + self.checkTimer() } + var dismissed = false func animateOut(completion: @escaping () -> Void) { + guard !self.dismissed else { + return + } + self.dismissed = true self.panelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { _ in }) self.panelWrapperNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false) { _ in completion() diff --git a/submodules/UrlHandling/BUCK b/submodules/UrlHandling/BUCK index b676eb77ce..ace74ae785 100644 --- a/submodules/UrlHandling/BUCK +++ b/submodules/UrlHandling/BUCK @@ -13,7 +13,7 @@ static_library( "//submodules/MtProtoKit:MtProtoKit#shared", "//submodules/AccountContext:AccountContext", "//submodules/TelegramUIPreferences:TelegramUIPreferences", - "//submodules/WalletUrl:WalletUrl", + #"//submodules/WalletUrl:WalletUrl", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 75f53c78ae..9c1fac2cfb 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -12,7 +12,11 @@ import MtProtoKitDynamic import TelegramPresentationData import TelegramUIPreferences import AccountContext +#if ENABLE_WALLET import WalletUrl +#endif + +private let baseTelegramMePaths = ["telegram.me", "t.me", "telegram.dog"] public enum ParsedInternalPeerUrlParameter { case botStart(String) @@ -174,15 +178,34 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { } else if pathComponents[0] == "bg" { let component = pathComponents[1] let parameter: WallpaperUrlParameter - if component.count == 6, component.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789abcdefABCDEF").inverted) == nil, let color = UIColor(hexString: component) { + if [6, 8].contains(component.count), component.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789abcdefABCDEF").inverted) == nil, let color = UIColor(hexString: component) { parameter = .color(color) + } else if [13, 15, 17].contains(component.count), component.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789abcdefABCDEF-").inverted) == nil { + var rotation: Int32? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "rotation" { + rotation = Int32(value) + } + } + } + } + let components = component.components(separatedBy: "-") + if components.count == 2, let topColor = UIColor(hexString: components[0]), let bottomColor = UIColor(hexString: components[1]) { + parameter = .gradient(topColor, bottomColor, rotation) + } else { + return nil + } } else { var options: WallpaperPresentationOptions = [] var intensity: Int32? - var color: UIColor? + var topColor: UIColor? + var bottomColor: UIColor? + var rotation: Int32? if let queryItems = components.queryItems { for queryItem in queryItems { - if let value = queryItem.value{ + if let value = queryItem.value { if queryItem.name == "mode" { for option in value.components(separatedBy: "+") { switch option.lowercased() { @@ -195,14 +218,24 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { } } } else if queryItem.name == "bg_color" { - color = UIColor(hexString: value) + if [6, 8].contains(value.count), value.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789abcdefABCDEF").inverted) == nil, let color = UIColor(hexString: value) { + topColor = color + } else if [13, 15, 17].contains(value.count), value.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789abcdefABCDEF-").inverted) == nil { + let components = value.components(separatedBy: "-") + if components.count == 2, let topColorValue = UIColor(hexString: components[0]), let bottomColorValue = UIColor(hexString: components[1]) { + topColor = topColorValue + bottomColor = bottomColorValue + } + } } else if queryItem.name == "intensity" { intensity = Int32(value) + } else if queryItem.name == "rotation" { + rotation = Int32(value) } } } } - parameter = .slug(component, options, color, intensity) + parameter = .slug(component, options, topColor, bottomColor, intensity, rotation) } return .wallpaper(parameter) } else if pathComponents[0] == "addtheme" { @@ -317,7 +350,6 @@ private func resolveInternalUrl(account: Account, url: ParsedInternalUrl) -> Sig public func isTelegramMeLink(_ url: String) -> Bool { let schemes = ["http://", "https://", ""] - let baseTelegramMePaths = ["telegram.me", "t.me"] for basePath in baseTelegramMePaths { for scheme in schemes { let basePrefix = scheme + basePath + "/" @@ -331,7 +363,6 @@ public func isTelegramMeLink(_ url: String) -> Bool { public func parseProxyUrl(_ url: String) -> (host: String, port: Int32, username: String?, password: String?, secret: Data?)? { let schemes = ["http://", "https://", ""] - let baseTelegramMePaths = ["telegram.me", "t.me"] for basePath in baseTelegramMePaths { for scheme in schemes { let basePrefix = scheme + basePath + "/" @@ -351,9 +382,29 @@ public func parseProxyUrl(_ url: String) -> (host: String, port: Int32, username return nil } +public func parseStickerPackUrl(_ url: String) -> String? { + let schemes = ["http://", "https://", ""] + for basePath in baseTelegramMePaths { + for scheme in schemes { + let basePrefix = scheme + basePath + "/" + if url.lowercased().hasPrefix(basePrefix) { + if let internalUrl = parseInternalUrl(query: String(url[basePrefix.endIndex...])), case let .stickerPack(name) = internalUrl { + return name + } + } + } + } + if let parsedUrl = URL(string: url), parsedUrl.scheme == "tg", let host = parsedUrl.host, let query = parsedUrl.query { + if let internalUrl = parseInternalUrl(query: host + "?" + query), case let .stickerPack(name) = internalUrl { + return name + } + } + + return nil +} + public func parseWallpaperUrl(_ url: String) -> WallpaperUrlParameter? { let schemes = ["http://", "https://", ""] - let baseTelegramMePaths = ["telegram.me", "t.me"] for basePath in baseTelegramMePaths { for scheme in schemes { let basePrefix = scheme + basePath + "/" @@ -374,13 +425,14 @@ public func parseWallpaperUrl(_ url: String) -> WallpaperUrlParameter? { } public func resolveUrlImpl(account: Account, url: String) -> Signal { + #if ENABLE_WALLET if url.hasPrefix("ton://") { if let url = URL(string: url), let parsedUrl = parseWalletUrl(url) { return .single(.wallet(address: parsedUrl.address, amount: parsedUrl.amount, comment: parsedUrl.comment)) } } + #endif let schemes = ["http://", "https://", ""] - let baseTelegramMePaths = ["telegram.me", "t.me"] for basePath in baseTelegramMePaths { for scheme in schemes { let basePrefix = scheme + basePath + "/" diff --git a/submodules/WalletUI/BUCK b/submodules/WalletUI/BUCK index 5194a2e218..cbda117102 100644 --- a/submodules/WalletUI/BUCK +++ b/submodules/WalletUI/BUCK @@ -37,8 +37,8 @@ static_library( "//submodules/LocalAuth:LocalAuth", "//submodules/ScreenCaptureDetection:ScreenCaptureDetection", "//submodules/AnimatedStickerNode:AnimatedStickerNode", - "//submodules/WalletUrl:WalletUrl", - "//submodules/WalletCore:WalletCore", + #"//submodules/WalletUrl:WalletUrl", + #"//submodules/WalletCore:WalletCore", "//submodules/StringPluralization:StringPluralization", "//submodules/ActivityIndicator:ActivityIndicator", "//submodules/ProgressNavigationButtonNode:ProgressNavigationButtonNode", diff --git a/submodules/WalletUI/Resources/WalletStrings.mapping b/submodules/WalletUI/Resources/WalletStrings.mapping index 9f7c348920..589c6fe85e 100644 Binary files a/submodules/WalletUI/Resources/WalletStrings.mapping and b/submodules/WalletUI/Resources/WalletStrings.mapping differ diff --git a/submodules/WalletUI/Sources/ItemList/ItemListControllerNode.swift b/submodules/WalletUI/Sources/ItemList/ItemListControllerNode.swift index 72784e604b..6c382aa6d0 100644 --- a/submodules/WalletUI/Sources/ItemList/ItemListControllerNode.swift +++ b/submodules/WalletUI/Sources/ItemList/ItemListControllerNode.swift @@ -356,28 +356,6 @@ class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight @@ -406,10 +384,12 @@ class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { } } + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.leftOverlayNode.frame = CGRect(x: 0.0, y: 0.0, width: insets.left, height: layout.size.height) self.rightOverlayNode.frame = CGRect(x: layout.size.width - insets.right, y: 0.0, width: insets.right, height: layout.size.height) diff --git a/submodules/WalletUI/Sources/ItemList/Items/ItemListDisclosureItem.swift b/submodules/WalletUI/Sources/ItemList/Items/ItemListDisclosureItem.swift index 7b661a8b41..ea7f9a6e03 100644 --- a/submodules/WalletUI/Sources/ItemList/Items/ItemListDisclosureItem.swift +++ b/submodules/WalletUI/Sources/ItemList/Items/ItemListDisclosureItem.swift @@ -279,12 +279,12 @@ class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { let height: CGFloat switch item.labelStyle { - case .detailText: - height = 64.0 - case .multilineDetailText: - height = 44.0 + labelLayout.size.height - default: - height = 44.0 + case .detailText: + height = 64.0 + case .multilineDetailText: + height = 44.0 + labelLayout.size.height + default: + height = 44.0 } switch item.style { diff --git a/submodules/WalletUI/Sources/WalletInfoScreen.swift b/submodules/WalletUI/Sources/WalletInfoScreen.swift index 21624b1ffb..07db0b6db6 100644 --- a/submodules/WalletUI/Sources/WalletInfoScreen.swift +++ b/submodules/WalletUI/Sources/WalletInfoScreen.swift @@ -783,30 +783,10 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode { self.headerNode.update(size: headerFrame.size, navigationHeight: navigationHeight, offset: visualHeaderOffset, transition: transition, isScrolling: false) transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: visualListOffset), size: layout.size)) - - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), headerInsets: UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), scrollIndicatorInsets: UIEdgeInsets(top: topInset + 3.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), headerInsets: UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), scrollIndicatorInsets: UIEdgeInsets(top: topInset + 3.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if isFirstLayout { while !self.enqueuedTransactions.isEmpty { diff --git a/submodules/WalletUI/Sources/WalletInfoTransactionItem.swift b/submodules/WalletUI/Sources/WalletInfoTransactionItem.swift index ec2ee91826..cef12fb45c 100644 --- a/submodules/WalletUI/Sources/WalletInfoTransactionItem.swift +++ b/submodules/WalletUI/Sources/WalletInfoTransactionItem.swift @@ -520,6 +520,10 @@ private final class WalletInfoTransactionDateHeader: ListViewItemHeader { func node() -> ListViewItemHeaderNode { return WalletInfoTransactionDateHeaderNode(theme: self.theme, strings: self.strings, roundedTimestamp: self.localTimestamp) } + + func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) { + + } } private let sectionTitleFont = Font.semibold(17.0) diff --git a/submodules/WalletUI/Sources/WalletStrings.swift b/submodules/WalletUI/Sources/WalletStrings.swift index 9d5c0af52d..c572929515 100644 --- a/submodules/WalletUI/Sources/WalletStrings.swift +++ b/submodules/WalletUI/Sources/WalletStrings.swift @@ -448,12 +448,12 @@ public final class WalletStrings: Equatable { public var Wallet_SecureStorageReset_Title: String { return self._s[218]! } public var Wallet_Receive_CommentHeader: String { return self._s[219]! } public var Wallet_Info_ReceiveGrams: String { return self._s[220]! } - public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String { + public func Wallet_Updated_HoursAgo(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[0 * 6 + Int(form.rawValue)]!, stringValue) } - public func Wallet_Updated_HoursAgo(_ value: Int32) -> String { + public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[1 * 6 + Int(form.rawValue)]!, stringValue) diff --git a/submodules/WalletUI/Sources/WalletTransactionInfoScreen.swift b/submodules/WalletUI/Sources/WalletTransactionInfoScreen.swift index 12b9061deb..d09e8f2097 100644 --- a/submodules/WalletUI/Sources/WalletTransactionInfoScreen.swift +++ b/submodules/WalletUI/Sources/WalletTransactionInfoScreen.swift @@ -239,7 +239,7 @@ final class WalletTransactionInfoScreen: ViewController { } else { text = NSAttributedString(string: strongSelf.context.presentationData.strings.Wallet_TransactionInfo_StorageFeeInfo, font: Font.regular(14.0), textColor: .white, paragraphAlignment: .center) } - let controller = TooltipController(content: .attributedText(text), timeout: 3.0, dismissByTapOutside: true, dismissByTapOutsideSource: false, dismissImmediatelyOnLayoutUpdate: false, arrowOnBottom: false) + let controller = TooltipController(content: .attributedText(text), baseFontSize: 17.0, timeout: 3.0, dismissByTapOutside: true, dismissByTapOutsideSource: false, dismissImmediatelyOnLayoutUpdate: false, arrowOnBottom: false) controller.dismissed = { [weak self] tappedInside in if let strongSelf = self, tappedInside { if let feeInfoUrl = strongSelf.context.feeInfoUrl { diff --git a/submodules/WallpaperResources/BUCK b/submodules/WallpaperResources/BUCK index c00518c487..c1aa1769d7 100644 --- a/submodules/WallpaperResources/BUCK +++ b/submodules/WallpaperResources/BUCK @@ -18,6 +18,7 @@ static_library( "//submodules/PhotoResources:PhotoResources", "//submodules/PersistentStringHash:PersistentStringHash", "//submodules/AppBundle:AppBundle", + "//submodules/Svg:Svg", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/WallpaperResources/Sources/WallpaperCache.swift b/submodules/WallpaperResources/Sources/WallpaperCache.swift index ac5557bf32..5d01c54c3d 100644 --- a/submodules/WallpaperResources/Sources/WallpaperCache.swift +++ b/submodules/WallpaperResources/Sources/WallpaperCache.swift @@ -26,18 +26,18 @@ public final class CachedWallpaper: PostboxCoding { private let collectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 10000, highWaterItemCount: 20000) -public func cachedWallpaper(account: Account, slug: String, settings: WallpaperSettings?) -> Signal { +public func cachedWallpaper(account: Account, slug: String, settings: WallpaperSettings?, update: Bool = false) -> Signal { return account.postbox.transaction { transaction -> Signal in let key = ValueBoxKey(length: 8) key.setInt64(0, value: Int64(bitPattern: slug.persistentHashValue)) - if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: ApplicationSpecificItemCacheCollectionId.cachedWallpapers, key: key)) as? CachedWallpaper { + if !update, let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: ApplicationSpecificItemCacheCollectionId.cachedWallpapers, key: key)) as? CachedWallpaper { if let settings = settings { return .single(CachedWallpaper(wallpaper: entry.wallpaper.withUpdatedSettings(settings))) } else { return .single(entry) } } else { - return getWallpaper(account: account, slug: slug) + return getWallpaper(network: account.network, slug: slug) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/WallpaperResources/Sources/WallpaperResources.swift b/submodules/WallpaperResources/Sources/WallpaperResources.swift index 8b90a33ef5..0c39b08129 100644 --- a/submodules/WallpaperResources/Sources/WallpaperResources.swift +++ b/submodules/WallpaperResources/Sources/WallpaperResources.swift @@ -13,6 +13,7 @@ import LocalMediaResources import TelegramPresentationData import TelegramUIPreferences import AppBundle +import Svg public func wallpaperDatas(account: Account, accountManager: AccountManager, fileReference: FileMediaReference? = nil, representations: [ImageRepresentationWithReference], alwaysShowThumbnailFirst: Bool = false, thumbnail: Bool = false, onlyFullSize: Bool = false, autoFetchFullSize: Bool = false, synchronousLoad: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { if let smallestRepresentation = smallestImageRepresentation(representations.map({ $0.representation })), let largestRepresentation = largestImageRepresentation(representations.map({ $0.representation })), let smallestIndex = representations.firstIndex(where: { $0.representation == smallestRepresentation }), let largestIndex = representations.firstIndex(where: { $0.representation == largestRepresentation }) { @@ -291,10 +292,29 @@ public func wallpaperImage(account: Account, accountManager: AccountManager, fil public enum PatternWallpaperDrawMode { case thumbnail - case fastScreen case screen } +public struct PatternWallpaperArguments: TransformImageCustomArguments { + let colors: [UIColor] + let rotation: Int32? + let preview: Bool + + public init(colors: [UIColor], rotation: Int32?, preview: Bool = false) { + self.colors = colors + self.rotation = rotation + self.preview = preview + } + + public func serialized() -> NSArray { + let array = NSMutableArray() + array.addObjects(from: self.colors) + array.add(NSNumber(value: self.rotation ?? 0)) + array.add(NSNumber(value: self.preview)) + return array + } +} + private func patternWallpaperDatas(account: Account, accountManager: AccountManager, representations: [ImageRepresentationWithReference], mode: PatternWallpaperDrawMode, autoFetchFullSize: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { if let smallestRepresentation = smallestImageRepresentation(representations.map({ $0.representation })), let largestRepresentation = largestImageRepresentation(representations.map({ $0.representation })), let smallestIndex = representations.firstIndex(where: { $0.representation == smallestRepresentation }), let largestIndex = representations.firstIndex(where: { $0.representation == largestRepresentation }) { @@ -302,8 +322,6 @@ private func patternWallpaperDatas(account: Account, accountManager: AccountMana switch mode { case .thumbnail: size = largestRepresentation.dimensions.cgSize.fitted(CGSize(width: 640.0, height: 640.0)) - case .fastScreen: - size = largestRepresentation.dimensions.cgSize.fitted(CGSize(width: 1280.0, height: 1280.0)) default: size = nil } @@ -387,67 +405,108 @@ public func patternWallpaperImageInternal(thumbnailData: Data?, fullSizeData: Da } var scale: CGFloat = 0.0 - if case .fastScreen = mode { - scale = max(1.0, UIScreenScale - 1.0) - } return .single((thumbnailData, fullSizeData, fullSizeComplete)) |> map { (thumbnailData, fullSizeData, fullSizeComplete) in - return { arguments in - let drawingRect = arguments.drawingRect - var fittedSize = arguments.imageSize - if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { - fittedSize.width = arguments.boundingSize.width - } - if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { - fittedSize.height = arguments.boundingSize.height - } - - let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) - - var fullSizeImage: CGImage? - if let fullSizeData = fullSizeData, fullSizeComplete { + var fullSizeImage: CGImage? + var scaledSizeImage: CGImage? + if let fullSizeData = fullSizeData, fullSizeComplete { + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + let options = NSMutableDictionary() - options[kCGImageSourceShouldCache as NSString] = false as NSNumber - if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { - fullSizeImage = image + options.setValue(960 as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) { + scaledSizeImage = image } } + } + + return { arguments in + var scale = scale - if let combinedColor = arguments.emptyColor { + let drawingRect = arguments.drawingRect + + if let customArguments = arguments.custom as? PatternWallpaperArguments, let combinedColor = customArguments.colors.first { + if customArguments.preview { + scale = max(1.0, UIScreenScale - 1.0) + } + + let combinedColors = customArguments.colors + let colors = combinedColors.reversed().map { $0.withAlphaComponent(1.0) } let color = combinedColor.withAlphaComponent(1.0) let intensity = combinedColor.alpha - if fullSizeImage == nil { - let context = DrawingContext(size: arguments.drawingSize, scale: 1.0, clear: true) - context.withFlippedContext { c in - c.setBlendMode(.copy) - c.setFillColor(color.cgColor) - c.fill(arguments.drawingRect) - } - - addCorners(context, arguments: arguments) - - return context - } - - let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true) + let context = DrawingContext(size: arguments.drawingSize, scale: fullSizeImage == nil ? 1.0 : scale, clear: !arguments.corners.isEmpty) context.withFlippedContext { c in c.setBlendMode(.copy) - c.setFillColor(color.cgColor) - c.fill(arguments.drawingRect) - if let fullSizeImage = fullSizeImage { - c.setBlendMode(.normal) - c.interpolationQuality = .medium - c.clip(to: fittedRect, mask: fullSizeImage) - c.setFillColor(patternColor(for: color, intensity: intensity, prominent: prominent).cgColor) + if colors.count == 1 { + c.setFillColor(color.cgColor) c.fill(arguments.drawingRect) + } else { + let gradientColors = colors.map { $0.cgColor } as CFArray + let delta: CGFloat = 1.0 / (CGFloat(colors.count) - 1.0) + + var locations: [CGFloat] = [] + for i in 0 ..< colors.count { + locations.append(delta * CGFloat(i)) + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + c.saveGState() + c.translateBy(x: arguments.drawingSize.width / 2.0, y: arguments.drawingSize.height / 2.0) + c.rotate(by: CGFloat(customArguments.rotation ?? 0) * CGFloat.pi / -180.0) + c.translateBy(x: -arguments.drawingSize.width / 2.0, y: -arguments.drawingSize.height / 2.0) + + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: arguments.drawingSize.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + c.restoreGState() + } + + let image = customArguments.preview ? (scaledSizeImage ?? fullSizeImage) : fullSizeImage + if let image = image { + var fittedSize = CGSize(width: image.width, height: image.height) + if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.width = arguments.boundingSize.width + } + if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.height = arguments.boundingSize.height + } + fittedSize = fittedSize.aspectFilled(arguments.drawingRect.size) + + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + c.setBlendMode(.normal) + c.interpolationQuality = customArguments.preview ? .low : .medium + c.clip(to: fittedRect, mask: image) + + if colors.count == 1 { + c.setFillColor(patternColor(for: color, intensity: intensity, prominent: prominent).cgColor) + c.fill(arguments.drawingRect) + } else { + let gradientColors = colors.map { patternColor(for: $0, intensity: intensity, prominent: prominent).cgColor } as CFArray + let delta: CGFloat = 1.0 / (CGFloat(colors.count) - 1.0) + + var locations: [CGFloat] = [] + for i in 0 ..< colors.count { + locations.append(delta * CGFloat(i)) + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + c.translateBy(x: arguments.drawingSize.width / 2.0, y: arguments.drawingSize.height / 2.0) + c.rotate(by: CGFloat(customArguments.rotation ?? 0) * CGFloat.pi / -180.0) + c.translateBy(x: -arguments.drawingSize.width / 2.0, y: -arguments.drawingSize.height / 2.0) + + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: arguments.drawingSize.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + } } } - addCorners(context, arguments: arguments) - return context } else { return nil @@ -469,18 +528,18 @@ public func patternColor(for color: UIColor, intensity: CGFloat, prominent: Bool } else { brightness = max(0.0, min(1.0, 1.0 - brightness * 0.65)) } - let alpha = (prominent ? 0.5 : 0.4) * intensity + let alpha = (prominent ? 0.6 : 0.55) * intensity return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha) } return .black } -public func solidColor(_ color: UIColor) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func solidColorImage(_ color: UIColor) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return .single({ arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) context.withFlippedContext { c in - c.setFillColor(color.cgColor) + c.setFillColor(color.withAlphaComponent(1.0).cgColor) c.fill(arguments.drawingRect) } @@ -490,6 +549,46 @@ public func solidColor(_ color: UIColor) -> Signal<(TransformImageArguments) -> }) } +public func gradientImage(_ colors: [UIColor], rotation: Int32? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + guard !colors.isEmpty else { + return .complete() + } + guard colors.count > 1 else { + if let color = colors.first { + return solidColorImage(color) + } else { + return .complete() + } + } + return .single({ arguments in + let context = DrawingContext(size: arguments.drawingSize, clear: !arguments.corners.isEmpty) + + context.withContext { c in + let gradientColors = colors.map { $0.withAlphaComponent(1.0).cgColor } as CFArray + let delta: CGFloat = 1.0 / (CGFloat(colors.count) - 1.0) + + var locations: [CGFloat] = [] + for i in 0 ..< colors.count { + locations.append(delta * CGFloat(i)) + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + if let rotation = rotation { + c.translateBy(x: arguments.drawingSize.width / 2.0, y: arguments.drawingSize.height / 2.0) + c.rotate(by: CGFloat(rotation) * CGFloat.pi / 180.0) + c.translateBy(x: -arguments.drawingSize.width / 2.0, y: -arguments.drawingSize.height / 2.0) + } + + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: arguments.drawingSize.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + } + + addCorners(context, arguments: arguments) + + return context + }) +} + private func builtinWallpaperData() -> Signal { return Signal { subscriber in if let filePath = getAppBundle().path(forResource: "ChatWallpaperBuiltin0", ofType: "jpg"), let image = UIImage(contentsOfFile: filePath) { @@ -602,8 +701,8 @@ public func photoWallpaper(postbox: Postbox, photoLibraryResource: PhotoLibraryM } } -public func telegramThemeData(account: Account, accountManager: AccountManager, resource: MediaResource, synchronousLoad: Bool = false) -> Signal { - let maybeFetched = accountManager.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) +public func telegramThemeData(account: Account, accountManager: AccountManager, reference: MediaResourceReference, synchronousLoad: Bool = false) -> Signal { + let maybeFetched = accountManager.mediaBox.resourceData(reference.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) return maybeFetched |> take(1) |> mapToSignal { maybeData in @@ -611,15 +710,15 @@ public func telegramThemeData(account: Account, accountManager: AccountManager, let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single(loadedData) } else { - let data = account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: false) + let data = account.postbox.mediaBox.resourceData(reference.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: false) return Signal { subscriber in - let fetch = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .standalone(resource: resource)).start() + let fetch = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: reference).start() let disposable = (data |> map { data -> Data? in return data.complete ? try? Data(contentsOf: URL(fileURLWithPath: data.path)) : nil }).start(next: { next in if let data = next { - accountManager.mediaBox.storeResourceData(resource.id, data: data) + accountManager.mediaBox.storeResourceData(reference.resource.id, data: data) } subscriber.putNext(next) }, error: { error in @@ -656,9 +755,15 @@ public func drawThemeImage(context c: CGContext, theme: PresentationTheme, wallp let size = image.size.aspectFilled(drawingRect.size) c.draw(cgImage, in: CGRect(origin: CGPoint(x: (drawingRect.size.width - size.width) / 2.0, y: (drawingRect.size.height - size.height) / 2.0), size: size)) } - case let .color(value): - c.setFillColor(UIColor(rgb: UInt32(bitPattern: value)).cgColor) + case let .color(color): + c.setFillColor(UIColor(rgb: color).cgColor) c.fill(drawingRect) + case let .gradient(topColor, bottomColor, _): + let gradientColors = [UIColor(rgb: topColor), UIColor(rgb: bottomColor)].map { $0.cgColor } as CFArray + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: drawingRect.height), end: CGPoint(x: 0.0, y: 0.0), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) case .file: c.setFillColor(theme.chatList.backgroundColor.cgColor) c.fill(drawingRect) @@ -675,7 +780,7 @@ public func drawThemeImage(context c: CGContext, theme: PresentationTheme, wallp c.fill(CGRect(origin: CGPoint(x: 0.0, y: drawingRect.height - 42.0), size: CGSize(width: drawingRect.width, height: 42.0))) c.setFillColor(theme.rootController.navigationBar.separatorColor.cgColor) - c.fill(CGRect(origin: CGPoint(x: 1.0, y: drawingRect.height - 43.0), size: CGSize(width: drawingRect.width - 2.0, height: 1.0))) + c.fill(CGRect(origin: CGPoint(x: 1.0, y: drawingRect.height - 42.0 - UIScreenPixel), size: CGSize(width: drawingRect.width - 2.0, height: UIScreenPixel))) c.setFillColor(theme.rootController.navigationBar.secondaryTextColor.cgColor) c.fillEllipse(in: CGRect(origin: CGPoint(x: drawingRect.width - 28.0 - 7.0, y: drawingRect.height - 7.0 - 28.0 - UIScreenPixel), size: CGSize(width: 28.0, height: 28.0))) @@ -683,11 +788,17 @@ public func drawThemeImage(context c: CGContext, theme: PresentationTheme, wallp if let arrow = generateBackArrowImage(color: theme.rootController.navigationBar.buttonColor), let image = arrow.cgImage { c.draw(image, in: CGRect(x: 9.0, y: drawingRect.height - 11.0 - 22.0 + UIScreenPixel, width: 13.0, height: 22.0)) } - c.setFillColor(theme.chat.inputPanel.panelBackgroundColor.cgColor) - c.fill(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: drawingRect.width, height: 42.0))) - c.setFillColor(theme.chat.inputPanel.panelSeparatorColor.cgColor) - c.fill(CGRect(origin: CGPoint(x: 1.0, y: 42.0), size: CGSize(width: drawingRect.width - 2.0, height: 1.0))) + if case let .color(color) = theme.chat.defaultWallpaper, UIColor(rgb: color).isEqual(theme.chat.inputPanel.panelBackgroundColorNoWallpaper) { + c.setFillColor(theme.chat.inputPanel.panelBackgroundColorNoWallpaper.cgColor) + c.fill(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: drawingRect.width, height: 42.0))) + } else { + c.setFillColor(theme.chat.inputPanel.panelBackgroundColor.cgColor) + c.fill(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: drawingRect.width, height: 42.0))) + + c.setFillColor(theme.chat.inputPanel.panelSeparatorColor.cgColor) + c.fill(CGRect(origin: CGPoint(x: 1.0, y: 42.0), size: CGSize(width: drawingRect.width - 2.0, height: UIScreenPixel))) + } c.setFillColor(theme.chat.inputPanel.inputBackgroundColor.cgColor) c.setStrokeColor(theme.chat.inputPanel.inputStrokeColor.cgColor) @@ -705,117 +816,159 @@ public func drawThemeImage(context c: CGContext, theme: PresentationTheme, wallp c.draw(image, in: CGRect(origin: CGPoint(x: drawingRect.width - 3.0 - 29.0, y: 7.0 + UIScreenPixel), size: microphone.size.fitted(CGSize(width: 30.0, height: 30.0)))) } + let incoming = theme.chat.message.incoming.bubble.withoutWallpaper + let outgoing = theme.chat.message.outgoing.bubble.withoutWallpaper + c.saveGState() - c.setFillColor(theme.chat.message.incoming.bubble.withoutWallpaper.fill.cgColor) - c.setStrokeColor(theme.chat.message.incoming.bubble.withoutWallpaper.stroke.cgColor) + c.translateBy(x: 5.0, y: 65.0) c.translateBy(x: 114.0, y: 32.0) c.scaleBy(x: 1.0, y: -1.0) c.translateBy(x: -114.0, y: -32.0) + let _ = try? drawSvgPath(c, path: "M98.0061174,0 C106.734138,0 113.82927,6.99200411 113.996965,15.6850616 L114,16 C114,24.836556 106.830179,32 98.0061174,32 L21.9938826,32 C18.2292665,32 14.7684355,30.699197 12.0362474,28.5221601 C8.56516444,32.1765452 -1.77635684e-15,31.9985981 -1.77635684e-15,31.9985981 C5.69252399,28.6991366 5.98604874,24.4421608 5.99940747,24.1573436 L6,24.1422468 L6,16 C6,7.163444 13.1698213,0 21.9938826,0 L98.0061174,0 ") - c.strokePath() - let _ = try? drawSvgPath(c, path: "M98.0061174,0 C106.734138,0 113.82927,6.99200411 113.996965,15.6850616 L114,16 C114,24.836556 106.830179,32 98.0061174,32 L21.9938826,32 C18.2292665,32 14.7684355,30.699197 12.0362474,28.5221601 C8.56516444,32.1765452 -1.77635684e-15,31.9985981 -1.77635684e-15,31.9985981 C5.69252399,28.6991366 5.98604874,24.4421608 5.99940747,24.1573436 L6,24.1422468 L6,16 C6,7.163444 13.1698213,0 21.9938826,0 L98.0061174,0 ") - c.fillPath() + if incoming.fill.rgb != incoming.gradientFill.rgb { + let gradientColors = [incoming.fill, incoming.gradientFill].map { $0.cgColor } as CFArray + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 32.0), options: CGGradientDrawingOptions()) + } else { + c.setFillColor(incoming.fill.cgColor) + c.setStrokeColor(incoming.stroke.cgColor) + + c.strokePath() + let _ = try? drawSvgPath(c, path: "M98.0061174,0 C106.734138,0 113.82927,6.99200411 113.996965,15.6850616 L114,16 C114,24.836556 106.830179,32 98.0061174,32 L21.9938826,32 C18.2292665,32 14.7684355,30.699197 12.0362474,28.5221601 C8.56516444,32.1765452 -1.77635684e-15,31.9985981 -1.77635684e-15,31.9985981 C5.69252399,28.6991366 5.98604874,24.4421608 5.99940747,24.1573436 L6,24.1422468 L6,16 C6,7.163444 13.1698213,0 21.9938826,0 L98.0061174,0 ") + c.fillPath() + } c.restoreGState() c.saveGState() - c.setFillColor(theme.chat.message.outgoing.bubble.withoutWallpaper.fill.cgColor) - c.setStrokeColor(theme.chat.message.outgoing.bubble.withoutWallpaper.stroke.cgColor) + c.translateBy(x: drawingRect.width - 114.0 - 5.0, y: 25.0) c.translateBy(x: 114.0, y: 32.0) c.scaleBy(x: -1.0, y: -1.0) c.translateBy(x: 0, y: -32.0) + let _ = try? drawSvgPath(c, path: "M98.0061174,0 C106.734138,0 113.82927,6.99200411 113.996965,15.6850616 L114,16 C114,24.836556 106.830179,32 98.0061174,32 L21.9938826,32 C18.2292665,32 14.7684355,30.699197 12.0362474,28.5221601 C8.56516444,32.1765452 -1.77635684e-15,31.9985981 -1.77635684e-15,31.9985981 C5.69252399,28.6991366 5.98604874,24.4421608 5.99940747,24.1573436 L6,24.1422468 L6,16 C6,7.163444 13.1698213,0 21.9938826,0 L98.0061174,0 ") - c.strokePath() - let _ = try? drawSvgPath(c, path: "M98.0061174,0 C106.734138,0 113.82927,6.99200411 113.996965,15.6850616 L114,16 C114,24.836556 106.830179,32 98.0061174,32 L21.9938826,32 C18.2292665,32 14.7684355,30.699197 12.0362474,28.5221601 C8.56516444,32.1765452 -1.77635684e-15,31.9985981 -1.77635684e-15,31.9985981 C5.69252399,28.6991366 5.98604874,24.4421608 5.99940747,24.1573436 L6,24.1422468 L6,16 C6,7.163444 13.1698213,0 21.9938826,0 L98.0061174,0 ") - c.fillPath() + if outgoing.fill.rgb != outgoing.gradientFill.rgb { + c.clip() + + let gradientColors = [outgoing.fill, outgoing.gradientFill].map { $0.cgColor } as CFArray + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 32.0), options: CGGradientDrawingOptions()) + } else { + c.setFillColor(outgoing.fill.cgColor) + c.setStrokeColor(outgoing.stroke.cgColor) + + c.strokePath() + let _ = try? drawSvgPath(c, path: "M98.0061174,0 C106.734138,0 113.82927,6.99200411 113.996965,15.6850616 L114,16 C114,24.836556 106.830179,32 98.0061174,32 L21.9938826,32 C18.2292665,32 14.7684355,30.699197 12.0362474,28.5221601 C8.56516444,32.1765452 -1.77635684e-15,31.9985981 -1.77635684e-15,31.9985981 C5.69252399,28.6991366 5.98604874,24.4421608 5.99940747,24.1573436 L6,24.1422468 L6,16 C6,7.163444 13.1698213,0 21.9938826,0 L98.0061174,0 ") + c.fillPath() + } + c.restoreGState() } -public func themeImage(account: Account, accountManager: AccountManager, fileReference: FileMediaReference, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let isSupportedTheme = fileReference.media.mimeType == "application/x-tgtheme-ios" - let maybeFetched = accountManager.mediaBox.resourceData(fileReference.media.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) - let data = maybeFetched - |> take(1) - |> mapToSignal { maybeData -> Signal<(Data?, Data?), NoError> in - if maybeData.complete && isSupportedTheme { - let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) - return .single((loadedData, nil)) - } else { - let decodedThumbnailData = fileReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) - - let previewRepresentation = fileReference.media.previewRepresentations.first - let fetchedThumbnail: Signal - if let previewRepresentation = previewRepresentation { - fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(previewRepresentation.resource)) - } else { - fetchedThumbnail = .complete() - } - - let thumbnailData: Signal - if let previewRepresentation = previewRepresentation { - thumbnailData = Signal { subscriber in - let fetchedDisposable = fetchedThumbnail.start() - let thumbnailDisposable = account.postbox.mediaBox.resourceData(previewRepresentation.resource).start(next: { next in - let data = next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []) - if let data = data, data.count > 0 { - subscriber.putNext(data) - } else { - subscriber.putNext(decodedThumbnailData) - } - }, error: subscriber.putError, completed: subscriber.putCompletion) +public enum ThemeImageSource { + case file(FileMediaReference) + case settings(TelegramThemeSettings) +} + +public func themeImage(account: Account, accountManager: AccountManager, source: ThemeImageSource, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let theme: Signal<(PresentationTheme?, Data?), NoError> + + switch source { + case let .file(fileReference): + let isSupportedTheme = fileReference.media.mimeType == "application/x-tgtheme-ios" + let maybeFetched = accountManager.mediaBox.resourceData(fileReference.media.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) + theme = maybeFetched + |> take(1) + |> mapToSignal { maybeData -> Signal<(PresentationTheme?, Data?), NoError> in + if maybeData.complete && isSupportedTheme { + let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) + return .single((loadedData.flatMap { makePresentationTheme(data: $0) }, nil)) + } else { + let decodedThumbnailData = fileReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) - return ActionDisposable { - fetchedDisposable.dispose() - thumbnailDisposable.dispose() + let previewRepresentation = fileReference.media.previewRepresentations.first + let fetchedThumbnail: Signal + if let previewRepresentation = previewRepresentation { + fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(previewRepresentation.resource)) + } else { + fetchedThumbnail = .complete() } - } - } else { - thumbnailData = .single(decodedThumbnailData) - } - - let reference = fileReference.resourceReference(fileReference.media.resource) - let fullSizeData: Signal - if isSupportedTheme { - fullSizeData = Signal { subscriber in - let fetch = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: reference).start() - let disposable = (account.postbox.mediaBox.resourceData(reference.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: false) - |> map { data -> Data? in - return data.complete ? try? Data(contentsOf: URL(fileURLWithPath: data.path)) : nil - }).start(next: { next in - if let data = next { - accountManager.mediaBox.storeResourceData(reference.resource.id, data: data) + + let thumbnailData: Signal + if let previewRepresentation = previewRepresentation { + thumbnailData = Signal { subscriber in + let fetchedDisposable = fetchedThumbnail.start() + let thumbnailDisposable = account.postbox.mediaBox.resourceData(previewRepresentation.resource).start(next: { next in + let data = next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []) + if let data = data, data.count > 0 { + subscriber.putNext(data) + } else { + subscriber.putNext(decodedThumbnailData) + } + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedDisposable.dispose() + thumbnailDisposable.dispose() } - subscriber.putNext(next) - }, error: { error in - subscriber.putError(error) - }, completed: { - subscriber.putCompletion() - }) - return ActionDisposable { - fetch.dispose() - disposable.dispose() + } + } else { + thumbnailData = .single(decodedThumbnailData) + } + + let reference = fileReference.resourceReference(fileReference.media.resource) + let fullSizeData: Signal + if isSupportedTheme { + fullSizeData = Signal { subscriber in + let fetch = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: reference).start() + let disposable = (account.postbox.mediaBox.resourceData(reference.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: false) + |> map { data -> Data? in + return data.complete ? try? Data(contentsOf: URL(fileURLWithPath: data.path)) : nil + }).start(next: { next in + if let data = next { + accountManager.mediaBox.storeResourceData(reference.resource.id, data: data) + } + subscriber.putNext(next) + }, error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + }) + return ActionDisposable { + fetch.dispose() + disposable.dispose() + } + } + } else { + fullSizeData = .single(nil) + } + + return thumbnailData |> mapToSignal { thumbnailData in + return fullSizeData |> map { fullSizeData in + return (fullSizeData.flatMap { makePresentationTheme(data: $0) }, thumbnailData) + } } } - } else { - fullSizeData = .single(nil) } - - return thumbnailData |> mapToSignal { thumbnailData in - return fullSizeData |> map { fullSizeData in - return (fullSizeData, thumbnailData) - } - } - } + case let .settings(settings): + theme = .single((makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: .builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)), accentColor: UIColor(argb: settings.accentColor), backgroundColors: nil, bubbleColors: settings.messageColors.flatMap { (UIColor(argb: $0.top), UIColor(argb: $0.bottom)) }, wallpaper: settings.wallpaper, serviceBackgroundColor: nil, preview: false), nil)) } - |> mapToSignal { (fullSizeData, thumbnailData) -> Signal<(PresentationTheme?, UIImage?, Data?), NoError> in - if let fullSizeData = fullSizeData, let theme = makePresentationTheme(data: fullSizeData) { - if case let .file(file) = theme.chat.defaultWallpaper, file.id == 0 { + + let data = theme + |> mapToSignal { (theme, thumbnailData) -> Signal<(PresentationTheme?, UIImage?, Data?), NoError> in + if let theme = theme { + if case let .file(file) = theme.chat.defaultWallpaper { return cachedWallpaper(account: account, slug: file.slug, settings: file.settings) |> mapToSignal { wallpaper -> Signal<(PresentationTheme?, UIImage?, Data?), NoError> in if let wallpaper = wallpaper, case let .file(file) = wallpaper.wallpaper { var convertedRepresentations: [ImageRepresentationWithReference] = [] - convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource), reference: .media(media: .standalone(media: file.file), resource: file.file.resource))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) return wallpaperDatas(account: account, accountManager: accountManager, fileReference: .standalone(media: file.file), representations: convertedRepresentations, alwaysShowThumbnailFirst: false, thumbnail: false, onlyFullSize: true, autoFetchFullSize: true, synchronousLoad: false) |> mapToSignal { _, fullSizeData, complete -> Signal<(PresentationTheme?, UIImage?, Data?), NoError> in guard complete, let fullSizeData = fullSizeData else { @@ -824,8 +977,8 @@ public func themeImage(account: Account, accountManager: AccountManager, fileRef accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData) let _ = accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start() - if file.isPattern, let color = file.settings.color, let intensity = file.settings.intensity { - return accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPatternWallpaperRepresentation(color: color, intensity: intensity), complete: true, fetch: true) + if wallpaper.wallpaper.isPattern, let color = file.settings.color, let intensity = file.settings.intensity { + return accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPatternWallpaperRepresentation(color: color, bottomColor: file.settings.bottomColor, intensity: intensity, rotation: file.settings.rotation), complete: true, fetch: true) |> mapToSignal { data in if data.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)), let image = UIImage(data: data) { return .single((theme, image, thumbnailData)) @@ -927,142 +1080,259 @@ public func themeImage(account: Account, accountManager: AccountManager, fileRef } } -public func themeIconImage(account: Account, accountManager: AccountManager, theme: PresentationThemeReference, accentColor: UIColor?) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal: Signal<(UIColor, UIColor, UIColor, UIImage?), NoError> +public func themeIconImage(account: Account, accountManager: AccountManager, theme: PresentationThemeReference, color: PresentationThemeAccentColor?, wallpaper: TelegramWallpaper? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let colorsSignal: Signal<((UIColor, UIColor?), (UIColor, UIColor), (UIColor, UIColor), UIImage?, Int32?), NoError> if case let .builtin(theme) = theme { + let incomingColor: UIColor + let outgoingColor: (UIColor, UIColor) + var accentColor = color?.color + var bubbleColors = color?.plainBubbleColors + var topBackgroundColor: UIColor + var bottomBackgroundColor: UIColor? switch theme { - case .dayClassic: - signal = .single((UIColor(rgb: 0xd6e2ee), UIColor(rgb: 0xffffff), UIColor(rgb: 0xe1ffc7), nil)) - case .day: - signal = .single((.white, UIColor(rgb: 0xd5dde6), accentColor ?? UIColor(rgb: 0x007aff), nil)) - case .night: - signal = .single((.black, UIColor(rgb: 0x1f1f1f), accentColor ?? UIColor(rgb: 0x313131), nil)) - case .nightAccent: - let accentColor = accentColor ?? UIColor(rgb: 0x007aff) - signal = .single((accentColor.withMultiplied(hue: 1.024, saturation: 0.573, brightness: 0.18), accentColor.withMultiplied(hue: 1.024, saturation: 0.585, brightness: 0.25), accentColor.withMultiplied(hue: 1.019, saturation: 0.731, brightness: 0.59), nil)) + case .dayClassic: + incomingColor = UIColor(rgb: 0xffffff) + if let accentColor = accentColor { + if let wallpaper = wallpaper, case let .file(file) = wallpaper { + topBackgroundColor = file.settings.color.flatMap { UIColor(rgb: $0) } ?? UIColor(rgb: 0xd6e2ee) + bottomBackgroundColor = file.settings.bottomColor.flatMap { UIColor(rgb: $0) } + } else { + if let bubbleColors = bubbleColors { + topBackgroundColor = UIColor(rgb: 0xd6e2ee) + } else { + topBackgroundColor = accentColor.withMultiplied(hue: 1.019, saturation: 0.867, brightness: 0.965) + } + } + if let bubbleColors = bubbleColors { + topBackgroundColor = UIColor(rgb: 0xd6e2ee) + outgoingColor = bubbleColors + } else { + topBackgroundColor = accentColor.withMultiplied(hue: 1.019, saturation: 0.867, brightness: 0.965) + let hsb = accentColor.hsb + let bubbleColor = UIColor(hue: hsb.0, saturation: hsb.2 > 0.0 ? 0.14 : 0.0, brightness: 0.79 + hsb.2 * 0.21, alpha: 1.0) + outgoingColor = (bubbleColor, bubbleColor) + } + } else { + topBackgroundColor = UIColor(rgb: 0xd6e2ee) + outgoingColor = (UIColor(rgb: 0xe1ffc7), UIColor(rgb: 0xe1ffc7)) + } + case .day: + topBackgroundColor = UIColor(rgb: 0xffffff) + incomingColor = UIColor(rgb: 0xd5dde6) + if accentColor == nil { + accentColor = UIColor(rgb: 0x007aff) + } + outgoingColor = bubbleColors ?? (accentColor!, accentColor!) + case .night: + topBackgroundColor = UIColor(rgb: 0x000000) + incomingColor = UIColor(rgb: 0x1f1f1f) + if accentColor == nil || accentColor?.rgb == 0xffffff { + accentColor = UIColor(rgb: 0x313131) + } + outgoingColor = bubbleColors ?? (accentColor!, accentColor!) + case .nightAccent: + let accentColor = accentColor ?? UIColor(rgb: 0x007aff) + topBackgroundColor = accentColor.withMultiplied(hue: 1.024, saturation: 0.573, brightness: 0.18) + incomingColor = accentColor.withMultiplied(hue: 1.024, saturation: 0.585, brightness: 0.25) + let accentBubbleColor = accentColor.withMultiplied(hue: 1.019, saturation: 0.731, brightness: 0.59) + outgoingColor = bubbleColors ?? (accentBubbleColor, accentBubbleColor) } + + var rotation: Int32? + if let wallpaper = wallpaper { + switch wallpaper { + case let .color(color): + topBackgroundColor = UIColor(rgb: color) + case let .gradient(topColor, bottomColor, settings): + topBackgroundColor = UIColor(rgb: topColor) + bottomBackgroundColor = UIColor(rgb: bottomColor) + rotation = settings.rotation + case let .file(file): + if let color = file.settings.color { + topBackgroundColor = UIColor(rgb: color) + bottomBackgroundColor = file.settings.bottomColor.flatMap { UIColor(rgb: $0) } + } + rotation = file.settings.rotation + default: + topBackgroundColor = UIColor(rgb: 0xd6e2ee) + } + } + + colorsSignal = .single(((topBackgroundColor, bottomBackgroundColor), (incomingColor, incomingColor), outgoingColor, nil, rotation)) } else { var resource: MediaResource? + var reference: MediaResourceReference? + var defaultWallpaper: TelegramWallpaper? if case let .local(theme) = theme { - resource = theme.resource - } else if case let .cloud(theme) = theme { - resource = theme.theme.file?.resource + reference = .standalone(resource: theme.resource) + } else if case let .cloud(theme) = theme, let resource = theme.theme.file?.resource { + reference = .theme(theme: .slug(theme.theme.slug), resource: resource) } - if let resource = resource { - signal = telegramThemeData(account: account, accountManager: accountManager, resource: resource, synchronousLoad: false) - |> mapToSignal { data -> Signal<(UIColor, UIColor, UIColor, UIImage?), NoError> in + + let themeSignal: Signal + if case let .cloud(theme) = theme, let settings = theme.theme.settings { + themeSignal = Signal { subscriber in + let theme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: .builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)), accentColor: UIColor(argb: settings.accentColor), backgroundColors: nil, bubbleColors: settings.messageColors.flatMap { (UIColor(argb: $0.top), UIColor(argb: $0.bottom)) }, wallpaper: settings.wallpaper, serviceBackgroundColor: nil, preview: false) + subscriber.putNext(theme) + subscriber.putCompletion() + + return EmptyDisposable + } + } else if let reference = reference { + themeSignal = telegramThemeData(account: account, accountManager: accountManager, reference: reference, synchronousLoad: false) + |> map { data -> PresentationTheme? in if let data = data, let theme = makePresentationTheme(data: data) { - var wallpaperSignal: Signal<(UIColor, UIColor, UIColor, UIImage?), NoError> = .complete() - let backgroundColor: UIColor - let incomingColor = theme.chat.message.incoming.bubble.withoutWallpaper.fill - let outgoingColor = theme.chat.message.outgoing.bubble.withoutWallpaper.fill - switch theme.chat.defaultWallpaper { - case .builtin: - backgroundColor = UIColor(rgb: 0xd6e2ee) - case let .color(color): - backgroundColor = UIColor(rgb: UInt32(bitPattern: color)) - case .image: - backgroundColor = .black - case let .file(file): - if file.isPattern, let color = file.settings.color { - backgroundColor = UIColor(rgb: UInt32(bitPattern: color)) - } else { - backgroundColor = theme.chatList.backgroundColor - } - wallpaperSignal = cachedWallpaper(account: account, slug: file.slug, settings: file.settings) - |> mapToSignal { wallpaper in - if let wallpaper = wallpaper, case let .file(file) = wallpaper.wallpaper { - var convertedRepresentations: [ImageRepresentationWithReference] = [] - convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource), reference: .media(media: .standalone(media: file.file), resource: file.file.resource))) - return wallpaperDatas(account: account, accountManager: accountManager, fileReference: .standalone(media: file.file), representations: convertedRepresentations, alwaysShowThumbnailFirst: false, thumbnail: false, onlyFullSize: true, autoFetchFullSize: true, synchronousLoad: false) - |> mapToSignal { _, fullSizeData, complete -> Signal<(UIColor, UIColor, UIColor, UIImage?), NoError> in - guard complete, let fullSizeData = fullSizeData else { - return .complete() - } - accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData) - let _ = accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start() - - if file.isPattern, let color = file.settings.color, let intensity = file.settings.intensity { - return accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPatternWallpaperRepresentation(color: color, intensity: intensity), complete: true, fetch: true) - |> mapToSignal { _ in - return .complete() - } - } else if file.settings.blur { - return accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true) - |> mapToSignal { _ in - if let image = UIImage(data: fullSizeData) { - return .single((backgroundColor, incomingColor, outgoingColor, image)) - } else { - return .complete() - } - } - } else if let image = UIImage(data: fullSizeData) { - return .single((backgroundColor, incomingColor, outgoingColor, image)) - } else { - return .complete() - } - } - } else { - return .complete() - } - } - } - return .single((backgroundColor, incomingColor, outgoingColor, nil)) - |> then(wallpaperSignal) + return theme } else { - return .complete() + return nil } } } else { - signal = .never() + themeSignal = .never() + } + + colorsSignal = themeSignal + |> mapToSignal { theme -> Signal<((UIColor, UIColor?), (UIColor, UIColor), (UIColor, UIColor), UIImage?, Int32?), NoError> in + if let theme = theme { + var wallpaperSignal: Signal<((UIColor, UIColor?), (UIColor, UIColor), (UIColor, UIColor), UIImage?, Int32?), NoError> = .complete() + var rotation: Int32? + var backgroundColor: (UIColor, UIColor?) + let incomingColor = (theme.chat.message.incoming.bubble.withoutWallpaper.fill, theme.chat.message.incoming.bubble.withoutWallpaper.gradientFill) + let outgoingColor = (theme.chat.message.outgoing.bubble.withoutWallpaper.fill, theme.chat.message.outgoing.bubble.withoutWallpaper.gradientFill) + switch theme.chat.defaultWallpaper { + case .builtin: + backgroundColor = (UIColor(rgb: 0xd6e2ee), nil) + case let .color(color): + backgroundColor = (UIColor(rgb: color), nil) + case let .gradient(topColor, bottomColor, settings): + backgroundColor = (UIColor(rgb: topColor), UIColor(rgb: bottomColor)) + rotation = settings.rotation + case .image: + backgroundColor = (.black, nil) + case let .file(file): + rotation = file.settings.rotation + if let color = file.settings.color { + backgroundColor = (UIColor(rgb: color), file.settings.bottomColor.flatMap { UIColor(rgb: $0) }) + } else { + backgroundColor = (theme.chatList.backgroundColor, nil) + } + wallpaperSignal = cachedWallpaper(account: account, slug: file.slug, settings: file.settings) + |> mapToSignal { wallpaper in + if let wallpaper = wallpaper, case let .file(file) = wallpaper.wallpaper { + var effectiveBackgroundColor = backgroundColor + if let color = file.settings.color { + effectiveBackgroundColor = (UIColor(rgb: color), file.settings.bottomColor.flatMap { UIColor(rgb: $0) }) + } + + var convertedRepresentations: [ImageRepresentationWithReference] = [] + convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) + return wallpaperDatas(account: account, accountManager: accountManager, fileReference: .standalone(media: file.file), representations: convertedRepresentations, alwaysShowThumbnailFirst: false, thumbnail: false, onlyFullSize: true, autoFetchFullSize: true, synchronousLoad: false) + |> mapToSignal { _, fullSizeData, complete -> Signal<((UIColor, UIColor?), (UIColor, UIColor), (UIColor, UIColor), UIImage?, Int32?), NoError> in + guard complete, let fullSizeData = fullSizeData else { + return .complete() + } + accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData) + let _ = accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start() + + if wallpaper.wallpaper.isPattern { + if let color = file.settings.color, let intensity = file.settings.intensity { + return accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPatternWallpaperRepresentation(color: color, bottomColor: file.settings.bottomColor, intensity: intensity, rotation: file.settings.rotation), complete: true, fetch: true) + |> mapToSignal { _ in + return .single((effectiveBackgroundColor, incomingColor, outgoingColor, nil, rotation)) + } + } else { + return .complete() + } + } else if file.settings.blur { + return accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true) + |> mapToSignal { _ in + if let image = UIImage(data: fullSizeData) { + return .single((backgroundColor, incomingColor, outgoingColor, image, rotation)) + } else { + return .complete() + } + } + } else if let image = UIImage(data: fullSizeData) { + return .single((backgroundColor, incomingColor, outgoingColor, image, rotation)) + } else { + return .complete() + } + } + } else { + return .complete() + } + } + } + return .single((backgroundColor, incomingColor, outgoingColor, nil, rotation)) + |> then(wallpaperSignal) + } else { + return .complete() + } } } - return signal - |> map { colors in - return { arguments in - let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: arguments.emptyColor == nil) - let drawingRect = arguments.drawingRect - - context.withContext { c in - c.setFillColor(colors.0.cgColor) - c.fill(drawingRect) - - if let image = colors.3 { - let initialThumbnailContextFittingSize = arguments.imageSize.fitted(CGSize(width: 90.0, height: 90.0)) - let thumbnailContextSize = image.size.aspectFilled(initialThumbnailContextFittingSize) - let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) - thumbnailContext.withFlippedContext { c in - c.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) - } - telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) - - if let blurredThumbnailImage = thumbnailContext.generateImage(), let cgImage = blurredThumbnailImage.cgImage { - let fittedSize = thumbnailContext.size.aspectFilled(CGSize(width: drawingRect.size.width + 1.0, height: drawingRect.size.height + 1.0)) - c.saveGState() - c.translateBy(x: drawingRect.width / 2.0, y: drawingRect.height / 2.0) - c.scaleBy(x: 1.0, y: -1.0) - c.translateBy(x: -drawingRect.width / 2.0, y: -drawingRect.height / 2.0) - c.draw(cgImage, in: CGRect(origin: CGPoint(x: (drawingRect.size.width - fittedSize.width) / 2.0, y: (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize)) - c.restoreGState() - } + return colorsSignal + |> map { colors in + return { arguments in + let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: arguments.emptyColor == nil) + let drawingRect = arguments.drawingRect + + context.withContext { c in + if let secondBackgroundColor = colors.0.1 { + let gradientColors = [colors.0.0, secondBackgroundColor].map { $0.cgColor } as CFArray + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + c.saveGState() + if let rotation = colors.4 { + c.translateBy(x: drawingRect.width / 2.0, y: drawingRect.height / 2.0) + c.rotate(by: CGFloat(rotation) * CGFloat.pi / 180.0) + c.translateBy(x: -drawingRect.width / 2.0, y: -drawingRect.height / 2.0) } - - let incoming = generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeBubble"), color: colors.1) - let outgoing = generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeBubble"), color: colors.2) - - c.translateBy(x: drawingRect.width / 2.0, y: drawingRect.height / 2.0) - c.scaleBy(x: 1.0, y: -1.0) - c.translateBy(x: -drawingRect.width / 2.0, y: -drawingRect.height / 2.0) - - c.draw(incoming!.cgImage!, in: CGRect(x: 9.0, y: 34.0, width: 57.0, height: 16.0)) - - c.translateBy(x: drawingRect.width / 2.0, y: drawingRect.height / 2.0) - c.scaleBy(x: -1.0, y: 1.0) - c.translateBy(x: -drawingRect.width / 2.0, y: -drawingRect.height / 2.0) - c.draw(outgoing!.cgImage!, in: CGRect(x: 9.0, y: 12.0, width: 57.0, height: 16.0)) + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: drawingRect.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + c.restoreGState() + } else { + c.setFillColor(colors.0.0.cgColor) + c.fill(drawingRect) } - return context + if let image = colors.3 { + let initialThumbnailContextFittingSize = arguments.imageSize.fitted(CGSize(width: 90.0, height: 90.0)) + let thumbnailContextSize = image.size.aspectFilled(initialThumbnailContextFittingSize) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withFlippedContext { c in + c.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + if let blurredThumbnailImage = thumbnailContext.generateImage(), let cgImage = blurredThumbnailImage.cgImage { + let fittedSize = thumbnailContext.size.aspectFilled(CGSize(width: drawingRect.size.width + 1.0, height: drawingRect.size.height + 1.0)) + c.saveGState() + c.translateBy(x: drawingRect.width / 2.0, y: drawingRect.height / 2.0) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -drawingRect.width / 2.0, y: -drawingRect.height / 2.0) + c.draw(cgImage, in: CGRect(origin: CGPoint(x: (drawingRect.size.width - fittedSize.width) / 2.0, y: (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize)) + c.restoreGState() + } + } + + let incomingColors = [colors.1.0, colors.1.1] + let incoming = generateGradientTintedImage(image: UIImage(bundleImageName: "Settings/ThemeBubble"), colors: incomingColors) + + let outgoingColors = [colors.2.0, colors.2.1] + let outgoing = generateGradientTintedImage(image: UIImage(bundleImageName: "Settings/ThemeBubble"), colors: outgoingColors) + + c.translateBy(x: drawingRect.width / 2.0, y: drawingRect.height / 2.0) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -drawingRect.width / 2.0, y: -drawingRect.height / 2.0) + c.draw(incoming!.cgImage!, in: CGRect(x: 9.0, y: 34.0, width: 57.0, height: 16.0)) + + c.translateBy(x: drawingRect.width / 2.0, y: drawingRect.height / 2.0) + c.scaleBy(x: -1.0, y: 1.0) + c.translateBy(x: -drawingRect.width / 2.0, y: -drawingRect.height / 2.0) + c.draw(outgoing!.cgImage!, in: CGRect(x: 9.0, y: 12.0, width: 57.0, height: 16.0)) } + addCorners(context, arguments: arguments) + return context + } } } diff --git a/submodules/WatchBridge/Sources/WatchBridge.swift b/submodules/WatchBridge/Sources/WatchBridge.swift index b074778ab7..b5cfd95ff5 100644 --- a/submodules/WatchBridge/Sources/WatchBridge.swift +++ b/submodules/WatchBridge/Sources/WatchBridge.swift @@ -319,7 +319,7 @@ func makeBridgeMedia(message: Message, strings: PresentationStrings, chatPeer: P } func makeBridgeChat(_ entry: ChatListEntry, strings: PresentationStrings) -> (TGBridgeChat, [Int64 : TGBridgeUser])? { - if case let .MessageEntry(index, message, readState, _, _, renderedPeer, _, _) = entry { + if case let .MessageEntry(index, message, readState, _, _, renderedPeer, _, _, hasFailed) = entry { guard index.messageIndex.id.peerId.namespace != Namespaces.Peer.SecretChat else { return nil } @@ -332,7 +332,7 @@ func makeBridgeChat(_ entry: ChatListEntry, strings: PresentationStrings) -> (TG bridgeChat.text = message.text bridgeChat.outgoing = !message.flags.contains(.Incoming) bridgeChat.deliveryState = makeBridgeDeliveryState(message) - bridgeChat.deliveryError = message.flags.contains(.Failed) + bridgeChat.deliveryError = hasFailed bridgeChat.media = makeBridgeMedia(message: message, strings: strings, filterUnsupportedActions: false) } bridgeChat.unread = readState?.isUnread ?? false diff --git a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift index 95f32f18e2..0609691bde 100644 --- a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift +++ b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift @@ -400,7 +400,7 @@ final class WatchMediaHandler: WatchRequestHandler { } } |> mapToSignal({ peer -> Signal in if let peer = peer, let representation = peer.smallProfileImage { - let imageData = peerAvatarImageData(account: context.account, peer: peer, authorOfMessage: nil, representation: representation, synchronousLoad: false) + let imageData = peerAvatarImageData(account: context.account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: representation, synchronousLoad: false) if let imageData = imageData { return imageData |> map { data -> UIImage? in diff --git a/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift b/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift index 0462646118..3a88437893 100644 --- a/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift +++ b/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift @@ -264,7 +264,7 @@ func legacyWebSearchItem(account: Account, result: ChatContextResult) -> LegacyW representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource)) } representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource)) - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) thumbnailSignal = chatMessagePhotoDatas(postbox: account.postbox, photoReference: .standalone(media: tmpImage), autoFetchFullSize: false) |> mapToSignal { value -> Signal in let thumbnailData = value._0 @@ -427,6 +427,8 @@ public func legacyEnqueueWebSearchMessages(_ selectionState: TGMediaSelectionCon } } - enqueueMediaMessages(signals) + if !signals.isEmpty { + enqueueMediaMessages(signals) + } } } diff --git a/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift b/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift index 98e38298cf..2865a2719a 100644 --- a/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift +++ b/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift @@ -149,7 +149,6 @@ class WebSearchControllerNode: ASDisplayNode { private let attributionNode: ASImageNode - private let recentQueriesPlaceholder: ImmediateTextNode private let recentQueriesNode: ListView private var enqueuedRecentTransitions: [(WebSearchRecentTransition, Bool)] = [] @@ -216,8 +215,6 @@ class WebSearchControllerNode: ASDisplayNode { self.recentQueriesNode = ListView() self.recentQueriesNode.backgroundColor = theme.list.plainBackgroundColor - self.recentQueriesPlaceholder = ImmediateTextNode() - super.init() self.setViewBlock({ @@ -284,7 +281,6 @@ class WebSearchControllerNode: ASDisplayNode { self?.dismissInput?() } - self.gridNode.visibleItemsUpdated = { [weak self] visibleItems in if let strongSelf = self, let bottom = visibleItems.bottom, let entries = strongSelf.currentEntries { if bottom.0 >= entries.count - 8 { @@ -448,30 +444,10 @@ class WebSearchControllerNode: ASDisplayNode { insets.bottom += toolbarHeight self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: insets, preloadSize: 400.0, type: gridNodeLayoutForContainerLayout(size: layout.size)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none,updateFirstIndexInSectionOffset: nil), completion: { _ in }) - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut, .custom: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) self.recentQueriesNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.recentQueriesNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.recentQueriesNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if !self.dequeuedInitialTransitionOnLayout { self.dequeuedInitialTransitionOnLayout = true @@ -708,7 +684,7 @@ class WebSearchControllerNode: ASDisplayNode { var entries: [WebSearchGalleryEntry] = [] var centralIndex: Int = 0 for i in 0 ..< results.count { - entries.append(WebSearchGalleryEntry(result: results[i])) + entries.append(WebSearchGalleryEntry(index: entries.count, result: results[i])) if results[i] == currentResult { centralIndex = i } @@ -735,7 +711,7 @@ class WebSearchControllerNode: ASDisplayNode { } } if let transitionNode = transitionNode { - return GalleryTransitionArguments(transitionNode: (transitionNode, { [weak transitionNode] in + return GalleryTransitionArguments(transitionNode: (transitionNode, transitionNode.bounds, { [weak transitionNode] in return (transitionNode?.transitionView().snapshotContentTree(unhide: true), nil) }), addToTransitionSurface: { view in if let strongSelf = self { diff --git a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift index ccf8804fdb..44d6053249 100644 --- a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift @@ -28,6 +28,7 @@ final class WebSearchGalleryControllerInteraction { } struct WebSearchGalleryEntry: Equatable { + let index: Int let result: ChatContextResult static func ==(lhs: WebSearchGalleryEntry, rhs: WebSearchGalleryEntry) -> Bool { @@ -39,11 +40,11 @@ struct WebSearchGalleryEntry: Equatable { case let .externalReference(_, _, type, _, _, _, content, thumbnail, _): if let content = content, type == "gif", let thumbnailResource = thumbnail?.resource, let dimensions = content.dimensions { let fileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource)], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])) - return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), fileReference: fileReference, loopVideo: true, enableSound: false, fetchAutomatically: true), controllerInteraction: controllerInteraction) + return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, index: self.index, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), fileReference: fileReference, loopVideo: true, enableSound: false, fetchAutomatically: true), controllerInteraction: controllerInteraction) } case let .internalReference(_, _, _, _, _, _, file, _): if let file = file { - return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), fileReference: .standalone(media: file), loopVideo: true, enableSound: false, fetchAutomatically: true), controllerInteraction: controllerInteraction) + return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, index: self.index, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), fileReference: .standalone(media: file), loopVideo: true, enableSound: false, fetchAutomatically: true), controllerInteraction: controllerInteraction) } } preconditionFailure() @@ -85,7 +86,7 @@ class WebSearchGalleryController: ViewController { private let centralItemTitle = Promise() private let centralItemTitleView = Promise() private let centralItemNavigationStyle = Promise() - private let centralItemFooterContentNode = Promise() + private let centralItemFooterContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>() private let centralItemAttributesDisposable = DisposableSet(); private let checkedDisposable = MetaDisposable() @@ -156,7 +157,7 @@ class WebSearchGalleryController: ViewController { self?.navigationItem.titleView = titleView })) - self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode in + self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode, _ in self?.galleryNode.updatePresentationState({ $0.withUpdatedFooterContentNode(footerContentNode) }, transition: .immediate) diff --git a/submodules/WebSearchUI/Sources/WebSearchItem.swift b/submodules/WebSearchUI/Sources/WebSearchItem.swift index 81cec09255..a7f6c72023 100644 --- a/submodules/WebSearchUI/Sources/WebSearchItem.swift +++ b/submodules/WebSearchUI/Sources/WebSearchItem.swift @@ -138,7 +138,7 @@ final class WebSearchItemNode: GridItemNode { representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource)) } if !representations.isEmpty { - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) updateImageSignal = mediaGridMessagePhoto(account: item.account, photoReference: .standalone(media: tmpImage)) } else { updateImageSignal = .complete() diff --git a/submodules/WebSearchUI/Sources/WebSearchNavigationContentNode.swift b/submodules/WebSearchUI/Sources/WebSearchNavigationContentNode.swift index b50c59dc6f..a50bd0b118 100644 --- a/submodules/WebSearchUI/Sources/WebSearchNavigationContentNode.swift +++ b/submodules/WebSearchUI/Sources/WebSearchNavigationContentNode.swift @@ -53,11 +53,11 @@ final class WebSearchNavigationContentNode: NavigationBarContentNode { } override var nominalHeight: CGFloat { - return 54.0 + return 56.0 } override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { - let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 54.0)) + let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 56.0)) self.searchBar.frame = searchBarFrame self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition) } diff --git a/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift b/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift index ee199f8778..788478b84f 100644 --- a/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift +++ b/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift @@ -14,15 +14,22 @@ import TelegramUniversalVideoContent import GalleryUI class WebSearchVideoGalleryItem: GalleryItem { + var id: AnyHashable { + return self.index + } + + let index: Int + let context: AccountContext let presentationData: PresentationData let result: ChatContextResult let content: UniversalVideoContent let controllerInteraction: WebSearchGalleryControllerInteraction? - init(context: AccountContext, presentationData: PresentationData, result: ChatContextResult, content: UniversalVideoContent, controllerInteraction: WebSearchGalleryControllerInteraction?) { + init(context: AccountContext, presentationData: PresentationData, index: Int, result: ChatContextResult, content: UniversalVideoContent, controllerInteraction: WebSearchGalleryControllerInteraction?) { self.context = context self.presentationData = presentationData + self.index = index self.result = result self.content = content self.controllerInteraction = controllerInteraction @@ -284,7 +291,7 @@ final class WebSearchVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - override func animateIn(from node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { + override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { guard let videoNode = self.videoNode else { return } @@ -307,8 +314,8 @@ final class WebSearchVideoGalleryItemNode: ZoomableContentGalleryItemNode { let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) let transformedCopyViewFinalFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) - let surfaceCopyView = node.1().0! - let copyView = node.1().0! + let surfaceCopyView = node.2().0! + let copyView = node.2().0! addToTransitionSurface(surfaceCopyView) @@ -361,7 +368,7 @@ final class WebSearchVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - override func animateOut(to node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { guard let videoNode = self.videoNode else { completion() return @@ -376,8 +383,8 @@ final class WebSearchVideoGalleryItemNode: ZoomableContentGalleryItemNode { var boundsCompleted = false var copyCompleted = false - let copyView = node.1().0! - let surfaceCopyView = node.1().0! + let copyView = node.2().0! + let surfaceCopyView = node.2().0! addToTransitionSurface(surfaceCopyView) @@ -534,7 +541,7 @@ final class WebSearchVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - override func footerContent() -> Signal { - return .single(self.footerContentNode) + override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { + return .single((self.footerContentNode, nil)) } } diff --git a/submodules/WidgetItems/Sources/WidgetItems.swift b/submodules/WidgetItems/Sources/WidgetItems.swift index 126c010468..0d6c10558c 100644 --- a/submodules/WidgetItems/Sources/WidgetItems.swift +++ b/submodules/WidgetItems/Sources/WidgetItems.swift @@ -7,12 +7,14 @@ public enum WidgetCodingError: Error { public struct WidgetDataPeer: Codable, Equatable { public var id: Int64 public var name: String + public var lastName: String? public var letters: [String] public var avatarPath: String? - public init(id: Int64, name: String, letters: [String], avatarPath: String?) { + public init(id: Int64, name: String, lastName: String?, letters: [String], avatarPath: String?) { self.id = id self.name = name + self.lastName = lastName self.letters = letters self.avatarPath = avatarPath } diff --git a/submodules/ffmpeg/BUCK b/submodules/ffmpeg/BUCK index aed1b5364a..19a4d37012 100644 --- a/submodules/ffmpeg/BUCK +++ b/submodules/ffmpeg/BUCK @@ -113,7 +113,9 @@ genrule( srcs = glob([ "FFMpeg/**/*", ], exclude = ["FFMpeg/**/.*"]), - bash = "$SRCDIR/FFMpeg/build-ffmpeg.sh release \"" + library_archs + "\" $OUT $SRCDIR/FFMpeg", + bash = + "PATH=\"$PATH:$(location //third-party/yasm:yasm)/yasm-1.3.0/build\" " + + "$SRCDIR/FFMpeg/build-ffmpeg.sh release \"" + library_archs + "\" $OUT $SRCDIR/FFMpeg", out = "libffmpeg", visibility = [ "//submodules/ffmpeg:FFMpeg", diff --git a/submodules/ffmpeg/gas-preprocessor.pl b/submodules/ffmpeg/gas-preprocessor.pl new file mode 100755 index 0000000000..6da37c1f0d --- /dev/null +++ b/submodules/ffmpeg/gas-preprocessor.pl @@ -0,0 +1,1210 @@ +#!/usr/bin/env perl +# by David Conrad +# This code is licensed under GPLv2 or later; go to gnu.org to read it +# (not that it much matters for an asm preprocessor) +# usage: set your assembler to be something like "perl gas-preprocessor.pl gcc" +use strict; + +# Apple's gas is ancient and doesn't support modern preprocessing features like +# .rept and has ugly macro syntax, among other things. Thus, this script +# implements the subset of the gas preprocessor used by x264 and ffmpeg +# that isn't supported by Apple's gas. + +my %canonical_arch = ("aarch64" => "aarch64", "arm64" => "aarch64", + "arm" => "arm", + "powerpc" => "powerpc", "ppc" => "powerpc"); + +my %comments = ("aarch64" => '//', + "arm" => '@', + "powerpc" => '#'); + +my @gcc_cmd; +my @preprocess_c_cmd; + +my $comm; +my $arch; +my $as_type = "apple-gas"; + +my $fix_unreq = $^O eq "darwin"; +my $force_thumb = 0; +my $verbose = 0; + +my $arm_cond_codes = "eq|ne|cs|cc|mi|pl|vs|vc|hi|ls|ge|lt|gt|le|al|hs|lo"; + +my $usage_str = " +$0\n +Gas-preprocessor.pl converts assembler files using modern GNU as syntax for +Apple's ancient gas version or clang's incompatible integrated assembler. The +conversion is regularly tested for Libav, x264 and vlc. Other projects might +use different features which are not correctly handled. + +Options for this program needs to be separated with ' -- ' from the assembler +command. Following options are currently supported: + + -help - this usage text + -arch - target architecture + -as-type - one value out of {{,apple-}{gas,clang},armasm} + -fix-unreq + -no-fix-unreq + -force-thumb - assemble as thumb regardless of the input source + (note, this is incomplete and only works for sources + it explicitly was tested with) + -verbose - print executed commands +"; + +sub usage() { + print $usage_str; +} + +while (@ARGV) { + my $opt = shift; + + if ($opt =~ /^-(no-)?fix-unreq$/) { + $fix_unreq = $1 ne "no-"; + } elsif ($opt eq "-force-thumb") { + $force_thumb = 1; + } elsif ($opt eq "-verbose") { + $verbose = 1; + } elsif ($opt eq "-arch") { + $arch = shift; + die "unknown arch: '$arch'\n" if not exists $canonical_arch{$arch}; + } elsif ($opt eq "-as-type") { + $as_type = shift; + die "unknown as type: '$as_type'\n" if $as_type !~ /^((apple-)?(gas|clang)|armasm)$/; + } elsif ($opt eq "-help") { + usage(); + exit 0; + } elsif ($opt eq "--" ) { + @gcc_cmd = @ARGV; + } elsif ($opt =~ /^-/) { + die "option '$opt' is not known. See '$0 -help' for usage information\n"; + } else { + push @gcc_cmd, $opt, @ARGV; + } + last if (@gcc_cmd); +} + +if (grep /\.c$/, @gcc_cmd) { + # C file (inline asm?) - compile + @preprocess_c_cmd = (@gcc_cmd, "-S"); +} elsif (grep /\.[sS]$/, @gcc_cmd) { + # asm file, just do C preprocessor + @preprocess_c_cmd = (@gcc_cmd, "-E"); +} elsif (grep /-(v|h|-version|dumpversion)/, @gcc_cmd) { + # pass -v/--version along, used during probing. Matching '-v' might have + # uninteded results but it doesn't matter much if gas-preprocessor or + # the compiler fails. + print STDERR join(" ", @gcc_cmd)."\n" if $verbose; + exec(@gcc_cmd); +} else { + die "Unrecognized input filetype"; +} +if ($as_type eq "armasm") { + + $preprocess_c_cmd[0] = "cpp"; + push(@preprocess_c_cmd, "-undef"); + # Normally a preprocessor for windows would predefine _WIN32, + # but we're using any generic system-agnostic preprocessor "cpp" + # with -undef (to avoid getting predefined variables from the host + # system in cross compilation cases), so manually define it here. + push(@preprocess_c_cmd, "-D_WIN32"); + + @preprocess_c_cmd = grep ! /^-nologo$/, @preprocess_c_cmd; + # Remove -ignore XX parameter pairs from preprocess_c_cmd + my $index = 1; + while ($index < $#preprocess_c_cmd) { + if ($preprocess_c_cmd[$index] eq "-ignore" and $index + 1 < $#preprocess_c_cmd) { + splice(@preprocess_c_cmd, $index, 2); + next; + } + $index++; + } + if (grep /^-MM$/, @preprocess_c_cmd) { + print STDERR join(" ", @preprocess_c_cmd)."\n" if $verbose; + system(@preprocess_c_cmd) == 0 or die "Error running preprocessor"; + exit 0; + } +} + +# if compiling, avoid creating an output file named '-.o' +if ((grep /^-c$/, @gcc_cmd) && !(grep /^-o/, @gcc_cmd)) { + foreach my $i (@gcc_cmd) { + if ($i =~ /\.[csS]$/) { + my $outputfile = $i; + $outputfile =~ s/\.[csS]$/.o/; + push(@gcc_cmd, "-o"); + push(@gcc_cmd, $outputfile); + last; + } + } +} +# replace only the '-o' argument with '-', avoids rewriting the make dependency +# target specified with -MT to '-' +my $index = 1; +while ($index < $#preprocess_c_cmd) { + if ($preprocess_c_cmd[$index] eq "-o") { + $index++; + $preprocess_c_cmd[$index] = "-"; + } + $index++; +} + +my $tempfile; +if ($as_type ne "armasm") { + @gcc_cmd = map { /\.[csS]$/ ? qw(-x assembler -) : $_ } @gcc_cmd; +} else { + @preprocess_c_cmd = grep ! /^-c$/, @preprocess_c_cmd; + @preprocess_c_cmd = grep ! /^-m/, @preprocess_c_cmd; + + @preprocess_c_cmd = grep ! /^-G/, @preprocess_c_cmd; + @preprocess_c_cmd = grep ! /^-W/, @preprocess_c_cmd; + @preprocess_c_cmd = grep ! /^-Z/, @preprocess_c_cmd; + @preprocess_c_cmd = grep ! /^-fp/, @preprocess_c_cmd; + @preprocess_c_cmd = grep ! /^-EHsc$/, @preprocess_c_cmd; + @preprocess_c_cmd = grep ! /^-O/, @preprocess_c_cmd; + + @gcc_cmd = grep ! /^-G/, @gcc_cmd; + @gcc_cmd = grep ! /^-W/, @gcc_cmd; + @gcc_cmd = grep ! /^-Z/, @gcc_cmd; + @gcc_cmd = grep ! /^-fp/, @gcc_cmd; + @gcc_cmd = grep ! /^-EHsc$/, @gcc_cmd; + @gcc_cmd = grep ! /^-O/, @gcc_cmd; + + my @outfiles = grep /\.(o|obj)$/, @gcc_cmd; + $tempfile = $outfiles[0].".asm"; + + # Remove most parameters from gcc_cmd, which actually is the armasm command, + # which doesn't support any of the common compiler/preprocessor options. + @gcc_cmd = grep ! /^-D/, @gcc_cmd; + @gcc_cmd = grep ! /^-U/, @gcc_cmd; + @gcc_cmd = grep ! /^-m/, @gcc_cmd; + @gcc_cmd = grep ! /^-M/, @gcc_cmd; + @gcc_cmd = grep ! /^-c$/, @gcc_cmd; + @gcc_cmd = grep ! /^-I/, @gcc_cmd; + @gcc_cmd = map { /\.S$/ ? $tempfile : $_ } @gcc_cmd; +} + +# detect architecture from gcc binary name +if (!$arch) { + if ($gcc_cmd[0] =~ /(arm64|aarch64|arm|powerpc|ppc)/) { + $arch = $1; + } else { + # look for -arch flag + foreach my $i (1 .. $#gcc_cmd-1) { + if ($gcc_cmd[$i] eq "-arch" and + $gcc_cmd[$i+1] =~ /(arm64|aarch64|arm|powerpc|ppc)/) { + $arch = $1; + } + } + } +} + +# assume we're not cross-compiling if no -arch or the binary doesn't have the arch name +$arch = qx/arch/ if (!$arch); + +die "Unknown target architecture '$arch'" if not exists $canonical_arch{$arch}; + +$arch = $canonical_arch{$arch}; +$comm = $comments{$arch}; +my $inputcomm = $comm; +$comm = ";" if $as_type =~ /armasm/; + +my %ppc_spr = (ctr => 9, + vrsave => 256); + +print STDERR join(" ", @preprocess_c_cmd)."\n" if $verbose; +open(INPUT, "-|", @preprocess_c_cmd) || die "Error running preprocessor"; + +if ($ENV{GASPP_DEBUG}) { + open(ASMFILE, ">&STDOUT"); +} else { + if ($as_type ne "armasm") { + print STDERR join(" ", @gcc_cmd)."\n" if $verbose; + open(ASMFILE, "|-", @gcc_cmd) or die "Error running assembler"; + } else { + open(ASMFILE, ">", $tempfile); + } +} + +my $current_macro = ''; +my $macro_level = 0; +my $rept_level = 0; +my %macro_lines; +my %macro_args; +my %macro_args_default; +my $macro_count = 0; +my $altmacro = 0; +my $in_irp = 0; + +my $num_repts; +my @rept_lines; + +my @irp_args; +my $irp_param; + +my @ifstack; + +my %symbols; + +my @sections; + +my %literal_labels; # for ldr , = +my $literal_num = 0; +my $literal_expr = ".word"; +$literal_expr = ".quad" if $arch eq "aarch64"; + +my $thumb = 0; + +my %thumb_labels; +my %call_targets; +my %import_symbols; + +my %neon_alias_reg; +my %neon_alias_type; + +my $temp_label_next = 0; +my %last_temp_labels; +my %next_temp_labels; + +my %labels_seen; + +my %aarch64_req_alias; + +if ($force_thumb) { + parse_line(".thumb\n"); +} + +# pass 1: parse .macro +# note that the handling of arguments is probably overly permissive vs. gas +# but it should be the same for valid cases +while () { + # remove lines starting with '#', preprocessing is done, '#' at start of + # the line indicates a comment for all supported archs (aarch64, arm, ppc + # and x86). Also strips line number comments but since they are off anyway + # it is no loss. + s/^\s*#.*$//; + # remove all comments (to avoid interfering with evaluating directives) + s/(? 0) { + $ifstack[-1] = -$ifstack[-1]; + } + return 1; + } elsif ($line =~ /\.else/) { + $ifstack[-1] = !$ifstack[-1]; + return 1; + } elsif (handle_if($line)) { + return 1; + } + } + + # discard lines in false .if blocks + foreach my $i (0 .. $#ifstack) { + if ($ifstack[$i] <= 0) { + return 1; + } + } + } + return 0; +} + +sub parse_line { + my $line = $_[0]; + + return if (parse_if_line($line)); + + if (scalar(@rept_lines) == 0) { + if ($line =~ /\.macro/) { + $macro_level++; + if ($macro_level > 1 && !$current_macro) { + die "nested macros but we don't have master macro"; + } + } elsif ($line =~ /\.endm/) { + $macro_level--; + if ($macro_level < 0) { + die "unmatched .endm"; + } elsif ($macro_level == 0) { + $current_macro = ''; + return; + } + } + } + + if ($macro_level == 0) { + if ($line =~ /\.(rept|irp)/) { + $rept_level++; + } elsif ($line =~ /.endr/) { + $rept_level--; + } + } + + if ($macro_level > 1) { + push(@{$macro_lines{$current_macro}}, $line); + } elsif (scalar(@rept_lines) and $rept_level >= 1) { + push(@rept_lines, $line); + } elsif ($macro_level == 0) { + expand_macros($line); + } else { + if ($line =~ /\.macro\s+([\d\w\.]+)\s*,?\s*(.*)/) { + $current_macro = $1; + + # commas in the argument list are optional, so only use whitespace as the separator + my $arglist = $2; + $arglist =~ s/,/ /g; + + my @args = split(/\s+/, $arglist); + foreach my $i (0 .. $#args) { + my @argpair = split(/=/, $args[$i]); + $macro_args{$current_macro}[$i] = $argpair[0]; + $argpair[0] =~ s/:vararg$//; + $macro_args_default{$current_macro}{$argpair[0]} = $argpair[1]; + } + # ensure %macro_lines has the macro name added as a key + $macro_lines{$current_macro} = []; + + } elsif ($current_macro) { + push(@{$macro_lines{$current_macro}}, $line); + } else { + die "macro level without a macro name"; + } + } +} + +sub handle_set { + my $line = $_[0]; + if ($line =~ /\.(?:set|equ)\s+(\S*)\s*,\s*(.*)/) { + $symbols{$1} = eval_expr($2); + return 1; + } + return 0; +} + +sub expand_macros { + my $line = $_[0]; + + # handle .if directives; apple's assembler doesn't support important non-basic ones + # evaluating them is also needed to handle recursive macros + if (handle_if($line)) { + return; + } + + if (/\.purgem\s+([\d\w\.]+)/) { + delete $macro_lines{$1}; + delete $macro_args{$1}; + delete $macro_args_default{$1}; + return; + } + + if ($line =~ /\.altmacro/) { + $altmacro = 1; + return; + } + + if ($line =~ /\.noaltmacro/) { + $altmacro = 0; + return; + } + + $line =~ s/\%([^,]*)/eval_expr($1)/eg if $altmacro; + + # Strip out the .set lines from the armasm output + return if (handle_set($line) and $as_type eq "armasm"); + + if ($line =~ /\.rept\s+(.*)/) { + $num_repts = $1; + @rept_lines = ("\n"); + + # handle the possibility of repeating another directive on the same line + # .endr on the same line is not valid, I don't know if a non-directive is + if ($num_repts =~ s/(\.\w+.*)//) { + push(@rept_lines, "$1\n"); + } + $num_repts = eval_expr($num_repts); + } elsif ($line =~ /\.irp\s+([\d\w\.]+)\s*(.*)/) { + $in_irp = 1; + $num_repts = 1; + @rept_lines = ("\n"); + $irp_param = $1; + + # only use whitespace as the separator + my $irp_arglist = $2; + $irp_arglist =~ s/,/ /g; + $irp_arglist =~ s/^\s+//; + @irp_args = split(/\s+/, $irp_arglist); + } elsif ($line =~ /\.irpc\s+([\d\w\.]+)\s*(.*)/) { + $in_irp = 1; + $num_repts = 1; + @rept_lines = ("\n"); + $irp_param = $1; + + my $irp_arglist = $2; + $irp_arglist =~ s/,/ /g; + $irp_arglist =~ s/^\s+//; + @irp_args = split(//, $irp_arglist); + } elsif ($line =~ /\.endr/) { + my @prev_rept_lines = @rept_lines; + my $prev_in_irp = $in_irp; + my @prev_irp_args = @irp_args; + my $prev_irp_param = $irp_param; + my $prev_num_repts = $num_repts; + @rept_lines = (); + $in_irp = 0; + @irp_args = ''; + + if ($prev_in_irp != 0) { + foreach my $i (@prev_irp_args) { + foreach my $origline (@prev_rept_lines) { + my $line = $origline; + $line =~ s/\\$prev_irp_param/$i/g; + $line =~ s/\\\(\)//g; # remove \() + parse_line($line); + } + } + } else { + for (1 .. $prev_num_repts) { + foreach my $origline (@prev_rept_lines) { + my $line = $origline; + parse_line($line); + } + } + } + } elsif ($line =~ /(\S+:|)\s*([\w\d\.]+)\s*(.*)/ && exists $macro_lines{$2}) { + handle_serialized_line($1); + my $macro = $2; + + # commas are optional here too, but are syntactically important because + # parameters can be blank + my @arglist = split(/,/, $3); + my @args; + my @args_seperator; + + my $comma_sep_required = 0; + foreach (@arglist) { + # allow arithmetic/shift operators in macro arguments + $_ =~ s/\s*(\+|-|\*|\/|<<|>>|<|>)\s*/$1/g; + + my @whitespace_split = split(/\s+/, $_); + if (!@whitespace_split) { + push(@args, ''); + push(@args_seperator, ''); + } else { + foreach (@whitespace_split) { + #print ("arglist = \"$_\"\n"); + if (length($_)) { + push(@args, $_); + my $sep = $comma_sep_required ? "," : " "; + push(@args_seperator, $sep); + #print ("sep = \"$sep\", arg = \"$_\"\n"); + $comma_sep_required = 0; + } + } + } + + $comma_sep_required = 1; + } + + my %replacements; + if ($macro_args_default{$macro}){ + %replacements = %{$macro_args_default{$macro}}; + } + + # construct hashtable of text to replace + foreach my $i (0 .. $#args) { + my $argname = $macro_args{$macro}[$i]; + my @macro_args = @{ $macro_args{$macro} }; + if ($args[$i] =~ m/=/) { + # arg=val references the argument name + # XXX: I'm not sure what the expected behaviour if a lot of + # these are mixed with unnamed args + my @named_arg = split(/=/, $args[$i]); + $replacements{$named_arg[0]} = $named_arg[1]; + } elsif ($i > $#{$macro_args{$macro}}) { + # more args given than the macro has named args + # XXX: is vararg allowed on arguments before the last? + $argname = $macro_args{$macro}[-1]; + if ($argname =~ s/:vararg$//) { + #print "macro = $macro, args[$i] = $args[$i], args_seperator=@args_seperator, argname = $argname, arglist[$i] = $arglist[$i], arglist = @arglist, args=@args, macro_args=@macro_args\n"; + #$replacements{$argname} .= ", $args[$i]"; + $replacements{$argname} .= "$args_seperator[$i] $args[$i]"; + } else { + die "Too many arguments to macro $macro"; + } + } else { + $argname =~ s/:vararg$//; + $replacements{$argname} = $args[$i]; + } + } + + my $count = $macro_count++; + + # apply replacements as regex + foreach (@{$macro_lines{$macro}}) { + my $macro_line = $_; + # do replacements by longest first, this avoids wrong replacement + # when argument names are subsets of each other + foreach (reverse sort {length $a <=> length $b} keys %replacements) { + $macro_line =~ s/\\$_/$replacements{$_}/g; + } + if ($altmacro) { + foreach (reverse sort {length $a <=> length $b} keys %replacements) { + $macro_line =~ s/\b$_\b/$replacements{$_}/g; + } + } + $macro_line =~ s/\\\@/$count/g; + $macro_line =~ s/\\\(\)//g; # remove \() + parse_line($macro_line); + } + } else { + handle_serialized_line($line); + } +} + +sub is_arm_register { + my $name = $_[0]; + if ($name eq "lr" or + $name eq "ip" or + $name =~ /^[rav]\d+$/) { + return 1; + } + return 0; +} + +sub is_aarch64_register { + my $name = $_[0]; + if ($name =~ /^[xw]\d+$/) { + return 1; + } + return 0; +} + +sub handle_local_label { + my $line = $_[0]; + my $num = $_[1]; + my $dir = $_[2]; + my $target = "$num$dir"; + if ($dir eq "b") { + $line =~ s/\b$target\b/$last_temp_labels{$num}/g; + } else { + my $name = "temp_label_$temp_label_next"; + $temp_label_next++; + push(@{$next_temp_labels{$num}}, $name); + $line =~ s/\b$target\b/$name/g; + } + return $line; +} + +sub handle_serialized_line { + my $line = $_[0]; + + # handle .previous (only with regard to .section not .subsection) + if ($line =~ /\.(section|text|const_data)/) { + push(@sections, $line); + } elsif ($line =~ /\.previous/) { + if (!$sections[-2]) { + die ".previous without a previous section"; + } + $line = $sections[-2]; + push(@sections, $line); + } + + $thumb = 1 if $line =~ /\.code\s+16|\.thumb/; + $thumb = 0 if $line =~ /\.code\s+32|\.arm/; + + # handle ldr , = + if ($line =~ /(.*)\s*ldr([\w\s\d]+)\s*,\s*=(.*)/ and $as_type ne "armasm") { + my $label = $literal_labels{$3}; + if (!$label) { + $label = "Literal_$literal_num"; + $literal_num++; + $literal_labels{$3} = $label; + } + $line = "$1 ldr$2, $label\n"; + } elsif ($line =~ /\.ltorg/ and $as_type ne "armasm") { + $line .= ".align 2\n"; + foreach my $literal (keys %literal_labels) { + $line .= "$literal_labels{$literal}:\n $literal_expr $literal\n"; + } + %literal_labels = (); + } + + # handle GNU as pc-relative relocations for adrp/add + if ($line =~ /(.*)\s*adrp([\w\s\d]+)\s*,\s*#?:pg_hi21:([^\s]+)/ and $as_type =~ /^apple-/) { + $line = "$1 adrp$2, ${3}\@PAGE\n"; + } elsif ($line =~ /(.*)\s*add([\w\s\d]+)\s*,([\w\s\d]+)\s*,\s*#?:lo12:([^\s]+)/ and $as_type =~ /^apple-/) { + $line = "$1 add$2, $3, ${4}\@PAGEOFF\n"; + } + + # thumb add with large immediate needs explicit add.w + if ($thumb and $line =~ /add\s+.*#([^@]+)/) { + $line =~ s/add/add.w/ if eval_expr($1) > 255; + } + + # mach-o local symbol names start with L (no dot) + $line =~ s/(? with ic as conditional code + if ($cond =~ /^(|$arm_cond_codes)$/) { + if (exists $thumb_labels{$label}) { + print ASMFILE ".thumb_func $label\n"; + } else { + $call_targets{$label}++; + } + } + } + + # @l -> lo16() @ha -> ha16() + $line =~ s/,\s+([^,]+)\@l\b/, lo16($1)/g; + $line =~ s/,\s+([^,]+)\@ha\b/, ha16($1)/g; + + # move to/from SPR + if ($line =~ /(\s+)(m[ft])([a-z]+)\s+(\w+)/ and exists $ppc_spr{$3}) { + if ($2 eq 'mt') { + $line = "$1${2}spr $ppc_spr{$3}, $4\n"; + } else { + $line = "$1${2}spr $4, $ppc_spr{$3}\n"; + } + } + + if ($line =~ /\.unreq\s+(.*)/) { + if (defined $neon_alias_reg{$1}) { + delete $neon_alias_reg{$1}; + delete $neon_alias_type{$1}; + return; + } elsif (defined $aarch64_req_alias{$1}) { + delete $aarch64_req_alias{$1}; + return; + } + } + # old gas versions store upper and lower case names on .req, + # but they remove only one on .unreq + if ($fix_unreq) { + if ($line =~ /\.unreq\s+(.*)/) { + $line = ".unreq " . lc($1) . "\n"; + $line .= ".unreq " . uc($1) . "\n"; + } + } + + if ($line =~ /(\w+)\s+\.(dn|qn)\s+(\w+)(?:\.(\w+))?(\[\d+\])?/) { + $neon_alias_reg{$1} = "$3$5"; + $neon_alias_type{$1} = $4; + return; + } + if (scalar keys %neon_alias_reg > 0 && $line =~ /^\s+v\w+/) { + # This line seems to possibly have a neon instruction + foreach (keys %neon_alias_reg) { + my $alias = $_; + # Require the register alias to match as an invididual word, not as a substring + # of a larger word-token. + if ($line =~ /\b$alias\b/) { + $line =~ s/\b$alias\b/$neon_alias_reg{$alias}/g; + # Add the type suffix. If multiple aliases match on the same line, + # only do this replacement the first time (a vfoo.bar string won't match v\w+). + $line =~ s/^(\s+)(v\w+)(\s+)/$1$2.$neon_alias_type{$alias}$3/; + } + } + } + + if ($arch eq "aarch64" or $as_type eq "armasm") { + # clang's integrated aarch64 assembler in Xcode 5 does not support .req/.unreq + if ($line =~ /\b(\w+)\s+\.req\s+(\w+)\b/) { + $aarch64_req_alias{$1} = $2; + return; + } + foreach (keys %aarch64_req_alias) { + my $alias = $_; + # recursively resolve aliases + my $resolved = $aarch64_req_alias{$alias}; + while (defined $aarch64_req_alias{$resolved}) { + $resolved = $aarch64_req_alias{$resolved}; + } + $line =~ s/\b$alias\b/$resolved/g; + } + } + if ($arch eq "aarch64") { + # fix missing aarch64 instructions in Xcode 5.1 (beta3) + # mov with vector arguments is not supported, use alias orr instead + if ($line =~ /^(\d+:)?\s*mov\s+(v\d[\.{}\[\]\w]+),\s*(v\d[\.{}\[\]\w]+)\b\s*$/) { + $line = "$1 orr $2, $3, $3\n"; + } + # movi 16, 32 bit shifted variant, shift is optional + if ($line =~ /^(\d+:)?\s*movi\s+(v[0-3]?\d\.(?:2|4|8)[hsHS])\s*,\s*(#\w+)\b\s*$/) { + $line = "$1 movi $2, $3, lsl #0\n"; + } + # Xcode 5 misses the alias uxtl. Replace it with the more general ushll. + # Clang 3.4 misses the alias sxtl too. Replace it with the more general sshll. + # armasm64 also misses these instructions. + if ($line =~ /^(\d+:)?\s*(s|u)xtl(2)?\s+(v[0-3]?\d\.[248][hsdHSD])\s*,\s*(v[0-3]?\d\.(?:2|4|8|16)[bhsBHS])\b\s*$/) { + $line = "$1 $2shll$3 $4, $5, #0\n"; + } + # clang 3.4 and armasm64 do not automatically use shifted immediates in add/sub + if (($as_type eq "clang" or $as_type eq "armasm") and + $line =~ /^(\d+:)?(\s*(?:add|sub)s?) ([^#l]+)#([\d\+\-\*\/ <>]+)\s*$/) { + my $imm = eval $4; + if ($imm > 4095 and not ($imm & 4095)) { + $line = "$1 $2 $3#" . ($imm >> 12) . ", lsl #12\n"; + } + } + if ($ENV{GASPP_FIX_XCODE5}) { + if ($line =~ /^\s*bsl\b/) { + $line =~ s/\b(bsl)(\s+v[0-3]?\d\.(\w+))\b/$1.$3$2/; + $line =~ s/\b(v[0-3]?\d)\.$3\b/$1/g; + } + if ($line =~ /^\s*saddl2?\b/) { + $line =~ s/\b(saddl2?)(\s+v[0-3]?\d\.(\w+))\b/$1.$3$2/; + $line =~ s/\b(v[0-3]?\d)\.\w+\b/$1/g; + } + if ($line =~ /^\s*dup\b.*\]$/) { + $line =~ s/\bdup(\s+v[0-3]?\d)\.(\w+)\b/dup.$2$1/g; + $line =~ s/\b(v[0-3]?\d)\.[bhsdBHSD](\[\d\])$/$1$2/g; + } + } + } + + if ($as_type eq "armasm") { + # Also replace variables set by .set + foreach (keys %symbols) { + my $sym = $_; + $line =~ s/\b$sym\b/$symbols{$sym}/g; + } + + # Handle function declarations and keep track of the declared labels + if ($line =~ s/^\s*\.func\s+(\w+)/$1 PROC/) { + $labels_seen{$1} = 1; + } + + if ($line =~ s/^\s*(\d+)://) { + # Convert local labels into unique labels. armasm (at least in + # RVCT) has something similar, but still different enough. + # By converting to unique labels we avoid any possible + # incompatibilities. + + my $num = $1; + foreach (@{$next_temp_labels{$num}}) { + $line = "$_\n" . $line; + } + @next_temp_labels{$num} = (); + my $name = "temp_label_$temp_label_next"; + $temp_label_next++; + # The matching regexp above removes the label from the start of + # the line (which might contain an instruction as well), readd + # it on a separate line above it. + $line = "$name:\n" . $line; + $last_temp_labels{$num} = $name; + } + + if ($line =~ s/^\s*(\w+):/$1/) { + # Skip labels that have already been declared with a PROC, + # labels must not be declared multiple times. + return if (defined $labels_seen{$1}); + $labels_seen{$1} = 1; + } elsif ($line !~ /(\w+) PROC/) { + # If not a label, make sure the line starts with whitespace, + # otherwise ms armasm interprets it incorrectly. + $line =~ s/^[\.\w]/\t$&/; + } + + + # Check branch instructions + if ($line =~ /(?:^|\n)\s*(\w+\s*:\s*)?(bl?x?\.?([^\s]{2})?(\.w)?)\s+(\w+)/) { + my $instr = $2; + my $cond = $3; + my $width = $4; + my $target = $5; + # Don't interpret e.g. bic as b with ic as conditional code + if ($cond !~ /^(|$arm_cond_codes)$/) { + # Not actually a branch + } elsif ($target =~ /^(\d+)([bf])$/) { + # The target is a local label + $line = handle_local_label($line, $1, $2); + $line =~ s/\b$instr\b/$&.w/ if $width eq "" and $arch eq "arm"; + } elsif (($arch eq "arm" and !is_arm_register($target)) or + ($arch eq "aarch64" and !is_aarch64_register($target))) { + $call_targets{$target}++; + } + } elsif ($line =~ /(?:^|\n)\s*(\w+\s*:\s*)?(cbn?z|adr|tbz)\s+(\w+)\s*,(\s*#\d+\s*,)?\s*(\w+)/) { + my $instr = $2; + my $reg = $3; + my $bit = $4; + my $target = $5; + if ($target =~ /^(\d+)([bf])$/) { + # The target is a local label + $line = handle_local_label($line, $1, $2); + } else { + $call_targets{$target}++; + } + # Convert tbz with a wX register into an xX register, + # due to armasm64 bugs/limitations. + if ($instr eq "tbz" and $reg =~ /w\d+/) { + my $xreg = $reg; + $xreg =~ s/w/x/; + $line =~ s/\b$reg\b/$xreg/; + } + } elsif ($line =~ /^\s*.h?word.*\b\d+[bf]\b/) { + while ($line =~ /\b(\d+)([bf])\b/g) { + $line = handle_local_label($line, $1, $2); + } + } + + # ALIGN in armasm syntax is the actual number of bytes + if ($line =~ /\.(?:p2)?align\s+(\d+)/) { + my $align = 1 << $1; + $line =~ s/\.(?:p2)?align\s+(\d+)/ALIGN $align/; + } + # Convert gas style [r0, :128] into armasm [r0@128] alignment specification + $line =~ s/\[([^\[,]+),?\s*:(\d+)\]/[$1\@$2]/g; + + # armasm treats logical values {TRUE} and {FALSE} separately from + # numeric values - logical operators and values can't be intermixed + # with numerical values. Evaluate ! and (a <> b) into numbers, + # let the assembler evaluate the rest of the expressions. This current + # only works for cases when ! and <> are used with actual constant numbers, + # we don't evaluate subexpressions here. + + # Evaluate ! + while ($line =~ /!\s*(\d+)/g) { + my $val = ($1 != 0) ? 0 : 1; + $line =~ s/!(\d+)/$val/; + } + # Evaluate (a > b) + while ($line =~ /\(\s*(\d+)\s*([<>])\s*(\d+)\s*\)/) { + my $val; + if ($2 eq "<") { + $val = ($1 < $3) ? 1 : 0; + } else { + $val = ($1 > $3) ? 1 : 0; + } + $line =~ s/\(\s*(\d+)\s*([<>])\s*(\d+)\s*\)/$val/; + } + + if ($arch eq "arm") { + # Change a movw... #:lower16: into a mov32 pseudoinstruction + $line =~ s/^(\s*)movw(\s+\w+\s*,\s*)\#:lower16:(.*)$/$1mov32$2$3/; + # and remove the following, matching movt completely + $line =~ s/^\s*movt\s+\w+\s*,\s*\#:upper16:.*$//; + + if ($line =~ /^\s*mov32\s+\w+,\s*([a-zA-Z]\w*)/) { + $import_symbols{$1}++; + } + + # Misc bugs/deficiencies: + # armasm seems unable to parse e.g. "vmov s0, s1" without a type + # qualifier, thus add .f32. + $line =~ s/^(\s+(?:vmov|vadd))(\s+s\d+\s*,\s*s\d+)/$1.f32$2/; + } elsif ($arch eq "aarch64") { + # Convert ext into ext8; armasm64 seems to require it named as ext8. + $line =~ s/^(\s+)ext(\s+)/$1ext8$2/; + + # Pick up targets from ldr x0, =sym+offset + if ($line =~ /^\s*ldr\s+(\w+)\s*,\s*=([a-zA-Z]\w*)(.*)$/) { + my $reg = $1; + my $sym = $2; + my $offset = eval_expr($3); + if ($offset < 0 and $ENV{GASPP_ARMASM64_SKIP_NEG_OFFSET}) { + # armasm64 in VS < 15.6 is buggy with ldr x0, =sym+offset where the + # offset is a negative value; it does write a negative + # offset into the literal pool as it should, but the + # negative offset only covers the lower 32 bit of the 64 + # bit literal/relocation. + # Thus remove the offset and apply it manually with a sub + # afterwards. + $offset = -$offset; + $line = "\tldr $reg, =$sym\n\tsub $reg, $reg, #$offset\n"; + } + $import_symbols{$sym}++; + } + + # armasm64 (currently) doesn't support offsets on adrp targets, + # even though the COFF format relocations (and the linker) + # supports it. Therefore strip out the offsets from adrp and + # add :lo12: (in case future armasm64 would start handling it) + # and add an extra explicit add instruction for the offset. + if ($line =~ s/(adrp\s+\w+\s*,\s*(\w+))([\d\+\-\*\/\(\) <>]+)?/\1/) { + $import_symbols{$2}++; + } + if ($line =~ s/(add\s+(\w+)\s*,\s*\w+\s*,\s*):lo12:(\w+)([\d\+\-\*\/\(\) <>]+)?/\1\3/) { + my $reg = $2; + my $sym = $3; + my $offset = eval_expr($4); + $line .= "\tadd $reg, $reg, #$offset\n" if $offset > 0; + $import_symbols{$sym}++; + } + + # Convert e.g. "add x0, x0, w0, uxtw" into "add x0, x0, w0, uxtw #0", + # or "ldr x0, [x0, w0, uxtw]" into "ldr x0, [x0, w0, uxtw #0]". + $line =~ s/(uxt[whb]|sxt[whb])(\s*\]?\s*)$/\1 #0\2/i; + + # Convert "mov x0, v0.d[0]" into "umov x0, v0.d[0]" + $line =~ s/\bmov\s+[xw]\d+\s*,\s*v\d+\.[ds]/u$&/i; + + # Convert "ccmp w0, #0, #0, ne" into "ccmpne w0, #0, #0", + # and "csel w0, w0, w0, ne" into "cselne w0, w0, w0". + $line =~ s/(ccmp|csel)\s+([xw]\w+)\s*,\s*([xw#]\w+)\s*,\s*([xw#]\w+)\s*,\s*($arm_cond_codes)/\1\5 \2, \3, \4/; + + # Convert "cinc w0, w0, ne" into "cincne w0, w0". + $line =~ s/(cinc)\s+([xw]\w+)\s*,\s*([xw]\w+)\s*,\s*($arm_cond_codes)/\1\4 \2, \3/; + + # Convert "cset w0, lo" into "csetlo w0" + $line =~ s/(cset)\s+([xw]\w+)\s*,\s*($arm_cond_codes)/\1\3 \2/; + + if ($ENV{GASPP_ARMASM64_SKIP_PRFUM}) { + # Strip out prfum; armasm64 (VS < 15.5) fails to assemble any + # variant/combination of prfum tested so far, but since it is + # a prefetch instruction it can be skipped without changing + # results. + $line =~ s/prfum.*\]//; + } + + # Convert "ldrb w0, [x0, #-1]" into "ldurb w0, [x0, #-1]". + # Don't do this for forms with writeback though. + if ($line =~ /(ld|st)(r[bh]?)\s+(\w+)\s*,\s*\[\s*(\w+)\s*,\s*#([^\]]+)\s*\][^!]/) { + my $instr = $1; + my $suffix = $2; + my $target = $3; + my $base = $4; + my $offset = eval_expr($5); + if ($offset < 0) { + $line =~ s/$instr$suffix/${instr}u$suffix/; + } + } + + if ($ENV{GASPP_ARMASM64_INVERT_SCALE}) { + # Instructions like fcvtzs and scvtf store the scale value + # inverted in the opcode (stored as 64 - scale), but armasm64 + # in VS < 15.5 stores it as-is. Thus convert from + # "fcvtzs w0, s0, #8" into "fcvtzs w0, s0, #56". + if ($line =~ /(?:fcvtzs|scvtf)\s+(\w+)\s*,\s*(\w+)\s*,\s*#(\d+)/) { + my $scale = $3; + my $inverted_scale = 64 - $3; + $line =~ s/#$scale/#$inverted_scale/; + } + } + + # Convert "ld1 {v0.4h-v3.4h}" into "ld1 {v0.4h,v1.4h,v2.4h,v3.4h}" + if ($line =~ /(?:ld|st)\d\s+({\s*v(\d+)\.(\d[bhsdBHSD])\s*-\s*v(\d+)\.(\d[bhsdBHSD])\s*})/) { + my $regspec = $1; + my $reg1 = $2; + my $layout1 = $3; + my $reg2 = $4; + my $layout2 = $5; + if ($layout1 eq $layout2) { + my $new_regspec = "{"; + foreach my $i ($reg1 .. $reg2) { + $new_regspec .= "," if ($i > $reg1); + $new_regspec .= "v$i.$layout1"; + } + $new_regspec .= "}"; + $line =~ s/$regspec/$new_regspec/; + } + } + } + # armasm is unable to parse &0x - add spacing + $line =~ s/&0x/& 0x/g; + } + + if ($force_thumb) { + # Convert register post indexing to a separate add instruction. + # This converts e.g. "ldr r0, [r1], r2" into "ldr r0, [r1]", + # "add r1, r1, r2". + $line =~ s/((?:ldr|str)[bh]?)\s+(\w+),\s*\[(\w+)\],\s*(\w+)/$1 $2, [$3]\n\tadd $3, $3, $4/g; + + # Convert "mov pc, lr" into "bx lr", since the former only works + # for switching from arm to thumb (and only in armv7), but not + # from thumb to arm. + $line =~ s/mov\s*pc\s*,\s*lr/bx lr/g; + + # Convert stmdb/ldmia/stmfd/ldmfd/ldm with only one register into a plain str/ldr with post-increment/decrement. + # Wide thumb2 encoding requires at least two registers in register list while all other encodings support one register too. + $line =~ s/stm(?:db|fd)\s+sp!\s*,\s*\{([^,-]+)\}/str $1, [sp, #-4]!/g; + $line =~ s/ldm(?:ia|fd)?\s+sp!\s*,\s*\{([^,-]+)\}/ldr $1, [sp], #4/g; + + # Convert muls into mul+cmp + $line =~ s/muls\s+(\w+),\s*(\w+)\,\s*(\w+)/mul $1, $2, $3\n\tcmp $1, #0/g; + + # Convert "and r0, sp, #xx" into "mov r0, sp", "and r0, r0, #xx" + $line =~ s/and\s+(\w+),\s*(sp|r13)\,\s*#(\w+)/mov $1, $2\n\tand $1, $1, #$3/g; + + # Convert "ldr r0, [r0, r1, lsl #6]" where the shift is >3 (which + # can't be handled in thumb) into "add r0, r0, r1, lsl #6", + # "ldr r0, [r0]", for the special case where the same address is + # used as base and target for the ldr. + if ($line =~ /(ldr[bh]?)\s+(\w+),\s*\[\2,\s*(\w+),\s*lsl\s*#(\w+)\]/ and $4 > 3) { + $line =~ s/(ldr[bh]?)\s+(\w+),\s*\[\2,\s*(\w+),\s*lsl\s*#(\w+)\]/add $2, $2, $3, lsl #$4\n\t$1 $2, [$2]/; + } + + $line =~ s/\.arm/.thumb/x; + } + + # comment out unsupported directives + $line =~ s/\.type/$comm$&/x if $as_type =~ /^(apple-|armasm)/; + $line =~ s/\.func/$comm$&/x if $as_type =~ /^(apple-|clang)/; + $line =~ s/\.endfunc/$comm$&/x if $as_type =~ /^(apple-|clang)/; + $line =~ s/\.endfunc/ENDP/x if $as_type =~ /armasm/; + $line =~ s/\.ltorg/$comm$&/x if $as_type =~ /^(apple-|clang)/; + $line =~ s/\.ltorg/LTORG/x if $as_type eq "armasm"; + $line =~ s/\.size/$comm$&/x if $as_type =~ /^(apple-|armasm)/; + $line =~ s/\.fpu/$comm$&/x if $as_type =~ /^(apple-|armasm)/; + $line =~ s/\.arch/$comm$&/x if $as_type =~ /^(apple-|clang|armasm)/; + $line =~ s/\.object_arch/$comm$&/x if $as_type =~ /^(apple-|armasm)/; + $line =~ s/.section\s+.note.GNU-stack.*/$comm$&/x if $as_type =~ /^(apple-|armasm)/; + + $line =~ s/\.syntax/$comm$&/x if $as_type =~ /armasm/; + + $line =~ s/\.hword/.short/x; + + if ($as_type =~ /^apple-/) { + # the syntax for these is a little different + $line =~ s/\.global/.globl/x; + # also catch .section .rodata since the equivalent to .const_data is .section __DATA,__const + $line =~ s/(.*)\.rodata/.const_data/x; + $line =~ s/\.int/.long/x; + $line =~ s/\.float/.single/x; + } + if ($as_type eq "apple-gas") { + $line =~ s/vmrs\s+APSR_nzcv/fmrx r15/x; + } + if ($as_type eq "armasm") { + $line =~ s/\.global/EXPORT/x; + $line =~ s/\.extern/IMPORT/x; + $line =~ s/\.int/dcd/x; + $line =~ s/\.long/dcd/x; + $line =~ s/\.float/dcfs/x; + $line =~ s/\.word/dcd/x; + $line =~ s/\.short/dcw/x; + $line =~ s/\.byte/dcb/x; + $line =~ s/\.quad/dcq/x; + $line =~ s/\.ascii/dcb/x; + $line =~ s/\.asciz(.*)$/dcb\1,0/x; + $line =~ s/\.thumb/THUMB/x; + $line =~ s/\.arm/ARM/x; + # The alignment in AREA is the power of two, just as .align in gas + $line =~ s/\.text/AREA |.text|, CODE, READONLY, ALIGN=4, CODEALIGN/; + $line =~ s/(\s*)(.*)\.ro?data/$1AREA |.rdata|, DATA, READONLY, ALIGN=5/; + $line =~ s/\.data/AREA |.data|, DATA, ALIGN=5/; + } + if ($as_type eq "armasm" and $arch eq "arm") { + $line =~ s/fmxr/vmsr/; + $line =~ s/fmrx/vmrs/; + $line =~ s/fadds/vadd.f32/; + } + if ($as_type eq "armasm" and $arch eq "aarch64") { + # Convert "b.eq" into "beq" + $line =~ s/\bb\.($arm_cond_codes)\b/b\1/; + } + + # catch unknown section names that aren't mach-o style (with a comma) + if ($as_type =~ /apple-/ and $line =~ /.section ([^,]*)$/) { + die ".section $1 unsupported; figure out the mach-o section name and add it"; + } + + print ASMFILE $line; +} + +if ($as_type ne "armasm") { + print ASMFILE ".text\n"; + print ASMFILE ".align 2\n"; + foreach my $literal (keys %literal_labels) { + print ASMFILE "$literal_labels{$literal}:\n $literal_expr $literal\n"; + } + + map print(ASMFILE ".thumb_func $_\n"), + grep exists $thumb_labels{$_}, keys %call_targets; +} else { + map print(ASMFILE "\tIMPORT $_\n"), + grep ! exists $labels_seen{$_}, (keys %call_targets, keys %import_symbols); + + print ASMFILE "\tEND\n"; +} + +close(INPUT) or exit 1; +close(ASMFILE) or exit 1; +if ($as_type eq "armasm" and ! defined $ENV{GASPP_DEBUG}) { + print STDERR join(" ", @gcc_cmd)."\n" if $verbose; + system(@gcc_cmd) == 0 or die "Error running assembler"; +} + +END { + unlink($tempfile) if defined $tempfile; +} +#exit 1 diff --git a/submodules/libtgvoip/OngoingCallThreadLocalContext.h b/submodules/libtgvoip/OngoingCallThreadLocalContext.h index c1216a3935..374c9dc6a2 100644 --- a/submodules/libtgvoip/OngoingCallThreadLocalContext.h +++ b/submodules/libtgvoip/OngoingCallThreadLocalContext.h @@ -18,7 +18,8 @@ typedef NS_ENUM(int32_t, OngoingCallState) { OngoingCallStateInitializing, OngoingCallStateConnected, - OngoingCallStateFailed + OngoingCallStateFailed, + OngoingCallStateReconnecting }; typedef NS_ENUM(int32_t, OngoingCallNetworkType) { diff --git a/submodules/libtgvoip/OngoingCallThreadLocalContext.mm b/submodules/libtgvoip/OngoingCallThreadLocalContext.mm index 995f3d9e91..c98bcf146f 100644 --- a/submodules/libtgvoip/OngoingCallThreadLocalContext.mm +++ b/submodules/libtgvoip/OngoingCallThreadLocalContext.mm @@ -374,6 +374,9 @@ static int callControllerDataSavingForType(OngoingCallDataSaving type) { case tgvoip::STATE_FAILED: callState = OngoingCallStateFailed; break; + case tgvoip::STATE_RECONNECTING: + callState = OngoingCallStateReconnecting; + break; default: break; } diff --git a/submodules/rlottie/rlottie b/submodules/rlottie/rlottie index a09896b3e7..0ee2e9c584 160000 --- a/submodules/rlottie/rlottie +++ b/submodules/rlottie/rlottie @@ -1 +1 @@ -Subproject commit a09896b3e72e76681c12e80572a7d570108cf885 +Subproject commit 0ee2e9c5843257ccd11672611829b9bb5d02aa98 diff --git a/submodules/ton/BUCK b/submodules/ton/BUCK index 1829fad04f..11b04572d7 100644 --- a/submodules/ton/BUCK +++ b/submodules/ton/BUCK @@ -46,7 +46,11 @@ genrule( "tonlib-src", "iOS.cmake" ], - bash = "sh $SRCDIR/build-ton.sh $OUT $SRCDIR $(location //submodules/openssl:openssl_build_merged)", + bash = +""" + export PATH=\"$PATH:$(location //third-party/cmake:cmake)/cmake-3.16.0/bin\" + sh $SRCDIR/build-ton.sh $OUT $SRCDIR $(location //submodules/openssl:openssl_build_merged) +""", out = "ton", visibility = [ "//submodules/ton:...", diff --git a/third-party/BUCK b/third-party/BUCK new file mode 100644 index 0000000000..e69de29bb2 diff --git a/third-party/cmake/BUCK b/third-party/cmake/BUCK new file mode 100644 index 0000000000..99d7db5a9f --- /dev/null +++ b/third-party/cmake/BUCK @@ -0,0 +1,21 @@ + +genrule( + name = "cmake", + srcs = [ + "cmake-3.16.0.tar.gz", + ], + bash = +""" + core_count="`sysctl -n hw.logicalcpu`" + mkdir -p "$OUT" + tar -xzf "$SRCDIR/cmake-3.16.0.tar.gz" --directory "$OUT" + pushd "$OUT/cmake-3.16.0" + ./bootstrap --parallel=$core_count -- -DCMAKE_BUILD_TYPE:STRING=Release + make -j $core_count + popd +""", + out = "cmake", + visibility = [ + "PUBLIC", + ] +) diff --git a/third-party/cmake/cmake-3.16.0.tar.gz b/third-party/cmake/cmake-3.16.0.tar.gz new file mode 100644 index 0000000000..af3e739dab Binary files /dev/null and b/third-party/cmake/cmake-3.16.0.tar.gz differ diff --git a/third-party/yasm/BUCK b/third-party/yasm/BUCK new file mode 100644 index 0000000000..eade17fbf2 --- /dev/null +++ b/third-party/yasm/BUCK @@ -0,0 +1,24 @@ + +genrule( + name = "yasm", + srcs = [ + "yasm-1.3.0.tar.gz", + ], + bash = +""" + core_count="`sysctl -n hw.logicalcpu`" + mkdir -p "$OUT" + tar -xzf "$SRCDIR/yasm-1.3.0.tar.gz" --directory "$OUT" + pushd "$OUT/yasm-1.3.0" + mkdir build + cd build + export PATH=\"$PATH:$(location //third-party/cmake:cmake)/cmake-3.16.0/bin\" + cmake .. -DYASM_BUILD_TESTS=OFF -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF + make -j $core_count + popd +""", + out = "yasm", + visibility = [ + "PUBLIC", + ] +) diff --git a/third-party/yasm/yasm-1.3.0.tar.gz b/third-party/yasm/yasm-1.3.0.tar.gz new file mode 100644 index 0000000000..665692ba80 Binary files /dev/null and b/third-party/yasm/yasm-1.3.0.tar.gz differ diff --git a/tools/buck-build/prepare_buck_source.sh b/tools/buck-build/prepare_buck_source.sh index ebf91824a2..1b8db8da32 100644 --- a/tools/buck-build/prepare_buck_source.sh +++ b/tools/buck-build/prepare_buck_source.sh @@ -12,6 +12,23 @@ fi mkdir -p "$target_directory" +jdk_archive_name="jdk.tar.gz" +jdk_archive_path="$target_directory/$jdk_archive_name" +jdk_unpacked_path="$target_directory/jdk8u232-b09" +jdk_url="https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u232-b09/OpenJDK8U-jdk_x64_mac_hotspot_8u232b09.tar.gz" + +if [ ! -f "$jdk_archive_path" ]; then + echo "Fetching JDK 8" + curl "$jdk_url" -L -o "$target_directory/jdk.tar.gz" +fi + +if [ ! -d "$jdk_unpacked_path" ]; then + echo "Unpacking JDK 8" + pushd "$target_directory" + tar -xf "$jdk_archive_name" + popd +fi + patch_file="$(ls *.patch | head -1)" patch_path="$(pwd)/$patch_file" @@ -39,10 +56,7 @@ git reset --hard "$commit_sha" git apply --check "$patch_path" git apply "$patch_path" -ant - -./bin/buck build --show-output buck - -#result_path="$(pwd)/buck-out/gen/programs/buck.pex" +PATH="$PATH:$jdk_unpacked_path/Contents/Home/bin" ant +PATH="$PATH:$jdk_unpacked_path/Contents/Home/bin" ./bin/buck build --show-output buck cd "$dir" diff --git a/tools/ipadiff.py b/tools/ipadiff.py index 11455dd713..e64a561f50 100644 --- a/tools/ipadiff.py +++ b/tools/ipadiff.py @@ -51,8 +51,6 @@ def remove_codesign_files(files): continue if re.match('Frameworks/.*\\.framework/_CodeSignature/CodeResources', f): continue - if f == 'Frameworks/ModernProto.framework/ModernProto': - continue result.add(f) return result @@ -163,7 +161,7 @@ def is_plist(file1): def diff_plists(file1, file2): - remove_properties = ['UISupportedDevices', 'DTAppStoreToolsBuild', 'MinimumOSVersion', 'BuildMachineOSBuild'] + remove_properties = ['UISupportedDevices', 'DTAppStoreToolsBuild', 'MinimumOSVersion', 'BuildMachineOSBuild', 'CFBundleVersion'] clean1_properties = '' clean2_properties = '' @@ -181,11 +179,6 @@ def diff_plists(file1, file2): if data1 == data2: return 'equal' else: - with open('lhs.plist', 'wb') as f: - f.write(str.encode(data1)) - with open('rhs.plist', 'wb') as f: - f.write(str.encode(data2)) - sys.exit(1) return 'not_equal' @@ -285,6 +278,7 @@ def ipadiff(self_base_path, ipa1, ipa2): print('Different files in ' + ipa1 + ' and ' + ipa2) for relative_file_path in different_files: print(' ' + relative_file_path) + sys.exit(1) else: if len(encrypted_files) != 0 or len(watch_ipa1_files) != 0 or len(plugin_ipa1_files) != 0: print('IPAs are equal, except for the files that can\'t currently be checked:') diff --git a/tools/main.cpp b/tools/main.cpp index 99c513eda5..540eee9687 100644 --- a/tools/main.cpp +++ b/tools/main.cpp @@ -175,16 +175,54 @@ static void writeDataToFile(std::vector const &data, std::string const static std::vector stripSwiftSymbols(std::string const &file) { std::string command; - command += "xcrun strip -ST -o /dev/stdout \""; + + command += "xcrun bitcode_strip \""; command += file; - command += "\" 2> /dev/null"; - + command += "\" -r -o \""; + command += file; + command += ".stripped\""; + uint8_t buffer[128]; - std::vector result; + FILE *pipe = popen(command.c_str(), "r"); if (!pipe) { throw std::runtime_error("popen() failed!"); } + while (true) { + size_t readBytes = fread(buffer, 1, 128, pipe); + if (readBytes <= 0) { + break; + } + } + pclose(pipe); + + command = ""; + command += "codesign --remove-signature \""; + command += file; + command += ".stripped\""; + + pipe = popen(command.c_str(), "r"); + if (!pipe) { + throw std::runtime_error("popen() failed!"); + } + while (true) { + size_t readBytes = fread(buffer, 1, 128, pipe); + if (readBytes <= 0) { + break; + } + } + pclose(pipe); + + command = ""; + command += "xcrun strip -ST -o /dev/stdout \""; + command += file; + command += ".stripped\" 2> /dev/null"; + + std::vector result; + pipe = popen(command.c_str(), "r"); + if (!pipe) { + throw std::runtime_error("popen() failed!"); + } while (true) { size_t readBytes = fread(buffer, 1, 128, pipe); if (readBytes <= 0) { @@ -193,6 +231,23 @@ static std::vector stripSwiftSymbols(std::string const &file) { result.insert(result.end(), buffer, buffer + readBytes); } pclose(pipe); + + command = ""; + command += "rm \""; + command += file; + command += ".stripped\""; + + pipe = popen(command.c_str(), "r"); + if (!pipe) { + throw std::runtime_error("popen() failed!"); + } + while (true) { + size_t readBytes = fread(buffer, 1, 128, pipe); + if (readBytes <= 0) { + break; + } + } + pclose(pipe); return result; }